Skip to content

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

Closed

Description

@mstomar125

Context

Follow-up to #2129 ("eprintln! diagnostics are silently discarded on Windows"). PR #2136 fixed the daemon-child-process half of that issue (routed nodespaced's stdout/stderr to ~/.nodespace/logs/nodespaced.log/nodespaced-error.log instead of Stdio::null()), but was written and reviewed entirely on macOS with no Windows machine, VM, or cross-compilation toolchain available — see the PR description and the comment on #2129 for the full verification-gap writeup. This issue tracks the two things that genuinely need a real Windows environment.

Also folds in Windows validation for #2138's fix (kill_running_daemon's taskkill image name is now edition-aware — see below) since it's the same file's Windows code path.

1. Live-validate PR #2136 (and #2140) on a real Windows box

What was verified (macOS only): native cargo check/test/clippy --all-targets/fmt on the whole nodespace-app crate, plus an isolated scratch-crate cargo check/clippy --target x86_64-pc-windows-msvc of just the new #[cfg(windows)] functions (compiles + clippy-clean against the real target, but that's a partial crate with no tauri/ring in its dependency tree — it proves the Windows API usage type-checks, nothing about runtime behavior).

What still needs a real Windows machine:

  • Full crate builds for x86_64-pc-windows-msvc (a full cargo check --target x86_64-pc-windows-msvc on this macOS machine fails on ring's C build needing a Windows SDK/MSVC toolchain — unrelated to the PR's change, but it means the whole crate has never actually been type-checked here, only the isolated new functions).
  • spawn_daemon_windows actually creates and appends to nodespaced.log/nodespaced-error.log under ~/.nodespace/logs/ when the daemon starts.
  • The file handles opened via OpenOptions::new().create(true).append(true) correctly inherit into the DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP-flagged child — confirm no handle-inheritance gotcha (Windows handles need bInheritHandle = TRUE at creation; verify Rust's std Command/Stdio::from(File) sets this up correctly for this specific flag combination).
  • Restarting the daemon (binary update / crash recovery, via kill_running_daemon's taskkill /F /IM <edition-correct-name>.exe, fixed in desktop-app: Windows kill_running_daemon targets edition-correct binary name #2140 to use daemon_binary_name() instead of a hardcoded nodespaced.exe literal) reopens the log files without a file-locking conflict, and appends rather than truncates across the restart. Also confirm the taskkill actually matches and kills the process for both editions (community nodespaced.exe and Pro nodespaced-pro.exe) — desktop-app: Windows kill_running_daemon targets edition-correct binary name #2140 fixed the edition-selection logic but the .exe-suffixed image name itself has never been confirmed against a real running process.
  • End-to-end: trigger a malformed-schema fields-parse failure (the original nodespace-types::SchemaNode::from_node silently swallows a schema field parse failure to an empty Vec #2107/nodespace-types: surface SchemaNode field-parse failures via eprintln (closes #2107) #2128 diagnostic) and confirm the message actually lands in nodespaced-error.log.
  • New (surfaced during desktop-app: Windows kill_running_daemon targets edition-correct binary name #2140's adversarial review): confirm the installed Windows daemon binary (~/.nodespace/bin/<daemon_binary_name()>, extracted by extract_sidecar_if_changed) actually carries a .exe extension on disk — extract_sidecar_if_changed/ensure_daemon_running construct the install path as bin_dir.join(daemon_binary_name()) with no cfg(windows) extension handling, so if the extracted file is extension-less, neither spawn_daemon_windows's Command::new(daemon_bin) nor kill_running_daemon's taskkill /IM <name>.exe would work as intended. Also check for a possible mismatch between resolve_sidecar_path's Windows resource lookup (binaries/{name}-{triple}, no .exe) and what CI actually names the built Windows sidecar artifact (.github/workflows/release.yml produces binaries/nodespaced-{triple}.exe, with .exe) — if the lookup doesn't find the resource, extract_sidecar_if_changed would fail before the daemon is ever installed at all, which would be a more severe, earlier-stage bug than anything tracked above.

2. Fix the desktop-app GUI process's own console-less stderr

Separate, unaddressed mechanism: packages/desktop-app/src-tauri/src/commands/schemas.rs's two direct SchemaNode::from_node calls (get_all_schemas/get_schema_definition) run inside the Tauri GUI process itself, which builds with #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] in main.rs — a release build has no console at all, so any eprintln! there is discarded regardless of PR #2136's daemon-side fix. This isn't a child-process stdio problem (nothing to redirect via Stdio); it's the GUI process's own stderr handle.

Candidate approaches (need Windows-side design + validation, not just code):

  • OS-level SetStdHandle(STD_ERROR_HANDLE, ...) redirect to a log file at GUI process startup (Windows FFI in main.rs) — meaningfully higher blast radius than the daemon fix since it touches app startup for the whole GUI process; needs runtime verification that it doesn't interfere with WebView2/Tauri's own expectations.
  • Restructure nodespace_types::SchemaNode::from_node to return the fields-parse diagnostic in its Result/output instead of printing internally, so callers can route it through whatever logging mechanism is appropriate for their process — rejected by PR nodespace-types: surface SchemaNode field-parse failures via eprintln (closes #2107) #2128's own rationale (chosen specifically to avoid touching call-site signatures across three callers uniformly), so this would need to revisit that decision explicitly if pursued.

Why Blocked

Both halves require either a real Windows machine/VM or at minimum cross/cargo-xwin + a full Windows SDK to get real compile+run verification — neither is available in this sandboxed macOS environment. Tracking as Blocked on the project board until Windows access is available (see MAC HANDOVER QUEUE precedent for the equivalent macOS-GUI-blocked pattern).

Activity

  1. self-assigned this
    on Sep 14, 2026
  2. mstomar125 commented on Sep 14, 2026

    @mstomar125
    ContributorAuthor

    Starting work on this issue. Baseline: worktree created from main @ 6fdd9f7, bun install clean, full test suite green (241 files / 4955 tests passed). Confirmed already assigned to mstomar125 and board status already 'In Progress'. Plan: (1) investigate the sidecar .exe-extension question first per issue priority, (2) real Windows validation of PR #2136/#2140's daemon stderr fix using the standalone-harness technique, (3) design + implement + Windows-validate a fix for the GUI process's own console-less stderr in schemas.rs.

  3. mstomar125 commented on Sep 14, 2026

    @mstomar125
    ContributorAuthor

    Resolved in #2614 — full real-Windows verification for both parts, using SSH access to a real Windows box (no libclang/LLVM there, so the full nodespace-app/nodespaced binaries still can't build; verified via the same standalone-harness technique used for #1452/#1498/#1519, extending it here to vendor the real nodespace-types crate and faithfully re-implement the specific daemon_setup.rs/main.rs functions under test).

    Sidecar .exe-extension question (checked first, per the issue) — real bug found and fixed. extract_sidecar_if_changed's install destination was bin_dir.join(name) with the bare daemon_binary_name(), never routed through bundled_sidecar_name (which correctly appends .exe on Windows for the source-lookup half of the same function) — so the daemon binary extracted to ~/.nodespace/bin/ on Windows carried no .exe extension. Confirmed live: dir shows a file literally named nodespaced. This is not the CI/release-pipeline mismatch the issue speculated about (release.yml and Tauri's bundler are both correct); it's local to this repo's own extraction code, so it's fixed directly rather than filed separately.

    Real-Windows proof of the practical impact — the daemon still starts (Command::new resolves an extensionless literal path fine), but kill_running_daemon's restart-time taskkill /F /IM nodespaced.exe silently never matches it:

    tasklist | findstr nodespaced  ->  nodespaced   3732 Services   0   4,516 K   (no .exe)
    taskkill /F /IM nodespaced.exe
    stderr: ERROR: The process "nodespaced.exe" not found.
    tasklist | findstr nodespaced  ->  nodespaced   3732 Services   0   4,516 K   (STILL ALIVE)
    

    A control run with the extension present isolates the root cause — same taskkill command matches correctly once the destination naming is fixed. Same result confirmed for the Pro edition name. Fixed via a new sidecar_install_path() helper used as the single source of truth by both the install destination and the daemon-bin/taskkill path, with two new regression tests.

    Rest of the Part-1 checklist — confirmed working as-is on real hardware: log file creation/append under ~/.nodespace/logs/, file-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 correctly in nodespaced-error.log end-to-end.

    Part 2 (GUI process's own console-less stderr): implemented SetStdHandle(STD_ERROR_HANDLE/STD_OUTPUT_HANDLE, <log file>) at GUI startup in main.rs, gated to release-on-Windows. Reconsidered the SchemaNode::from_node Result-restructuring alternative and kept #2128's original rejection — it has a third call site reachable from every non-GUI entry point, none of which have this bug, so threading a diagnostic through that shared API is a much bigger blast radius than a self-contained main.rs/daemon_setup.rs fix.

    The genuinely uncertain part — whether SetStdHandle actually affects Rust's own eprintln!/io::stderr() machinery, or whether Rust caches a stale "no console" state from before the call — was verified for real with a standalone windows_subsystem = "windows" harness launched via cmd /c start /WAIT (genuinely no inherited stdio, unlike a direct SSH invocation which inherits sshd's own pipes and doesn't reproduce the bug): output before the redirect is confirmed lost, output after it — including the real SchemaNode::from_node diagnostic on a genuinely malformed schema — lands correctly in the log file.

    Full evidence, code, and the honest statement of the remaining verification gap (the real nodespace-app/nodespaced binaries themselves still can't build on this Windows box) are in the PR description.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions