Skip to content

Fix Windows sidecar install extension mismatch, redirect GUI stdio to log files - #2614

Merged
mstomar125 merged 1 commit into
mainfrom
issue-2137-windows-stderr-validation
Sep 14, 2026
Merged

mstomar125 merged 1 commit into
mainfrom
issue-2137-windows-stderr-validation

Conversation

@mstomar125

Copy link
Copy Markdown
Contributor

Closes #2137

Summary

Live-validated PR #2136/#2140's daemon-side stderr-to-file fix on a real Windows box (SSH access to a Windows 11 machine, real MSVC toolchain, no libclang/LLVM so no full nodespaced/nodespace-app binary build — verification below uses the same standalone-harness technique used for #1452/#1498/#1519). Investigation surfaced a real, previously-unconfirmed bug in the sidecar install path, fixed here. Also implements the separate, previously-unaddressed fix for the GUI process's own console-less stderr.

Part 1: real Windows validation of PR #2136/#2140, and a real bug found + fixed

The issue's "check this first" item was real. extract_sidecar_if_changed computed its install destination as bin_dir.join(name), where name is the bare, cross-platform daemon_binary_name()/CLI_BINARY_NAME constant ("nodespaced", never routed through bundled_sidecar_name, which is what correctly appends .exe on Windows for the source-lookup half of the same function). So the daemon binary actually extracted to ~/.nodespace/bin/ on Windows carried no .exe extension — confirmed on a real Windows box, dir shows a file literally named nodespaced, not nodespaced.exe.

This is a different, more subtle bug than what the issue speculated (a CI/release-pipeline naming mismatch preventing install entirely — that part is fine: release.yml correctly produces binaries/nodespaced-{triple}.exe, and Tauri's bundler correctly stages it as nodespaced.exe beside the app's own .exe, confirmed by the pre-existing sidecar_sits_beside_the_windows_exe test). The bug is entirely in this repo's own extract_sidecar_if_changed, so it's fixed directly here rather than filed as a separate CI-pipeline issue.

