Skip to content

Logging System

The launcher's diagnostic log is the single most important diagnostic tool. Its design is documented here for anyone who needs to modify or debug it.

Goals

The logger needs to: 1. Capture per-session diagnostic detail — every action with a timestamp. 2. Bound size so it doesn't grow forever on a long-running install. 3. Keep the most recent N sessions — old sessions roll out, recent ones stay. 4. Order newest-first — when a user opens the log, they see the most recent session at the top. 5. Preserve crash data — if the launcher crashes mid-session, log entries up to the crash should still be on disk.

Implementation: UpdateLogger.vb

The class manages a single log file: Pulse Dashboard Client.log inside the Pulse Logs subfolder of the client directory.

Lifecycle

sequenceDiagram
    participant Main as Main.vb
    participant Logger as UpdateLogger
    participant Disk as Log file

    Main->>Logger: New(clientDirectory)
    Logger->>Disk: Read existing content
    Logger->>Logger: Trim to last 9 prior sessions
    Note over Logger: Capture _priorContent in memory
    Logger->>Logger: Seed buffer with new session header
    Logger->>Disk: Write [buffer][priorContent]
    Logger-->>Main: Constructor returns

    loop Each Info/Warn/Error call
        Main->>Logger: Info("...")
        Logger->>Logger: Append to in-memory buffer
        Logger->>Disk: Write [buffer][priorContent]
    end

    Main->>Logger: LogSessionDuration(...)
    Logger->>Logger: Append final line to buffer
    Logger->>Disk: Final write

File format

=== Session 20260501-181500 started at 2026-05-01 18:15:00 ===
18:15:00.103 [INFO ] Launcher running from: ...
18:15:00.110 [INFO ] Sync source (server): ...
18:15:00.450 [INFO ] Update finished in 350 ms.
18:15:00.812 [INFO ] Launched successfully.

=== Session 20260501-180350 started at 2026-05-01 18:03:50 ===
18:03:50.319 [INFO ] ...
...

=== Session 20260501-175800 started at 2026-05-01 17:58:00 ===
...

A session starts with a marker line (=== Session ... ===), then contains chronologically-ordered entries. Sessions are separated by blank lines.

The most recent session is always at the top of the file.

Why rewrite-on-every-write?

The simplest approach to "newest first" is to rewrite the entire file each time a log entry is added. Each rewrite is [current session buffer] + [prior content].

Alternatives considered:

Approach Issue
Append to end of file Newest entries end up at the bottom — wrong order.
Buffer everything in memory, write at session end Crash loses all log data — defeats the diagnostic purpose.
Write to a separate file per session More file I/O, harder to read all sessions in one place.
Use a logging framework (NLog, Serilog) Adds dependency for a feature we don't need at full scale.

Rewrite-on-write was chosen because: - The file is small (typically 10-50KB, capped at 5MB by a backstop). - Local disk writes of that size are sub-millisecond. - Each rewrite preserves all prior content, so a crash just leaves the file in whatever state it was at the last successful write.

The performance impact is negligible — see Sync process: log performance.

Retention policy

Two limits work together:

Session count limit

MaxSessionsToRetain = 10 (configurable constant). The 11th session pushes out the oldest.

Trimming happens once at startup: 1. Read the existing file. 2. Find session marker positions. 3. If there are more than MaxSessionsToRetain - 1 markers, slice off the tail (the oldest sessions). 4. Save the trimmed content as _priorContent for the rest of the session.

The new session, when it appends, brings the total back to 10.

Hard byte backstop

MaxLogBytes = 5 * 1024 * 1024 (5MB). If the existing file exceeds this on startup, it's discarded entirely instead of read and trimmed. This guards against a runaway session somehow producing pathological output.

In practice the byte backstop should never trigger.

Thread safety

Every public log method (Info, Warn, Error, LogSessionDuration) goes through WriteEntry, which takes a SyncLock _writeLock. This serializes writes from any thread.

In practice, the launcher logs from two threads: - UI thread — flag-setup logs, post-sync result logs, launch logs. - Worker thread — all UpdateClient per-file logging.

The lock prevents these from interleaving partial lines or corrupting the in-memory buffer.

Failure handling

The logger explicitly never throws out of any public method. If file I/O fails, the exception is caught and swallowed. The reasoning: logging is a diagnostic tool, and a failure to log should never cause the launcher to fail.

If log writes are silently failing, the user's first signal will be an empty or stale log file. The launcher itself will keep running and finish its mission.

Code layout

UpdateLogger.vb
├── Constants                       # MaxSessionsToRetain, MaxLogBytes, etc.
├── Fields
│   ├── _logFilePath                # Resolved path to the log file
│   ├── _writeLock                  # SyncLock object
│   ├── _sessionId                  # yyyyMMdd-HHmmss timestamp
│   ├── _sessionStopwatch           # For LogSessionDuration
│   ├── _currentSessionBuffer       # In-memory buffer of this session's lines
│   └── _priorContent               # Captured-once snapshot of trimmed prior sessions
├── Public API
│   ├── New(clientDirectory)        # Constructor: resolve path, trim, seed
│   ├── LogFilePath                 # Read-only property
│   ├── Info / Warn / Error         # Log entry methods
│   └── LogSessionDuration(label)   # Final timing entry
├── Private write path
│   ├── WriteEntry(level, message)  # Format and append, then flush
│   └── FlushToFile()               # Write [buffer][prior] to disk
└── Private startup helpers
    ├── ResolveLogDirectory()       # Pulse Logs subfolder, fallback to TEMP
    ├── LoadAndTrimPriorContent()   # Read existing file, trim to last 9
    └── FindSessionMarkerPositions  # Locate `=== Session ===` lines

When to make changes

If you need to: - Change retention count — adjust MaxSessionsToRetain constant. - Change file location — modify LogSubfolderName and LogFileName constants, or the logic in ResolveLogDirectory. - Add a new log level — add a public method that calls WriteEntry with a new prefix string. - Change the file format — be careful with SessionMarkerPrefix. The trim logic searches for that exact string to find session boundaries. If you change it, existing logs from the old format won't be parsed correctly on the next launch (the launcher will conservatively discard unparseable content).

What not to do

  • Don't write large amounts of data per log call. The 5MB backstop will truncate if a single session somehow produces that much, but it shouldn't ever come close. Each line should be one fact.
  • Don't add async I/O. It would complicate the threading model with no meaningful performance benefit. The log file is small enough to write synchronously.
  • Don't log inside tight inner loops unless the loop iteration is something the user genuinely needs to know about. Per-file copy logs are fine. Per-byte is not.