This page traces the runtime lifecycle of the Sucrose process stack: the cold start from Sucrose.Launcher.exe (single-instance gate → Configure() → tray icon + signal channel + Discord), how the wallpaper auto-restores on launch, the configuration helpers every process runs first, and the two teardown paths — "Exit" (leave the wallpaper running) versus "Close" (full stack teardown). It is a developer reference and complements the Architecture Overview (who exists) with when and in what order things happen.
sequenceDiagram
actor User
participant L as Launcher
participant Tray as TrayIconManager
participant Cmd as Commandog
participant Eng as Live engine
User->>L: run Sucrose.Launcher.exe
L->>L: OnStartup, SetLanguage(Culture)
L->>L: Instance.Basic mutex gate (first instance)
L->>L: Configure()
L->>Tray: TrayIconManager.Start()
Tray->>Cmd: Engine.Command(false) auto-restore
Cmd->>Eng: spawn Backgroundog, then Live engine
Tray->>Cmd: Reportdog.Command() flush reports
Tray->>Cmd: silent Update check (if Auto)
L->>L: LauncherManager.StartChannel (signal)
L->>L: Discord.Initialize() 5s timer
Sucrose.Launcher.exe (src/Launcher/Sucrose.Launcher/App.xaml.cs) is the process that starts when the user opens Sucrose. It is a WPF app with no main window — it lives entirely in the system tray, with ShutdownMode = ShutdownMode.OnExplicitShutdown.
The Launcher's OnStartup:
SRHR.SetLanguage(SMMG.Culture) — set the UI language from the saved Culture setting (default = the current UI two-letter ISO language).ShutdownMode = OnExplicitShutdown.SSSHI.Basic(SMMRM.Launcher, SMMRA.Launcher) (mutex {Sucrose-Wallpaper-Engine-Launcher}). See Single Instance Mutexes.true (this is the first instance), run Configure():SSLMI.TrayIconManager.Start() — create the tray icon + context menu.SSMI.LauncherManager.StartChannel(SSSSLSS.Handler) — start the Launcher signal channel (a FileSystemWatcher over Launcher.sgnl) using LauncherSignalService.Handler.Discord.Initialize() — start the Discord Rich Presence timer.false (an instance is already running), this second copy does not stay resident: it calls SSLCI.Command() (asks the running stack to open the Portal) and then Close()s itself.So double-launching the Launcher means "open the settings window," not "create a second tray icon."
In words:
User runs Sucrose.Launcher.exe
-> OnStartup: Instance.Basic("{...-Launcher}", "Sucrose.Launcher.exe") == true
-> Configure()
-> TrayIconManager.Start() (tray icon + menu; auto-restore wallpaper; Reportdog; update check)
-> LauncherManager.StartChannel(LauncherSignalService.Handler)
-> Discord.Initialize() (5s timer; presence if Discord running + enabled)
When TrayIconManager.Start() runs, it also performs three actions regardless of any user click:
SSLCE.Command(false) — Engine.Command(false): if a wallpaper is configured and no engine is currently running, start the wallpaper engine now. This is how the wallpaper auto-restores on launch — users do not re-apply each boot. Run.Start() reads the selected wallpaper's SucroseInfo JSON, maps its Type to the configured engine, and (when PerformanceCounter is on) first spawns Backgroundog via ✔Backgroundog✖<Backgroundog>, then spawns the engine via ✔Live✖<engine.exe>.SSLCRG.Command() — Reportdog.Command(): spawn Sucrose.Reportdog.exe (via Commandog) to flush pending telemetry / error reports.SMMU.Auto (auto-update on): SSLCU.Command(false) — a silent update check (spawns Sucrose.Update.exe via Commandog).Tray visibility honors the AppVisible setting (default true).
Every process's constructor or Main wires the global exception handlers first, then sets its thread culture, and finally runs the same two configuration helpers:
SSDHR.Configure() (Shared.Dependency.Helper.Runtime) — RELEASE-only. Points the process at the bundled private .NET runtime by setting environment variables DOTNET_ROOT (and the (x86)/(arm64) variants), DOTNET_MULTILEVEL_LOOKUP=0, DOTNET_ROLL_FORWARD=LatestMajor, and prepending the runtime folder <parent>/Sucrose.Runtime plus the app dir to PATH. (Debug builds use the machine's installed runtime — the path manipulation happens only in RELEASE.)SSDHG.Configure() (Shared.Dependency.Helper.Graphic) — calls EnsureHighPerformance(exe) so the process is registered for the high-performance GPU on multi-GPU laptops.The thread culture is set from the Culture setting (SHC.All = new CultureInfo(Culture, true)) before the two Configure() helpers above, and the global exception handlers are wired earlier still. WPF apps set ShutdownMode; the console apps (Commandog, Backgroundog, Reportdog) additionally set Console.InputEncoding/OutputEncoding = UTF8 so the ✔ (U+2714) / ✖ (U+2716) command delimiters are not mangled.
Build-level runtime/GC config from Directory.Build.targets (all non-library outputs): ThreadPoolMinThreads=10, ThreadPoolMaxThreads=500, UseWindowsThreadPool=true; ServerGarbageCollection=true, ConcurrentGarbageCollection=true, RetainVMGarbageCollection=false; TieredCompilation=true, InvariantGlobalization=false.
Discord.Initialize() starts a DispatcherTimer (InitializeTimer) ticking every 5 seconds. On each tick it checks whether the user enabled Discord (SMMH.DiscordConnect) and whether Discord.exe or DiscordPTB.exe is running (ApplicationNames = ["Discord.exe", "DiscordPTB.exe"]):
Client.Initialize() then SetPresence().ClearPresence() and stop the refresh timer.An optional auto-refresh timer (RefreshTimer) runs at interval SMMH.DiscordRefreshDelay seconds and re-randomizes the presence text/images. The Discord application id is 1126294965950103612 (library: DiscordRPC). On shutdown the Launcher's OnExit runs Discord.Dispose() → LauncherManager.StopChannel() → TrayIconManager.Dispose() → Close(). See Discord Rich Presence.
User clicks tray "Open" (or runs Launcher again)
-> Interface.Command() -> spawn Commandog "✔Interface✖<Portal.exe>"
-> Commandog: Process.Start(Portal.exe)
-> Portal OnStartup: Instance.Basic("{...-Portal}", "Sucrose.Portal.exe")
-> if first: build Generic Host (DI), Security.Apply(), host.Start() -> MainWindow
-> if already running: Close() (no second window)
✔RestartLive✖Unknown → Commandog stops the live engine + browser subprocesses, kills Sucrose.Backgroundog.exe, runs Run.Start(), waits 1500 ms, and retries if the engine is not running.✔Cycyling✖... → same as Reload but without killing Backgroundog.Killing/starting the engine is handled by Kill.Stop() / Kill.StopSubprocess() (which also resumes and kills msedgewebview2 / CefSharp.BrowserSubprocess child processes whose command line contains Sucrose) and Run.Start(). See Commandog Dispatcher.
The tray menu's last item label and behavior depend on the AppExit setting (default false), via Close.cs (src/Shared/Sucrose.Shared.Launcher/Command/Close.cs):
AppExit |
Tray label | Behavior |
|---|---|---|
false (default) |
"Exit" | Only the Launcher itself exits. The wallpaper engine and the background services keep running. |
true |
"Close" | A full teardown: stop the engine (Kill.Stop / StopSubprocess), then kill (if running), in order — Undo, Portal, Update, Property, Watchdog, Commandog, Reportdog, Backgroundog — release the tray, then Process.GetCurrentProcess().Kill() + Environment.Exit(0). |
Sucrose.Undo.exe is a separate full-removal path (the uninstaller), not the same as Close. See Undo Internals.
Any process throws
-> Watchdog.Start(app, ex, show, log)
-> spawn Commandog "✔Watchdog✖<Watchdog.exe>✖<Base64(app✖ex✖show✖log)>"
-> Watchdog: write Cache/Report/<guid>.json -> Kill(app) -> (if show) Dark/Light error dialog
... later ...
Reportdog loop -> upload Cache/Report/*.json (only if ExceptionData enabled)
Global exception handling is wired identically in Launcher / Portal / Watchdog / Property (handlers for ThreadException, FirstChanceException, UnhandledException, UnobservedTaskException, DispatcherUnhandledException). See Crash Reporting.
Instance.Basic gate used at startup.Run/Reload/Cycle commands are executed.
Wiki: Architecture Overview
Wiki: Commandog Dispatcher
Wiki: Discord Rich Presence
Wiki: Engines Overview
Wiki: Home
Wiki: Single Instance Mutexes
Wiki: Theme Tray Startup
Wiki: Troubleshooting Common
Wiki: Wallpaper Types
Wiki: _Sidebar