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:
- Capture per-session diagnostic detail — every action with a timestamp.
- Bound size so it doesn't grow forever on a long-running install.
- Keep the most recent N sessions — old sessions roll out, recent ones stay.
- Order newest-first — when a user opens the log, they see the most recent session at the top.
- 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:
- Read the existing file.
- Find session marker positions.
- If there are more than
MaxSessionsToRetain - 1markers, slice off the tail (the oldest sessions). - Save the trimmed content as
_priorContentfor 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
UpdateClientper-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
MaxSessionsToRetainconstant. - Change file location — modify
LogSubfolderNameandLogFileNameconstants, or the logic inResolveLogDirectory. - Add a new log level — add a public method that calls
WriteEntrywith 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.