Skip to content

feat(logging): add diagnostic crash log subsystem#5

Open
f1rep0wr wants to merge 1 commit into
Custom-Galactic-War:mainfrom
f1rep0wr:feat/diagnostic-logging
Open

feat(logging): add diagnostic crash log subsystem#5
f1rep0wr wants to merge 1 commit into
Custom-Galactic-War:mainfrom
f1rep0wr:feat/diagnostic-logging

Conversation

@f1rep0wr
Copy link
Copy Markdown
Contributor

@f1rep0wr f1rep0wr commented May 18, 2026

Summary

The launcher ships as a PyInstaller --windowed --onefile exe, so every existing print(...) writes to a stdout that doesn't exist and uncaught exceptions on Qt threads or in discord_game_sdk.dll die silently. This PR adds a stdlib-logging-based diagnostic subsystem so end-user bug reports become actionable, and migrates every existing print site to it.

  • New logger.pyRotatingFileHandler at data/logs/launcher.log (1 MB × 5), sys.excepthook + threading.excepthook + faulthandler.enable, session preamble (version/Python/platform/CWD/argv), 3-tier path fallback (<cwd>/data/logs%LOCALAPPDATA%%TEMP%), atexit cleanup of the faulthandler FD, RLock-guarded idempotency. A username-redaction filter runs on every log record so the file is shareable publicly.
  • print → logging migration across functions.py / mech_editor.py / discord_rpc.py / run.py. log.exception(...) inside except blocks so the full traceback lands on disk. Filesystem-sourced strings (filenames, paths) formatted with %r to defeat log injection via planted filenames.
  • show_error_box and prompt_for_new_game_folder_error_count increment moved adjacent to the dialog so a logging or Tk failure can never bump the counter without the user actually seeing the dialog (preserves the invariant InjectionThread at run.py:29 relies on).
  • run.py — adds an "Open Log File" IconButton that opens launcher.log in Notepad (deliberately not Explorer — spawning Explorer from a --uac-admin process is a documented UAC escalation surface). Adds a Session end (close). log line in closeEvent and removes a pre-existing duplicate self.rpc_manager.stop() call at the old run.py:307.
  • constants.py — new LAUNCHER_VERSION = "1.0.8" constant consumed by the log preamble; must stay in sync with version.txt's filevers tuple when bumping.

What this looks like for end users

1. A new icon under the Discord and Info icons. Same IconButton widget already used for the Info button. Placed at (10, 95) directly below it. If data/assets/log.png exists it's used; otherwise the widget renders its existing fallback (a primary-green bordered circle with a bold "L" letter, matching how the Info button falls back to "i" today). Tooltip on hover: "Open diagnostic log".

2. Clicking it opens launcher.log in Notepad. Two branches in _open_log_file (run.py:158-179):

  • Normal case (logging initialized + file exists on disk) → subprocess.Popen(["notepad.exe", log_path]). The user sees Notepad open with the full log.
  • Fallback case (logging never initialized, or the file is missing) → a plain QMessageBox.information saying "The diagnostic log has not been created yet, or the launcher failed to initialize logging. Check %TEMP%\e710-launcher-boot.log for clues if this is unexpected." This is deliberately QMessageBox.information, not show_error_box, because using the latter would bump _error_count from a benign UI affordance and taint the next CONNECT cycle.

3. Every existing error dialog now ends with a "Diagnostic logs folder:" footer. This goes through the modified show_error_box (functions.py). The footer is appended via a lazy from logger import get_log_dir so the dialog still renders even if logging is uninitialized (in which case no footer is appended — graceful degradation). End users filing a bug now have a path to copy + paste from without being told where to look.

4. Log file content is publicly shareable. The username-redaction filter rewrites every record (message, args, and materialized exception tracebacks) so paths like C:\Users\<actual-name>\AppData\Local\... become C:\Users\<user>\AppData\Local\... before they hit disk. Users can attach launcher.log directly to a Discord message or GitHub issue without scrubbing it first. faulthandler.log (native crash file) only contains the "armed at" timestamp markers under normal operation — no memory addresses or DLL paths leak.

Test plan

  • python -m py_compile on every touched file
  • Idempotency: setup_logging() called twice in one process keeps len(root.handlers) == 1; concurrent calls from 4 threads also produce 1 handler with no PermissionError
  • Fallback chain: marking data/logs/launcher.log read-only triggers fallback to %LOCALAPPDATA%\E-710 Launcher\logs
  • Privacy: scanned the produced launcher.log after a session + an artificially raised PermissionError with a home-directory path — zero occurrences of the actual Windows username
  • _error_count invariant: monkey-patched tk.Tk to raise, called prompt_for_new_game_folder, confirmed counter stays at 0 when no dialog could be shown
  • Hooks fire end-to-end: log.exception(...) writes a traceback; raising in a worker thread triggers threading.excepthook; preamble + all log levels land in the file
  • UI verified live on a dev machine: launcher boots, triggers show_antivirus_warning on a missing msvcp140.dll, the error dialog now ends with "Diagnostic logs folder:\nC:\projects\cgw-launcher\data\logs" — confirming the new footer is wired correctly through the lazy logger.get_log_dir import

@f1rep0wr f1rep0wr force-pushed the feat/diagnostic-logging branch from 08fd622 to faf43c7 Compare May 18, 2026 20:04
The launcher ships as a PyInstaller --windowed --onefile exe, so every
existing print(...) writes to a stdout that doesn't exist and uncaught
exceptions on Qt threads or in discord_game_sdk.dll die silently. This
adds a stdlib logging-based subsystem so end-user bug reports become
actionable.

What's new:

- logger.py — RotatingFileHandler at data/logs/launcher.log (1 MB x 5),
  sys.excepthook + threading.excepthook + faulthandler.enable, session
  preamble with version/platform/Python/argv/CWD, 3-tier path fallback
  (cwd -> %LOCALAPPDATA% -> %TEMP%), atexit cleanup, RLock-guarded
  idempotency. A username-redaction filter runs on every record so log
  files are shareable publicly (Windows username never appears, even
  inside exception tracebacks).

Refactors:

- functions.py / mech_editor.py / discord_rpc.py / run.py — every
  print(...) replaced with structured log.* calls. log.exception()
  inside except blocks so the full traceback lands on disk.
  Filesystem-sourced strings (filenames, paths) formatted with %r to
  prevent log-injection via planted filenames.

- show_error_box (functions.py) and prompt_for_new_game_folder — the
  _error_count increment is now only bumped after the Tk dialog
  actually shows, preserving the invariant InjectionThread relies on
  even when logging or Tk itself fails.

- run.py — adds an "Open Log File" IconButton that opens launcher.log
  in Notepad (not Explorer; spawning Explorer from an elevated process
  is a documented UAC escalation surface). Adds a "Session end" log
  line in closeEvent and removes a pre-existing duplicate
  rpc_manager.stop() call.

- constants.py — adds LAUNCHER_VERSION constant ("1.0.8") used by the
  log preamble; must be kept in sync with version.txt's filevers tuple
  when bumping.
@f1rep0wr f1rep0wr force-pushed the feat/diagnostic-logging branch from faf43c7 to d179c0a Compare May 18, 2026 20:10
@f1rep0wr f1rep0wr marked this pull request as ready for review May 18, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant