fix(tui): init runtime log before alt-screen, add Windows stderr redirect (#1909)#2259
Conversation
…tderr redirect Two root causes of verbose logging leaking into the TUI alt-screen: 1. EnterAlternateScreen was called BEFORE runtime_log::init(), so any logging between alt-screen entry and redirect init leaked raw bytes into the TUI buffer — causing the 'scroll demon' on all platforms. 2. redirect_stderr_to was #[cfg(unix)] only — Windows had no stderr redirect at all, so every eprintln!/tracing call during the TUI session wrote directly into the alt-screen buffer. Fixes: - Swap order: init runtime_log (with stderr redirect) BEFORE entering the alt-screen, so the redirect is active from the start. - Add #[cfg(windows)] redirect_stderr_to using SetStdHandle from the already-available windows crate, with corresponding Drop impl to restore the original handle. Fixes #1909
There was a problem hiding this comment.
Code Review
This pull request introduces Windows support for stderr redirection in the TUI runtime log and reorders the initialization of the log guard to occur before entering the alternate screen, preventing rendering issues. The reviewer suggests checking the return value of SetStdHandle on Windows and propagating any errors instead of ignoring them.
| unsafe { | ||
| let _ = SetStdHandle(STD_ERROR_HANDLE, windows::Win32::Foundation::HANDLE(target as isize)); | ||
| } |
There was a problem hiding this comment.
The return value of SetStdHandle is currently ignored. If SetStdHandle fails, the redirection will not take effect, but the function will still return Ok(saved). It is safer and more robust to check the return value of SetStdHandle and propagate any error so that the failure is properly handled and logged.
unsafe {
SetStdHandle(STD_ERROR_HANDLE, windows::Win32::Foundation::HANDLE(target as isize))
.ok()
.context("SetStdHandle(STD_ERROR_HANDLE) failed")?;
}| unsafe { | ||
| let _ = SetStdHandle(STD_ERROR_HANDLE, windows::Win32::Foundation::HANDLE(target as isize)); | ||
| } |
There was a problem hiding this comment.
🟡 Windows redirect_stderr_to silently swallows SetStdHandle failure, returning false success
The Windows redirect_stderr_to function discards the SetStdHandle result with let _ = ... and unconditionally returns Ok(saved). If SetStdHandle fails, the caller (init() at crates/tui/src/runtime_log.rs:167) receives Ok(handle) from .ok(), stores Some(handle) in the guard, and believes stderr was redirected when it was not. On drop, the guard performs a pointless SetStdHandle to "restore" an already-current handle.
The Unix counterpart at crates/tui/src/runtime_log.rs:260-264 correctly checks the dup2 return value and propagates the error (also cleaning up the saved fd). The Windows version should do the same so that failure is visible and the guard stores None (via the .ok() in init) rather than a misleading Some.
Prompt for agents
In redirect_stderr_to (Windows), the SetStdHandle result is discarded with `let _ = ...` and the function returns Ok(saved) unconditionally. If SetStdHandle fails, this reports false success. The Unix counterpart properly checks dup2's return and returns Err on failure (also closing the saved fd). The fix should propagate the SetStdHandle error, e.g. by using `?` or `.context(...)?.` on the Result, so that the caller's `.ok()` in init() correctly produces None when the redirect failed. See the Unix error-handling pattern at lines 260-264 for reference.
Was this helpful? React with 👍 or 👎 to provide feedback.
Use .map_err() with a descriptive message and cast target to isize for the HANDLE constructor, so the error is properly propagated if SetStdHandle fails. Co-Authored-By: bot_apk <apk@cognition.ai>
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Pushed Local verification:
|
|
Independent review: Approach is correct. Init-before- Windows handle ownership (already greptile-flagged): v0.8.48 / PR #2256: No conflict — #2256 touches Overlap with PR #2262: Same root issue (#1909), different mechanism — this PR redirects stderr fd/handle; #2262 suppresses the verbose CLI logger globally while alt-screen is active. They compose well (defense in depth: #2262 stops Codewhale's own verbose writes at the source, #2259 catches everything else — deps, panics, sub-agents) but produce a textual conflict in |
…aliasing The previous implementation used file.as_raw_handle() directly with SetStdHandle, causing both _file and the process stderr table entry to share the same HANDLE. If a third-party library called CloseHandle on the stderr handle during the TUI session, it would silently invalidate _file and cause a double-CloseHandle on guard drop. Now we call DuplicateHandle before SetStdHandle to produce a truly independent handle for the redirected stderr, mirroring the Unix path's use of libc::dup. The duplicated handle is tracked in the guard struct and properly closed on drop after the original stderr is restored. Co-Authored-By: bot_apk <apk@cognition.ai>
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Refreshed onto current main and preserved the runtime-log defense-in-depth path. Merge-refresh details:
Local verification:
A full local Windows target check was blocked by missing Windows C headers for |
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Thanks again for this one. I merged current Local re-checks from the isolated worktree:
CI should restart on the fresh head now; I’ll merge it as soon as the Windows lane comes back clean. |
Problem
Verbose logging leaks into the TUI alt-screen on all platforms, especially severe on Windows where there was no stderr redirect at all.
Two root causes:
EnterAlternateScreenwas called BEFOREruntime_log::init()— any logging between alt-screen entry and redirect init leaks raw bytes into the TUI bufferredirect_stderr_towas#[cfg(unix)]only — Windows had no stderr redirect, so everyeprintln!/tracingcall during the TUI session wrote directly into the alt-screenFix
#[cfg(windows)] redirect_stderr_tousingSetStdHandlefrom the already-availablewindowscrate, with correspondingDropimpl to restore the original handleVerification
cargo check --workspace✅cargo test -p codewhale-tui✅ — 3,449 passed, 0 failedwindowscrate (already inCargo.tomlas a Windows dep)Closes #1909
Greptile Summary
This PR fixes the TUI "scroll demon" on Windows and all platforms by (1) moving
runtime_log::init()to run beforeEnterAlternateScreenso the stderr redirect is always active before alt-screen bytes flow, and (2) adding a#[cfg(windows)]redirect_stderr_toimplementation usingGetStdHandle/SetStdHandleto cover the platform that previously had no redirect at all.crates/tui/src/tui/ui.rs: Swaps the order ofruntime_log::init()andEnterAlternateScreen— the redirect is now established before any alt-screen escape sequences are written.crates/tui/src/runtime_log.rs: Adds a Windowsredirect_stderr_tofunction that saves the original stderr handle withGetStdHandle, redirects it to the log file withSetStdHandle, and restores it in a new#[cfg(windows)] Dropimpl; a matching#[cfg(not(any(unix, windows)))]no-opDropimpl covers all remaining platforms.Confidence Score: 5/5
Safe to merge; both changes are narrowly scoped and correct in their normal lifecycle.
The order-swap in ui.rs is a mechanical two-line move with no logical risk. The Windows redirect is structurally correct: the guard's custom Drop restores stderr before _file closes the handle, so the dual-reference to the same HANDLE is resolved in the right order during teardown.
The Windows redirect_stderr_to implementation in crates/tui/src/runtime_log.rs is the only spot worth a second look, specifically whether DuplicateHandle should be used instead of the raw handle from as_raw_handle().
Important Files Changed
Sequence Diagram
sequenceDiagram participant UI as run_tui() participant RL as runtime_log::init() participant OS as OS stderr participant Term as Terminal (alt-screen) Note over UI: enable_raw_mode() UI->>RL: init() RL->>OS: GetStdHandle / dup(STDERR_FILENO) — save original RL->>OS: SetStdHandle / dup2 — redirect stderr → log file RL-->>UI: TuiLogGuard (redirect active) UI->>Term: EnterAlternateScreen ✅ redirect already live Note over Term: TUI session — all stderr writes go to log file UI->>Term: LeaveAlternateScreen Note over UI: TuiLogGuard drops RL->>OS: SetStdHandle / dup2 — restore original stderrComments Outside Diff (2)
crates/tui/src/runtime_log.rs, line 2-4 (link)crates/tui/src/runtime_log.rs, line 53-55 (link)stderrfd", but the struct now also carries a Windows saved handle field. Worth updating alongside the module-level doc.Reviews (3): Last reviewed commit: "docs(tui): update runtime log stderr red..." | Re-trigger Greptile