From 06911d27aa386e22e18834ac8fc8d2f5928ba28c Mon Sep 17 00:00:00 2001 From: forbes Date: Sat, 14 Feb 2026 13:28:08 -0600 Subject: [PATCH] docs: Create module bootstrap sequence (#148) Document the two-phase bootstrap: console-phase addon loading via exec(), GUI-phase workbench registration, six deferred QTimer callbacks (kc_format, silo origin, auth panel, first-start check, activity panel, update checker), and the Gitea releases API polling with skip/interval logic. --- docs/src/SUMMARY.md | 2 + docs/src/reference/create-module-bootstrap.md | 132 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 docs/src/reference/create-module-bootstrap.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 00868e29ac..4008f237e2 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -50,6 +50,8 @@ # Reference - [Configuration](./reference/configuration.md) +- [Create Module Bootstrap](./reference/create-module-bootstrap.md) +- [Datum Creator](./reference/datum-creator.md) - [Glossary](./reference/glossary.md) # C++ API Reference diff --git a/docs/src/reference/create-module-bootstrap.md b/docs/src/reference/create-module-bootstrap.md new file mode 100644 index 0000000000..db67137eed --- /dev/null +++ b/docs/src/reference/create-module-bootstrap.md @@ -0,0 +1,132 @@ +# Create Module Bootstrap Sequence + +The Create module (`src/Mod/Create/`) is the integration layer that bootstraps Kindred Create's addons and deferred services. It runs in two phases: console (headless) and GUI. + +**Source files:** + +- `src/Mod/Create/Init.py` -- console-phase addon loading +- `src/Mod/Create/InitGui.py` -- GUI-phase workbench loading and deferred timers +- `src/Mod/Create/update_checker.py` -- Gitea releases API polling +- `src/Mod/Create/kc_format.py` -- .kc file format round-trip preservation + +## Loading Mechanism + +FreeCAD loads module `Init.py` and `InitGui.py` files using **direct `exec()`**, not Python's import system. The C++ startup code scans `Mod/` directories, reads each file as text, compiles it, and executes it in an isolated namespace: + +```python +source = init_py.read_text(encoding="utf-8") +code = compile(source, init_py, "exec") +exec(code) +``` + +Modules loaded this way are **not** added to `sys.modules`. Each execution gets a fresh globals dict with no caching or dependency resolution. + +## Phase 1: Console (`Init.py`) + +Runs immediately at application startup, before any GUI is available. + +`setup_kindred_addons()` discovers and loads two built-in addons from `mods/`: + +| Addon | Path | Purpose | +|-------|------|---------| +| ztools | `mods/ztools/ztools/` | Part design, assembly, and sketcher tools | +| silo | `mods/silo/freecad/` | Database integration and version control | + +For each addon: + +1. Build the path from `FreeCAD.getHomePath() + "mods/" + addon_path` +2. Add to `sys.path` if not already present +3. Find `Init.py` in the addon directory +4. Execute it with `exec(compile(source, init_file, "exec"), exec_globals)` + +Failures are logged to `FreeCAD.Console.PrintWarning()` and do not prevent other addons from loading. + +## Phase 2: GUI (`InitGui.py`) + +Runs after the console phase, when `FreeCADGui` is available. + +### Synchronous + +`setup_kindred_workbenches()` loads `InitGui.py` from the same two addons (ztools, silo), registering their workbenches and GUI commands. + +### Deferred Timers + +Six `QTimer.singleShot` calls stagger initialization to let the GUI event loop settle: + +| Delay | Function | Purpose | +|-------|----------|---------| +| 500 ms | `_register_kc_format()` | Register .kc file format observer | +| 1500 ms | `_register_silo_origin()` | Register Silo as a file origin | +| 2000 ms | `_setup_silo_auth_panel()` | Dock the Database Auth panel | +| 3000 ms | `_check_silo_first_start()` | Show settings dialog on first run | +| 4000 ms | `_setup_silo_activity_panel()` | Dock the Database Activity panel | +| 10000 ms | `_check_for_updates()` | Poll Gitea for new releases | + +Every timer callback is wrapped in `try/except`. Failures log to `FreeCAD.Console.PrintLog()` and do not crash the application. + +### Timer Details + +**_register_kc_format (500 ms)** -- Imports `kc_format` and calls `kc_format.register()`, which installs a `DocumentObserver` with `slotStartSaveDocument` / `slotFinishSaveDocument` hooks. These cache `silo/` ZIP entries before FreeCAD's C++ save rewrites the archive, then re-inject them afterwards. Only processes `.kc` files. + +**_register_silo_origin (1500 ms)** -- Imports `silo_origin` and calls `silo_origin.register_silo_origin()`, making Silo available in the origin selector dropdown alongside LocalFileOrigin. + +**_setup_silo_auth_panel (2000 ms)** -- Creates a `QDockWidget` titled "Database Auth" containing a `SiloAuthDockWidget` from `silo_commands`. Docked on the right side of the main window. Guards against duplicates by checking if the widget already exists. + +**_check_silo_first_start (3000 ms)** -- Reads `User parameter:BaseApp/Preferences/Mod/KindredSilo`. On the very first launch (when `FirstStartChecked` is not set), runs the `Silo_Settings` command if no API URL is configured. + +**_setup_silo_activity_panel (4000 ms)** -- Creates a "Database Activity" dock widget showing the 20 most recent Silo items (part number, description, date). Falls back to "(Unable to connect to Silo database)" on connection failure. + +**_check_for_updates (10000 ms)** -- Calls `update_checker._run_update_check()` in the background. + +## Update Checker + +`update_checker.py` polls the Gitea releases API for newer versions. + +### Check Decision (`_should_check`) + +Reads preferences from `User parameter:BaseApp/Preferences/Mod/KindredCreate/Update`: + +| Parameter | Type | Default | Purpose | +|-----------|------|---------|---------| +| `CheckEnabled` | bool | true | Master enable/disable | +| `CheckIntervalDays` | int | 1 | Minimum days between checks | +| `LastCheckTimestamp` | string | (none) | ISO timestamp of last check | +| `SkippedVersion` | string | (none) | Version the user chose to skip | + +A check runs only if enabled, the interval has elapsed, and the current version is not the skipped version. + +### Check Execution (`check_for_update`) + +1. Query `https://git.kindred-systems.com/api/v1/repos/kindred/create/releases?limit=10` (5 s timeout) +2. Filter out drafts, pre-releases, and the "latest" tag +3. Parse version tags (`v0.1.3` becomes tuple `(0, 1, 3)`) +4. Compare against `version.VERSION` from the Create module +5. If a newer version exists, log it to the console +6. Record `LastCheckTimestamp` + +The checker never shows a dialog -- it only logs to `FreeCAD.Console`. + +## Dependency Chain + +``` +FreeCAD startup + | + Init.py (exec'd, immediate) + +-- setup_kindred_addons() + | +-- ztools/Init.py (exec'd) + | +-- silo/freecad/Init.py (exec'd) + | + [GUI startup] + | + InitGui.py (exec'd, immediate) + +-- setup_kindred_workbenches() + | +-- ztools/InitGui.py (exec'd) + | +-- silo/freecad/InitGui.py (exec'd) + | + +-- QTimer 500ms --> kc_format.register() + +-- QTimer 1500ms --> silo_origin.register_silo_origin() + +-- QTimer 2000ms --> SiloAuthDockWidget + +-- QTimer 3000ms --> Silo first-start check + +-- QTimer 4000ms --> Database Activity panel + +-- QTimer 10000ms --> update_checker._run_update_check() +```