← Back to Suite Creator

Developer Guide: Working on this Solution

Solution architecture, the AOT runtime projects, and the full build → deploy → execute flow.

This document explains how the solution fits together — which projects do what, how they're wired into each other at build time, and what actually happens end-to-end when a packager builds a suite and it runs on a target device. Read this before making changes that cross project boundaries.

The big picture

There are really two applications in this repo, plus the glue between them:

  1. The Creator (SuiteCreatorAvalonia + SuiteCreatorAvalonia.Desktop) — the Avalonia desktop app packagers use to design a suite. It runs on the packager's machine.
  2. The runtime — everything that ends up inside a built suite and runs on the target device: SuiteSfxStub, SuiteExecutor, SuiteUserPopup, and SuiteProgressPopup.

The Creator doesn't reference the runtime projects as normal project references. Instead it bundles their published executables as content, and stitches them into a single self-extracting suite exe at build time.

flowchart TD
    subgraph Packager["Packager's machine — Suite Creator app"]
        UI["SuiteCreatorAvalonia<br/>(editor UI)"] -->|Build pressed| SB["SuiteBuilder"]
        SB -->|"serialises config"| CFG["SuiteConfig.scfg (JSON)"]
        SB -->|"copies from app's SuiteExec folder"| RT["SuiteExecutor.exe<br/>+ Popup exes"]
        SB -->|"copies"| PKGS["Installers, files,<br/>scripts, logos,<br/>popconfig.json"]
        CFG --> ZIP["SuiteFiles.zip"]
        RT --> ZIP
        PKGS --> ZIP
        ZIP -->|"appended to SuiteSfxStub.exe<br/>+ length + 'SUFX' magic trailer"| EXE["Single suite .exe<br/>+ detection rule"]
    end

    EXE -->|"deployed via Intune / SCCM / etc."| STUB

    subgraph Target["Target device — suite runs"]
        STUB["SuiteSfxStub.exe<br/>(reads its own tail)"] -->|"extracts zip"| CACHE["%windir%\\<br/>SuiteInstallerCache\\<br/>{suiteGUID}<br/>(hardened, admin-only)"]
        STUB -->|"installs/updates newest"| PF["%ProgramFiles%\\<br/>SuiteExecutor\\<br/>SuiteExecutor.exe<br/>+ popup exes"]
        STUB -->|launches| EXEC["SuiteExecutor"]
        EXEC <-->|"launches if conditions met<br/>reports back: continue / defer / skip"| POP["SuiteUserPopup.exe<br/>(run as logged-in user)"]
        EXEC --> RUN["Run stages: closures,<br/>packages, files, registry,<br/>env, scripts..."]
        EXEC -.->|"if enabled"| PROG["SuiteProgressPopup.exe"]
        RUN --> DET["Write detection registry key<br/>+ uninstall media"]
    end
      

Project map

ProjectRoleNotes
SuiteCreatorAvaloniaThe editor UI (views, viewmodels, SuiteBuilder, settings)Avalonia 12, MVVM via CommunityToolkit
SuiteCreatorAvalonia.DesktopThin desktop entry point for the CreatorAlso owns the MSBuild targets that bundle the runtime exes (see below)
SuiteExecutorRuns the suite on the target device: stages, packages, events, popups, detection, fail-safe, deferralAOT-published. Partial Suite class split across Suite.*.cs files by concern
SuiteUserPopupThe end-user warning/deferral popup shown before the suite runsAOT-published Avalonia app. Communicates its result back via exit codes
SuiteProgressPopupThe install-progress window end users seeAOT-published Avalonia app
SuiteSfxStubSelf-extracting bootstrapper the payload zip is appended toAOT-published. Becomes the actual built suite exe
SuiteCreatorModelsShared models & enums, including SuiteExecConfig (the .scfg schema)Referenced by both Creator and Executor — schema changes affect both sides
SuiteOperationsThe *ExecEvent implementations (file, registry, env, cert, driver, PowerShell...)Shared execution logic used by the Executor
LoggerCommon loggingReferenced everywhere, including the popups
MSITools / MSIxToolsMSI / MSIX inspection and handling
SystemToolsNative helpers: robocopy wrapper, PowerShell invocation, environment refresh, native methods
UserToolsImpersonation — launching processes as the logged-in user from an elevated/SYSTEM contextThis is how the Executor shows popups in the user's session
SuiteUIToolsUI helpers (image loading, shell/browser helpers) shared by the Avalonia apps

How the runtime exes get into the Creator

This is the most important non-obvious wiring in the repo.

SuiteExecutor, SuiteUserPopup, SuiteProgressPopup, and SuiteSfxStub each have a Properties/PublishProfiles/FolderProfile.pubxml that publishes them self-contained, win-x64, Native AOT to {project}\bin\publish\win-x64\net10.0-windows10.0.22000.0\. AOT is used deliberately to keep the files small, because these exes get compiled into every built suite.

