Skip to content

fix: console visibility, NSIS registry view, and Northstar LSX (#3)#3

Merged
AA-EION merged 3 commits into
masterfrom
fix/v0.2.1-launch-invisible
May 15, 2026
Merged

fix: console visibility, NSIS registry view, and Northstar LSX (#3)#3
AA-EION merged 3 commits into
masterfrom
fix/v0.2.1-launch-invisible

Conversation

@AA-EION
Copy link
Copy Markdown
Owner

@AA-EION AA-EION commented May 15, 2026

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 SetRegView leak → protocol handlers in wrong registry view (most likely root cause of invisible launch)

SetRegView 64 was set for WOW6432Node writes and never reset before the HKCR protocol handler writes. v0.2.0 used the default (32-bit) view; v0.2.1 wrote link2ea://, qrc://, origin2:// under the 64-bit view. 32-bit Wine consumers (TF2, Origin) resolve HKCR via the 32-bit view → stale/missing handlers → maxima-bootstrap never invoked → nothing appears.

Fix: SetRegView default before all BackupProtocol calls and HKCR writes. Same fix inside RestoreProtocol in the uninstaller. Upgrade guard in BackupProtocol so upgrading over an existing install doesn't overwrite the original EA-handler backup with Maxima's own values.

2. AllocConsole() didn't reattach stdio

AllocConsole opens 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 but println! / the logger wrote to dead pipes → blank window.

Fix: After AllocConsole, call SetStdHandle for stdout/stderr/stdin pointing at CONOUT$/CONIN$.

3. #[tokio::main] ran before AllocConsole

The 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 calls AllocConsole + logger, then builds the runtime manually with Runtime::new().block_on(startup(args)).

4. Args::parse() before logger init

Clap parse errors wrote to the unattached stderr pipe — invisible.

Fix: init_logger_named() before Args::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-cli crash → bootstrap logged "Result: Success". Made every other bug invisible in maxima_execution.log.

Fix: Check status.success() after wait() 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 without maxima-cli launch, so maxima.playing() is None when TF2's LSX module connects. The old code shut the socket immediately (LSXConnectionError::GameContext). handle_set_presence_request also called .unwrap() on playing() → 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. Safe let-else in profile.rs.

Files changed

File What changed
installer/maxima-setup.nsi SetRegView default before HKCR ops; upgrade guard in BackupProtocol
maxima-cli/src/main.rs Plain main, panic hook, stdio reattach, logger-before-args, manual tokio runtime
maxima-cli/Cargo.toml Added winapi features: processenv, fileapi, winbase, winnt
maxima-bootstrap/src/main.rs Log non-zero exit codes from maxima-cli
maxima-lib/src/lsx/connection.rs Accept external LSX connections (Northstar)
maxima-lib/src/lsx/request/profile.rs Safe let-else instead of unwrap on playing()
CLAUDE.md Document all findings with line references

Test plan

  • Install v0.2.1, then install this build over it (upgrade path) — verify link2ea:// still dispatched to maxima-bootstrap by checking maxima_execution.log grows on TF2 launch
  • Verify CLI console appears and shows log output when TF2 triggers link2ea://
  • Verify %LOCALAPPDATA%\Maxima\Logs\maxima-cli.log is populated after a launch attempt
  • Launch Northstar via Steam (steam.exe -applaunch 1237970 -northstar) and confirm online play works

🤖 Generated with Claude Code

…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>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +278 to +289
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()
);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Comment on lines +336 to +347
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()
);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Comment thread maxima-lib/src/lsx/connection.rs Outdated
Comment on lines 260 to 264
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!");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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!"),
_ => {}
}

AA-EION and others added 2 commits May 14, 2026 20:26
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>
@AA-EION AA-EION merged commit 4da1517 into master May 15, 2026
3 checks passed
@AA-EION AA-EION deleted the fix/v0.2.1-launch-invisible branch May 15, 2026 01:45
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