Architecture Overview¶
This page describes how the Pulse Dashboard Client is built and the design decisions behind it. It's aimed at developers maintaining the launcher, not end users.
What problem the launcher solves¶
Running an executable directly off a network share has three problems:
-
The file is locked. While anyone is running the app, the .exe and its DLLs are held open, which prevents an admin from deploying a replacement. In practice, this means updates can only happen at off-hours, and any running session blocks all rollouts.
-
Cold-start latency. Every launch loads the executable and dependencies over the network, even though most of those bytes haven't changed since yesterday.
-
Robustness. A network blip mid-execution can manifest as inscrutable .NET errors when the runtime tries to load a not-yet-cached assembly.
The launcher solves all three by:
- Maintaining a local copy of the application in
%LOCALAPPDATA%\Leahy\Pulse Dashboard 5\. - Synchronizing the local copy with the server before each run.
- Running the local copy. The server copy is left alone, free to be replaced at any time.
High-level flow¶
flowchart TD
Start([Launcher starts]) --> ParseFlags[Parse command-line flags]
ParseFlags --> CreateLogger[Create UpdateLogger]
CreateLogger --> ResolveServer{Server path<br/>resolvable?}
ResolveServer -->|Yes| CleanCheck{/clean flag?}
ResolveServer -->|No| BrowsePrompt[Prompt user to browse]
BrowsePrompt --> ResolveServer
CleanCheck -->|Yes| ConfirmClean{Confirm or<br/>/yes flag?}
CleanCheck -->|No| Sync[Run sync]
ConfirmClean -->|Yes| RunClean[Wipe client folder<br/>except exemptions]
ConfirmClean -->|No| Exit([Exit])
RunClean --> CleanOK{Clean<br/>succeeded?}
CleanOK -->|Yes| Sync
CleanOK -->|No| RetryClean{User retries?}
RetryClean -->|Yes| RunClean
RetryClean -->|No| Exit
Sync --> SyncOK{Sync<br/>succeeded?}
SyncOK -->|No| RetrySync{User retries?}
RetrySync -->|Yes| Sync
RetrySync -->|No| Exit
SyncOK -->|Yes| LaunchCheck{/skiplaunch?}
LaunchCheck -->|Yes| Exit
LaunchCheck -->|No| Launch[Start Pulse Dashboard.exe]
Launch --> Exit
Components¶
The launcher consists of these source files. Each has a single, focused responsibility:
Forms¶
-
Main.vb— the main form (runs hidden as the message-loop anchor) plus all the orchestration logic. Contains startup logic, server-path resolution, retry loops, and failure dialogs. The bulk of user-visible decision-making happens here. -
MainSplash.vb— the splash window shown during sync. Just displays status text and exposes a thread-safeSetStatusmethod. -
BrowseExecutableLocation.vb— folder browser dialog for when the server path can't be auto-detected.
Services¶
-
ClientUpdater.vb— the sync engine. Knows how to compare files, copy them, retry on failure, verify post-copy, clean up orphans, and launch the application. Stateful per-run; does not know about the UI. -
UpdateLogger.vb— session-bounded log writer. Manages the log file's lifecycle, ordering (newest-first), and retention policy.
Models¶
UpdateResult.vb— aggregate outcome of an update run.FileCopyFailure.vb— single per-file copy failure detail.CleanResult.vb— aggregate outcome of a clean operation.CleanFailure.vb— single per-item clean failure detail.
Helpers¶
-
CommandLineHelper.vb— flag parsing, named flag constants, and the rule for what's launcher-only vs forwarded to the child app. -
ServerPathFile.vb— read/write theServerPath.txtfile that persists the server location.
Why this is architected the way it is¶
A few non-obvious decisions are worth explaining for future maintainers.
Separation of concerns¶
ClientUpdater does not know about WinForms, the splash, or dialogs. It
raises a StatusChanged event that callers can subscribe to, and returns
result objects that describe what happened. This means the same updater
could be driven from a console app, a service, or a test harness without
modification.
Main.vb is the integration layer that wires the updater to the UI and
makes the user-facing decisions (retry? cancel? confirm?).
Why per-file errors don't throw¶
Originally the updater threw on the first file failure, which aborted the
entire sync. This made it impossible to know whether you had 1 broken file or
100. The current design captures each failure into the UpdateResult and
keeps going, surfacing the complete list to the user at the end. A user
needing to close 5 different applications to unlock 5 different files can do
it once instead of in 5 round-trip retries.
Why we have a custom logger instead of using NLog or similar¶
This is a small, single-purpose tool. Adding a logging framework dependency
would dwarf the rest of the project. The custom UpdateLogger is ~150 lines,
has zero dependencies, gives us exactly the session-bounded retention and
newest-first ordering we wanted, and is easy to read.
Why log writes rewrite the whole file¶
To maintain newest-first ordering, each new log line has to be inserted at the top of the current session's block, which sits at the top of the file. The simplest correct implementation is to keep the current session's lines in memory and rewrite the file on every write.
The cost is bounded — log files are capped at ~25KB in practice, and modern file systems handle small writes via the OS cache so the perceived latency is microseconds. See the sync process docs.
Why we don't use a manifest file on the server¶
A manifest (version.txt or similar) would let the launcher skip the file
walk entirely when nothing's changed. We considered it but didn't implement
it because:
- The current scan phase is fast enough (sub-second on a healthy network).
- Adding a manifest requires changes to whoever deploys to the server, and introduces a new failure mode (stale manifest).
- The launcher's current approach is self-contained and correct without external help.
If the launcher is ever deployed at a scale where scan latency becomes an issue, a manifest would be the right next step. Until then, the simpler design wins.
What hasn't been built (and why)¶
Things that aren't here, with the reasoning:
- Atomic update (stage to a temp folder, then rename). Would close the "partial sync leaves a mixed state" gap, but the retry-on-failure plus "don't launch if any copy failed" rule handles this in practice.
- Self-update of the launcher itself. The launcher doesn't replace its own .exe — it skips it during copy and preserves it during cleanup. If the launcher itself ships a bug, every existing copy is stuck with it. Solving this elegantly is the "who updates the updater" problem; not worth the complexity at this scale.
- Centralized telemetry writing back to the server share. Useful for fleet-wide insight but adds a new failure mode and a privacy surface. Logs stay local for now.
- Code-signing verification. The launcher trusts whatever's on the server
share. Anyone with write access to the share can replace
Pulse Dashboard.exewith anything. Acceptable for an internal tool with controlled share permissions; would be a hard requirement for anything external.