fix: console visibility, NSIS registry view, and Northstar LSX (#3)#3
Conversation
…ions (#3) Four independent bugs that combined to make Maxima invisible on TF2 launch in v0.2.1, plus a fifth that prevented Northstar online play. == maxima-cli/src/main.rs + Cargo.toml == Rewrote the startup prologue so each step happens in the right order: 1. Panic hook installed first — any subsequent crash writes to %LOCALAPPDATA%\Maxima\Logs\maxima-cli.panic.log before the process exits, giving a durable record even when no console is attached yet. 2. ensure_console_attached() now also rewires STD_OUTPUT_HANDLE / STD_ERROR_HANDLE / STD_INPUT_HANDLE to CONOUT$/CONIN$ via SetStdHandle after AllocConsole. Without this, the v0.2.1 console window opened but stayed blank: Rust's println! was still writing to the invalid handles inherited from maxima-bootstrap (a GUI-subsystem parent with no stdio). Added winapi features: processenv, fileapi, winbase, winnt. 3. main() is now a plain fn (no #[tokio::main]) that builds the tokio runtime manually with Runtime::new().block_on(startup(args)). Previously #[tokio::main] constructed the runtime before user code ran, so a panic in IOCP/thread-pool init under Wine would kill the process before AllocConsole or the logger ever executed. 4. init_logger_named() is called before Args::parse(). If clap exits on a bad argv it writes to stderr — which was the unattached pipe. Logger first means clap errors hit the file sink. == maxima-bootstrap/src/main.rs == Log the exit code of maxima-cli to maxima_execution.log when it exits non-zero. Previously bootstrap returned Ok(true) / "Result: Success" regardless of whether the child succeeded, making failures invisible in the one log file users are directed to check. == installer/maxima-setup.nsi == SetRegView 64 was set at line ~173 for HKLM WOW6432Node writes and never reset. The HKCR protocol-handler BackupProtocol calls and WriteRegStr ops that followed inherited view 64. On a 32-bit NSIS installer the default view is 32-bit; so v0.2.0 wrote HKCR\link2ea (etc.) under the 32-bit view. v0.2.1 wrote them under the 64-bit view. 32-bit Wine consumers (Titanfall2.exe, origin2:// emitters) resolve HKCR via the 32-bit view and saw the stale v0.2.0 entries or nothing — link2ea:// was never dispatched to maxima-bootstrap, so nothing appeared on launch. Fixes: - SetRegView default before BackupProtocol calls and before HKCR writes. - SetRegView default inside RestoreProtocol so uninstaller targets the same store. - Upgrade guard in BackupProtocol: if HKLM\Software\Maxima\InstallPath already exists (prior Maxima install), skip the backup phase. This prevents an upgrade from overwriting the original pre-Maxima EA handler backup with Maxima's own values, which would cause the uninstaller to "restore" a deleted binary. - Same guard via BackupValueUpgradeSafe for the two HKLM BackupValue calls (Origin\ClientPath, EA Desktop\InstallSuccessful). == maxima-lib/src/lsx/connection.rs + profile.rs == Port of catornot/Maxima@patch-external-lsx (upstream PR ArmchairDevelopers#42 by p0358). When Northstar is launched via Steam (steam.exe -applaunch 1237970 -northstar), the game starts without going through maxima-cli launch, so maxima.playing() is None when TF2's LSX module connects back to Maxima's TCP server. The old code shut the socket immediately and returned LSXConnectionError::GameContext — no LSX, no auth ticket, no online play. connection.rs: instead of dropping the connection when playing() is None, log a warning and continue. The PID/Kyber path only executes when a context exists. profile.rs: handle_set_presence_request unwrapped playing() unconditionally (line 93), panicking on external connections. Replace .unwrap() with a let-else that returns a harmless success response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request addresses critical launch and visibility issues in v0.2.1 by fixing NSIS registry view leaks, ensuring proper stdio reattachment after AllocConsole, and implementing a panic hook for better diagnostics. It also enables LSX connections for externally launched games. Feedback suggests improving error handling in the bootstrap process by returning errors instead of manual logging and using more idiomatic Rust patterns for PID checking.
| if !status.success() { | ||
| let temp_dir = std::env::temp_dir(); | ||
| let debug_log = temp_dir.join("maxima_execution.log"); | ||
| if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&debug_log) { | ||
| use std::io::Write; | ||
| let _ = writeln!( | ||
| file, | ||
| "maxima-cli (link2ea) exited non-zero: code={:?}", | ||
| status.code() | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
The manual logging of the non-zero exit code to maxima_execution.log is redundant because the handle_launch_args function already logs the result. By returning an error here, you can leverage the existing infrastructure. Additionally, ensure that if Stdio::piped() is used, the stderr output is captured and included in the error message to provide better diagnostic information.
if !status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("maxima-cli (link2ea) exited non-zero: code={:?}", status.code())
).into());
}References
- When capturing command output in Rust using Stdio::piped(), ensure that stderr is also read and incorporated into error reporting to avoid losing diagnostic information.
| if !status.success() { | ||
| let temp_dir = std::env::temp_dir(); | ||
| let debug_log = temp_dir.join("maxima_execution.log"); | ||
| if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&debug_log) { | ||
| use std::io::Write; | ||
| let _ = writeln!( | ||
| file, | ||
| "maxima-cli (origin2) exited non-zero: code={:?}", | ||
| status.code() | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Similar to the link2ea block, returning an error here is cleaner than manual logging. This ensures consistency and utilizes centralized logging. Note that if Stdio::piped() is used, stderr should be read and incorporated into the error reporting to avoid losing diagnostic information.
if !status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("maxima-cli (origin2) exited non-zero: code={:?}", status.code())
).into());
}References
- When capturing command output in Rust using Stdio::piped(), ensure that stderr is also read and incorporated into error reporting to avoid losing diagnostic information.
| if let Err(ref err) = pid { | ||
| warn!("Error while finding game PID: {}", err); | ||
| } else if pid.as_ref().unwrap() == &0 { | ||
| warn!("Failed to find PID through launch ID, things may not work!"); | ||
| } |
There was a problem hiding this comment.
While the unwrap() is technically safe here due to the preceding if let Err check, using a match statement on a reference is more idiomatic and robust against future refactoring, as it avoids unnecessary unwrapping.
| if let Err(ref err) = pid { | |
| warn!("Error while finding game PID: {}", err); | |
| } else if pid.as_ref().unwrap() == &0 { | |
| warn!("Failed to find PID through launch ID, things may not work!"); | |
| } | |
| match &pid { | |
| Err(err) => warn!("Error while finding game PID: {}", err), | |
| Ok(0) => warn!("Failed to find PID through launch ID, things may not work!"), | |
| _ => {} | |
| } |
Add a dedicated "Northstar online play" section to the README explaining: - Launch via Steam (-northstar flag), not NorthstarLauncher.exe - Required launch args: -noOriginStartup -multiple -northstar (identified by catornot; without -noOriginStartup Northstar hangs trying to start Origin in Wine) - Reference to catornot/flightcore-ng for the source Credit catornot in the Upstream section for the external LSX connection patch and the launch argument discovery. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three suggestions from the automated review, all accepted:
1. maxima-bootstrap (link2ea + origin2): replace manual log + Ok(true) on
non-zero maxima-cli exit with a proper Err return. handle_launch_args
already routes any Err from run() to maxima_execution.log and
maxima_bootstrap_error.log, so the manual writes were redundant. Returning
Err also makes failures visible as errors in the log instead of "Success".
2. maxima-lib/lsx/connection.rs: replace the if-let-Err / else-if-unwrap
pattern on the pid Result with a single idiomatic match &pid { ... }.
Avoids the technically-safe-but-stylistically-poor .unwrap().
No behavior change for the happy path; non-zero maxima-cli exits now
propagate as errors instead of being swallowed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem
After upgrading to v0.2.1, nothing appears when launching TF2 from Steam — no CLI window, no UI, no TUI. In earlier versions the CLI console at least popped up. Additionally, Northstar online play never worked because external LSX connections were rejected.
Root causes fixed (5 independent bugs)
1. NSIS
SetRegViewleak → protocol handlers in wrong registry view (most likely root cause of invisible launch)SetRegView 64was set forWOW6432Nodewrites and never reset before theHKCRprotocol handler writes. v0.2.0 used the default (32-bit) view; v0.2.1 wrotelink2ea://,qrc://,origin2://under the 64-bit view. 32-bit Wine consumers (TF2, Origin) resolve HKCR via the 32-bit view → stale/missing handlers →maxima-bootstrapnever invoked → nothing appears.Fix:
SetRegView defaultbefore allBackupProtocolcalls andHKCRwrites. Same fix insideRestoreProtocolin the uninstaller. Upgrade guard inBackupProtocolso upgrading over an existing install doesn't overwrite the original EA-handler backup with Maxima's own values.2.
AllocConsole()didn't reattach stdioAllocConsoleopens a console window but does not redirect existing std handles. Maxima-bootstrap is a GUI-subsystem process with null stdio; the child inherited those null handles. The console appeared butprintln!/ the logger wrote to dead pipes → blank window.Fix: After
AllocConsole, callSetStdHandlefor stdout/stderr/stdin pointing atCONOUT$/CONIN$.3.
#[tokio::main]ran beforeAllocConsoleThe tokio macro desugars to runtime construction before user code. A panic in IOCP/thread-pool init under Wine killed the process silently before any console existed.
Fix: Plain
fn main()that callsAllocConsole+ logger, then builds the runtime manually withRuntime::new().block_on(startup(args)).4.
Args::parse()before logger initClap parse errors wrote to the unattached stderr pipe — invisible.
Fix:
init_logger_named()beforeArgs::parse(). Panic hook installed first so even pre-logger crashes write to%LOCALAPPDATA%\Maxima\Logs\maxima-cli.panic.log.5. Bootstrap swallowed non-zero exit codes
maxima-clicrash → bootstrap logged "Result: Success". Made every other bug invisible inmaxima_execution.log.Fix: Check
status.success()afterwait()and log the exit code.6. External LSX connections rejected → Northstar online play broken
When Northstar launches via
steam.exe -applaunch 1237970 -northstar, the game starts withoutmaxima-cli launch, somaxima.playing()isNonewhen TF2's LSX module connects. The old code shut the socket immediately (LSXConnectionError::GameContext).handle_set_presence_requestalso called.unwrap()onplaying()→ panic.Fix: Port of
catornot/Maxima@patch-external-lsx(upstream PR ArmchairDevelopers#42 by p0358). Accept external connections with a warning; skip PID/Kyber path when no context. Safelet-elseinprofile.rs.Files changed
installer/maxima-setup.nsimaxima-cli/src/main.rsmaxima-cli/Cargo.tomlmaxima-bootstrap/src/main.rsmaxima-lib/src/lsx/connection.rsmaxima-lib/src/lsx/request/profile.rsCLAUDE.mdTest plan
link2ea://still dispatched tomaxima-bootstrapby checkingmaxima_execution.loggrows on TF2 launchlink2ea://%LOCALAPPDATA%\Maxima\Logs\maxima-cli.logis populated after a launch attemptsteam.exe -applaunch 1237970 -northstar) and confirm online play works🤖 Generated with Claude Code