SuiteCreatorAvalonia.Desktop.csproj contains custom MSBuild targets (CopyOtherPublishFilesOutput, and a publish-time equivalent) that run after every build:

  1. For each of the four runtime projects, check whether its publish folder exists and has files.
  2. If not, run dotnet publish ... /p:PublishProfile=FolderProfile for it automatically.
  3. Copy the published files (minus .pdb etc.) into a SuiteExec subfolder of the Creator's own output directory.

At runtime, SuiteBuilder reads everything it bundles from {AppContext.BaseDirectory}\SuiteExec.

Gotcha: the auto-publish only triggers when a publish folder is missing. If you change code in SuiteExecutor or the popups, the Creator will keep bundling the stale published copies until you either re-publish that project yourself (dotnet publish -c Release /p:PublishProfile=FolderProfile) or delete its bin\publish folder and rebuild the Desktop project. If a runtime fix "doesn't seem to do anything", check this first.
AOT gotcha: because these projects are trimmed + AOT, reflection-heavy code needs care. The Avalonia popups keep a TrimmerRootDescriptor.xml to protect types the trimmer would otherwise strip. Test runtime changes against the published AOT output, not just an F5 debug build — trimming failures only show up in the published exe.

What happens when a packager presses Build

All of this lives in SuiteCreatorAvalonia/Services/SuiteBuilder.cs:

  1. The in-memory project is converted to a SuiteExecConfig (from SuiteCreatorModels) and serialised as JSON to SuiteConfig.scfg in a temp folder.
  2. The Creator's SuiteExec folder (the Executor and its files, excluding the stub and popup exes) is robocopied into the temp folder.
  3. Package installers, file-event payloads, scripts, certificates, drivers, etc. are copied in.
  4. If a popup is enabled, a Popup\ subfolder is created containing SuiteUserPopup.exe, SuiteProgressPopup.exe, popconfig.json, the suite logo, and the company logo.
  5. The temp folder is zipped, and the zip is appended to a copy of SuiteSfxStub.exe with a trailer of [zip bytes][int32 zip length][int32 magic 'SUFX'].
  6. The result is a single exe, plus a detection rule string (a registry key under the suite's UpgradeCode with a DisplayVersion comparison) the packager pastes into their deployment tool.

What happens on the target device

  1. SuiteSfxStub.exe runs (elevated, from the deployment tool). It reads its own tail to find the zip and extracts it to %windir%\SuiteInstallerCache\{suiteGUID} — a deliberately admin-only, hardened location, since everything the elevated Executor later runs comes from here. The Popup folder gets Users read/execute so the popup can run in the user's session. If the cache already holds a strictly newer version of the same suite, extraction is skipped.
  2. The stub then installs or updates SuiteExecutor.exe, SuiteUserPopup.exe, and SuiteProgressPopup.exe into %ProgramFiles%\SuiteExecutor, keeping the newest version, and launches the installed Executor pointing at the extracted SuiteConfig.scfg.
  3. SuiteExecutor takes over: acquires a mutex, works out the action (Deployment / Removal / Rollback), optionally registers a fail-safe scheduled task so an interrupted run restarts on next boot/logon, and evaluates the popup conditions (global admin condition first, then the per-suite condition — both PowerShell scripts that must output $True/$False).
  4. If a popup should show, and Pause during meeting is enabled, the Executor first runs SuiteUserPopup.exe --CheckMeetingStatus — a headless mode that never shows a window, just queries WASAPI for whether any currently-active capture device (not just the system default - a call app can be pointed at any specific mic) has a live session, and exits 0/1/2 (not in use / in use / check failed). If in use, the Executor (Suite.MeetingDetection.cs) creates a repeating scheduled task (SuiteMeetingWait_{UpgradeCode}, if one doesn't already exist) that re-invokes the Executor with --reminder every minute for up to 4 hours, then exits 1602 immediately — the deployment tool is never kept waiting, since the actual rechecking happens via Task Scheduler's own repetition rather than the Executor blocking in place. Each --reminder recheck lands back in ExecutePopup (the same run mode the deferral mechanism already uses, which skips the deferral gate); if the mic is still in use it exits 1602 again immediately, otherwise it deletes the recheck task and continues. If a meeting runs past the 4-hour cap, the popup shows anyway as a fail-safe. Once clear, the Executor launches SuiteUserPopup.exe as the logged-in user (via UserTools impersonation, since the Executor itself runs elevated/SYSTEM) for the real popup. It reports back through exit codes: continue, device locked, error, defer (with a reminder time on stdout), or timer expired. A deferral schedules a reminder task and exits with 1602; the reminder task later re-runs the executor with --reminder so it proceeds instead of re-deferring.
  5. The suite then executes: service/process closures, then stages of packages and events (implemented in SuiteOperations), with SuiteProgressPopup.exe showing progress if enabled. Finally it writes the detection registry key, drops uninstall media, refreshes the environment if configured, and cleans up.

Useful Executor flags when debugging: --debug (debug builds only — waits in a loop until a debugger attaches, so you can F5-attach to a run started by the stub), --reminder, --failsafe, plus internal modes --failsafe-unblock-all and --watch-pid. SuiteExecutor/DebugAssets holds a sample SuiteConfig.scfg for running the Executor directly from the IDE.

Conventions & tips