Real-Windows evidence (standalone harness: vendored nodespace-types — confirmed dependency-light, no DB/HTTP/NLP deps, builds natively on the Windows box — plus faithful re-implementations of daemon_setup.rs's Windows functions, [[bin]] targets extractor/spawner/killer/fakedaemon):

[extractor] copied 1989811 bytes ... to C:\...\bin_bug\nodespaced (bare `name` ... matching daemon_setup.rs's `bin_dir.join(name)` dest computation)
[extractor] does the extracted destination filename carry a .exe extension? false

[spawner] SUCCESS: spawned 'C:\...\bin_bug\nodespaced' as detached child PID 3732
tasklist | findstr nodespaced  ->  nodespaced   3732 Services   0   4,516 K   (image name has NO .exe)

[killer] taskkill /F /IM nodespaced.exe     <- the exact command kill_running_daemon runs
stderr: ERROR: The process "nodespaced.exe" not found.
tasklist | findstr nodespaced  ->  nodespaced   3732 Services   0   4,516 K   (STILL ALIVE)

taskkill /F /IM nodespaced   (the real, bare image name)
SUCCESS: The process "nodespaced" with PID 3732 has been terminated.

Control (fixed extension) isolates the root cause — same taskkill command now matches correctly:

[spawner] SUCCESS: spawned 'C:\...\bin_fixed\nodespaced.exe' as detached child PID 8472
tasklist | findstr nodespaced  ->  nodespaced.exe   8472 Services   0   5,000 K
[killer] taskkill /F /IM nodespaced.exe
stdout: SUCCESS: The process "nodespaced.exe" with PID 8472 has been terminated.

Same result for the Pro edition name (nodespaced-pro vs nodespaced-pro.exe).

Practical impact of the bug (unfixed): the daemon still starts fine on first install (Command::new resolves an extensionless full path that literally exists), but a binary-update restart's kill_running_daemon silently fails to kill the previous instance (the taskkill call's result is ignored — let _ = ...output()), so the old daemon keeps holding the named pipe while a new one is spawned alongside it.

Fix: new sidecar_install_path(bin_dir, name) helper (routes through the existing bundled_sidecar_name), used as the single source of truth by both extract_sidecar_if_changed's install destination and ensure_daemon_running's daemon_bin — the exact two places that had silently drifted apart. Two new unit tests pin this (sidecar_install_path_carries_the_native_extension, sidecar_install_path_covers_pro_daemon_and_cli_names).

Rest of the Part-1 checklist — verified working as-is, no code changes needed:

  • spawn_daemon_windows creates/appends nodespaced.log/nodespaced-error.log under ~/.nodespace/logs/ — confirmed.
  • OpenOptions::create(true).append(true)-opened file handles correctly inherit into a DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP child spawned via Stdio::from(File) — confirmed real: the child's stdout/stderr both land in the log files.
  • Restarting the daemon reopens logs by appending, not truncating — confirmed by spawning twice into the same log dir and seeing both runs' full output preserved.
  • End-to-end: a real malformed fields value (a string, not an array) fed through the real nodespace_types::SchemaNode::from_node/parse_fields produces the exact nodespace-types: SchemaNode::from_node: failed to parse fields for schema node ... diagnostic, and it lands correctly in nodespaced-error.log.

Part 2: fix the GUI process's own console-less stderr

commands/schemas.rs's get_all_schemas/get_schema_definition call SchemaNode::from_node directly inside the Tauri GUI process, which builds with windows_subsystem = "windows" in release (main.rs) — no console at all, so PR #2136's daemon-side fix (child-process Stdio redirection) doesn't touch this at all.

Approach chosen: SetStdHandle(STD_ERROR_HANDLE/STD_OUTPUT_HANDLE, <log file handle>) at GUI process startup, in main.rs, gated to release-on-Windows (debug builds keep their attached console). Reconsidered the other candidate (restructuring SchemaNode::from_node to return the diagnostic via Result) and kept the original #2128 rationale for rejecting it: it has three production call sites, two of them here and one (node_to_typed_value, via nodes_to_typed_values's batch Result collection) reachable from every non-GUI entry point (CLI, MCP, HTTP) — none of which have this bug. Threading the diagnostic through the shared nodespace-types API to fix a two-call-site, GUI-process-local problem would be a much larger blast radius than a self-contained fix in main.rs/daemon_setup.rs. Nothing about having real Windows access now changes that trade-off.

Real-Windows proof it actually works (this was the genuinely uncertain part: does SetStdHandle actually affect Rust's own io::stdout()/io::stderr()/eprintln! machinery, or does Rust cache a "no console" state from before the call?). Standalone #![windows_subsystem = "windows"] harness with the fix's exact function bodies, launched via cmd /c start /WAIT (genuinely no inherited stdio — a direct SSH invocation instead inherits sshd's own piped stdio and doesn't reproduce the bug):

(no output captured by the launching shell at all -- confirms no console/no inherited stdio)

marker.txt: "gui_stderr_harness ran"   (proves the process ran, via plain std::fs, independent of stdio)

gui-stdout.log:
AFTER-REDIRECT stdout line -- should land in the log file
SchemaNode::from_node -> Ok with 0 fields ...

gui-stderr.log:
AFTER-REDIRECT stderr line -- should land in the log file
nodespace-types: SchemaNode::from_node: failed to parse `fields` for schema node `...`: invalid type: string "not-an-array-will-fail-to-parse", expected a sequence — reading back as an empty field list. Likely a stale/corrupted storage format.

The harness's BEFORE-REDIRECT lines never appear anywhere (confirmed lost, matching the real bug); the AFTER-REDIRECT lines and the real SchemaNode::from_node diagnostic both land correctly in the log files — confirming Rust's stdio machinery re-resolves the OS standard handle at write time rather than caching a stale state, on this Rust/Windows version.

Verification gap, stated honestly

The full nodespace-app crate (and nodespaced) still cannot be built or run on this Windows box — it depends transitively on nodespace-nlp-engine/llama-cpp-sys-2, which needs libclang/LLVM (installing it was explicitly blocked by this session's safety classifier; no other software install was attempted). Every real-Windows claim above is therefore from a standalone harness that either vendors the actual lightweight crate (nodespace-types) or faithfully re-implements the specific daemon_setup.rs/main.rs functions being tested — not a build of the real nodespace-app/nodespaced binaries themselves. cargo check -p nodespace-app --bins --lib and the full bun run test:all (unit + browser + Rust, including the Tauri-seam ADR-048 integration tests) pass on macOS.

Testing

  • bun run test:all — clean (4955+ frontend tests, 167 desktop-app Rust unit tests including 2 new regression tests, full Tauri-seam integration suite).
  • bun run quality:fix — clean, no additional changes beyond the two files above.
  • cargo check -p nodespace-app --bins --lib — clean on macOS.
  • Real Windows execution evidence for both parts, detailed above.

🤖 Generated with Claude Code

… log files (closes #2137)

Real-Windows validation of PR #2136/#2140's daemon stderr-to-file fix
surfaced a genuine bug: extract_sidecar_if_changed installed the daemon
binary under ~/.nodespace/bin/ using the bare daemon_binary_name()
("nodespaced"), never routed through bundled_sidecar_name(), so the
extracted file carried no .exe extension. The daemon still spawned (a
literal existing path resolves via Command::new even without an
extension), but registered in the Windows process table under the bare
image name, so kill_running_daemon's taskkill /IM nodespaced.exe never
matched it -- confirmed live on a Windows box: taskkill reported
"process not found" while the real process kept running, for both
community and Pro edition names. A restart could therefore never kill
the previous daemon before spawning a new one.

Fix: route both the install destination (extract_sidecar_if_changed)
and the launch/kill path (ensure_daemon_running's daemon_bin) through a
single new sidecar_install_path() helper, so they can't drift apart
again. Everything else in the daemon stderr-to-file path -- log file
creation/append, handle inheritance into a DETACHED_PROCESS |
CREATE_NEW_PROCESS_GROUP child, append-not-truncate across a simulated
restart, and the real SchemaNode::from_node fields-parse diagnostic
landing in nodespaced-error.log -- verified working as-is on the same
Windows box.

Separately, add a fix for the GUI process's own console-less stderr
(schemas.rs's two direct SchemaNode::from_node calls, discarded by
windows_subsystem = "windows" in release builds): redirect this
process's own STD_OUTPUT_HANDLE/STD_ERROR_HANDLE to log files via
SetStdHandle at startup, gated to release-on-Windows so debug builds
keep their attached console. Verified on a real Windows box with a
standalone windows_subsystem = "windows" harness launched with no
inherited stdio: eprintln!/println! output is lost before the
redirect and lands correctly in the log file after it, including the
real SchemaNode::from_node diagnostic on a genuinely malformed schema.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mstomar125
mstomar125 merged commit 325c526 into main Sep 14, 2026
@mstomar125
mstomar125 deleted the issue-2137-windows-stderr-validation branch September 14, 2026 12:25
malibio added a commit that referenced this pull request Sep 27, 2026
The doc comment claimed the eprintln! diagnostic is invisible on
Windows/release builds since those have no console, but PR #2614
already redirects the Windows daemon and release-GUI stdio to
persistent log files, closing that gap. Update the comment to reflect
that the diagnostic is universally log-file-only rather than
two-platform-specific.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Windows: live-validate daemon stderr-to-file fix (PR #2136) + fix desktop-app GUI console-less stderr

1 participant