Skip to content

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:

  1. 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.

  2. Cold-start latency. Every launch loads the executable and dependencies over the network, even though most of those bytes haven't changed since yesterday.

  3. 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:

  1. Maintaining a local copy of the application in %LOCALAPPDATA%\Leahy\Pulse Dashboard 5\.
  2. Synchronizing the local copy with the server before each run.
  3. 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-safe SetStatus method.

  • 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 the ServerPath.txt file 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.exe with anything. Acceptable for an internal tool with controlled share permissions; would be a hard requirement for anything external.

See also