Skip to content

Fix output visibility for nix copy commands after subprocess update#626

Merged
NotAShelf merged 4 commits into
masterfrom
notashelf/push-zlyknnslyqoz
Apr 28, 2026
Merged

Fix output visibility for nix copy commands after subprocess update#626
NotAShelf merged 4 commits into
masterfrom
notashelf/push-zlyknnslyqoz

Conversation

@NotAShelf
Copy link
Copy Markdown
Member

@NotAShelf NotAShelf commented Apr 21, 2026

Add exec_with_streaming() helper to nh_core::command that uses .start() with piped stdout/stderr and spawns threads to stream output to the terminal in real-time. This captures output for error reporting on failure, and HOPEFULLY fixes the stdout/stderr bs that was happening after the Subprocess 1.0 upgrade.

Fixes: #624, #620

Summary by CodeRabbit

  • New Features

    • Real-time streaming of command output: stdout/stderr shown live during execution and still returned after completion.
  • Bug Fixes

    • Remote copy failures now report process exit status consistently when streaming output is enabled.
  • Chores

    • Workspace dependency for subprocess updated to v1.0.3 to align dependency resolution across the project.

@NotAShelf NotAShelf requested a review from faukah April 21, 2026 12:52
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Bumps workspace subprocess from 1.0.0 to 1.0.3, adds exec_with_streaming for live stdout/stderr streaming and capture, updates Command::run to use it when show_output is true, and switches several remote copy operations to use the new streaming execution path.

Changes

Cohort / File(s) Summary
Dependency Update
Cargo.toml
Bumped workspace subprocess dependency from 1.0.0 to 1.0.3.
Core command execution
crates/nh-core/src/command.rs
Added pub fn exec_with_streaming(cmd: Exec) -> Result<(subprocess::ExitStatus, String, String)> that spawns the child with piped stdout/stderr, launches reader threads that write chunks live to the terminal while accumulating output, waits/joins, and returns (exit_status, stdout, stderr). Modified Command::run to call this helper when self.show_output is true and to change non‑zero exit error formatting in that mode (report exit status rather than embedding captured stderr). When show_output is false, preserved existing capture() behavior and stderr-inclusive formatting on errors.
Remote copy operations
crates/nh-remote/src/remote.rs
Replaced cmd.capture() usages in copy_closure_to, copy_closure_from, copy_to_remote, and copy_closure_between_remotes with exec_with_streaming(cmd). Error handling now checks the returned exit_status and reports failures as ... failed (exit status: <status>). Spinner lifecycle preserved in copy_to_remote.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Parent as Parent Process
    participant ExecFn as exec_with_streaming
    participant Child as Child Process
    participant StdoutRd as Stdout Reader Thread
    participant StderrRd as Stderr Reader Thread
    participant Terminal

    User->>Parent: request operation that runs external command
    Parent->>ExecFn: build Exec and call exec_with_streaming
    ExecFn->>Child: spawn child with stdout/stderr pipes
    ExecFn->>StdoutRd: spawn stdout reader thread
    ExecFn->>StderrRd: spawn stderr reader thread

    par stream stdout
        StdoutRd->>Child: read stdout chunks
        StdoutRd->>Terminal: write chunks live
        StdoutRd->>ExecFn: accumulate stdout
    and stream stderr
        StderrRd->>Child: read stderr chunks
        StderrRd->>Terminal: write chunks live
        StderrRd->>ExecFn: accumulate stderr
    end

    Child->>ExecFn: exit with status
    ExecFn->>StdoutRd: join
    ExecFn->>StderrRd: join
    StdoutRd->>ExecFn: return accumulated stdout
    StderrRd->>ExecFn: return accumulated stderr
    ExecFn->>Parent: return (exit_status, stdout, stderr)
    Parent->>User: proceed based on exit_status and streamed output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing output visibility for nix copy commands following a subprocess library update.
Linked Issues check ✅ Passed The PR implements the recommended fix from issue #624 by using exec_with_streaming() to restore real-time output visibility when show_output is true, directly addressing the root cause of silently dropped stdout after the subprocess 1.0 upgrade.
Out of Scope Changes check ✅ Passed All changes are scope-aligned: subprocess version bump, the exec_with_streaming() helper to restore output visibility, Command::run() modifications for conditional streaming vs capturing, and remote.rs updates to use the new streaming function.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch notashelf/push-zlyknnslyqoz

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@NotAShelf
Copy link
Copy Markdown
Member Author

I don't like the helper approach btw, but it's the cleanest way we can do this in a futureproof manner imho. Unless we decide to pull nh-subprocess out of somewhere the sun don't shine.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/nh-remote/src/remote.rs (1)

1043-1064: ⚠️ Potential issue | 🟠 Major

Spinner and streamed child output will fight over the terminal.

ProgressBar::new_spinner() with enable_steady_tick(80ms) writes spinner frames to stderr on every tick, while exec_with_streaming(cmd) simultaneously streams the child's stdout and stderr directly to std::io::stdout() / std::io::stderr() in background threads. nix copy progress output goes to stderr, so you'll get interleaving: child stderr lines breaking up spinner redraws, and spinner tick sequences (\r, cursor moves) garbling child output.

Options to consider:

  • Drop the spinner when streaming is enabled — the child's own progress output already tells the user things are happening.
  • Or, suspend the spinner for the duration of the streamed exec (indicatif's MultiProgress/suspend pattern), and only start ticking again after the child exits.
  • Or, keep capture-only behavior for nix copy --to (where the spinner is justified) and only stream for the command sites that actually need live progress.

As-is, the behavior on a real nix copy will likely be visually worse than either the old capture+spinner path or a pure pass-through. Worth confirming interactively.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-remote/src/remote.rs` around lines 1043 - 1064, The spinner
(ProgressBar::new_spinner + enable_steady_tick) races with
exec_with_streaming(cmd) because both write to stderr; fix by suspending the
spinner while the child is streamed: wrap the exec_with_streaming(cmd) call
inside spinner.suspend(|| { ... }) (using ProgressBar::suspend) so the spinner's
ticks are paused during streaming, then finish_and_clear the spinner and handle
exit_status/stderr as before; alternatively, if you prefer no spinner during
streaming, skip creating/enabling the spinner when calling exec_with_streaming.
🧹 Nitpick comments (2)
crates/nh-remote/src/remote.rs (1)

829-834: Repeated exec_with_streaming + bail! block — consider extracting a helper.

The same pattern let (status, _out, stderr) = exec_with_streaming(cmd).wrap_err(ctx)?; if !status.success() { bail!("{} failed:\n{}", label, stderr); } appears four times in this file (plus the spinner variant). A small helper like run_streamed(cmd, context, label) or a fn check_status(status, label, stderr) -> Result<()> would de-duplicate and keep the error shape consistent.

Non-blocking.

Also applies to: 977-982, 1054-1063, 1096-1106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-remote/src/remote.rs` around lines 829 - 834, The code repeats the
pattern of calling exec_with_streaming(...).wrap_err(...)? followed by checking
exit_status.success() and bail!-ing with stderr (seen around exec_with_streaming
usages and the spinner variant); extract a small helper function (e.g.,
run_streamed(cmd: Command, ctx_msg: &str, label: &str) -> Result<()>) that wraps
exec_with_streaming, applies wrap_err(ctx_msg), checks the returned exit_status,
and returns an Err with the same bail!-style message including stderr when not
successful; replace each duplicated block (references: exec_with_streaming calls
around lines that build the nix-copy-closure call and the other three
occurrences) with calls to this helper so error shape and logging remain
consistent.
crates/nh-core/src/command.rs (1)

41-54: exec_with_streaming silently overrides any redirection already set on cmd.

Lines 42–43 unconditionally apply stdout(Pipe).stderr(Pipe), which will clobber any prior stderr(Redirection::Merge), stdout(Redirection::None), stderr(Redirection::File) etc. the caller configured on the passed-in Exec. Today's four call sites in remote.rs happen not to pre-set these, but the function is pub and the name doesn't hint at that contract. Please either:

  • Document the redirection override behavior on the docstring, or
  • Take only the pieces you need (e.g., a command + args builder) rather than an arbitrary Exec, to make the override intentional.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-core/src/command.rs` around lines 41 - 54, exec_with_streaming
currently unconditionally calls
cmd.stdout(Redirection::Pipe).stderr(Redirection::Pipe) which clobbers any
redirections the caller set on the passed-in Exec; change the API so this
override is intentional: replace the pub fn exec_with_streaming(cmd: Exec, ...)
signature with one that takes the command name and args (e.g., fn
exec_with_streaming<S: AsRef<str>>(program: S, args: &[S], ...) or a small
builder type) and construct the Exec inside the function so you control
redirections, then update all call sites in remote.rs to pass program+args;
alternatively (if you prefer minimal churn) detect whether stdout/stderr
redirections are already set on the supplied Exec and only apply
.stdout(Redirection::Pipe)/.stderr(Redirection::Pipe) when none exist, and
update the exec_with_streaming docstring to clearly document the redirection
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/nh-core/src/command.rs`:
- Around line 38-106: Command::run still drops stdout on success because it uses
cmd.capture() and never prints capture.stdout_str() in the success branch;
update Command::run (the method that checks self.show_output) to either call
exec_with_streaming when self.show_output is true or use Exec::join()/cmd.join()
instead of cmd.capture() so stdout is streamed/returned and the GC summary
(capture.stdout_str()) is preserved and printed on success; ensure the success
branch prints the captured stdout (capture.stdout_str()) or consumes the join
result so the "N store paths deleted, X MiB freed" summary is emitted.
- Around line 56-92: In both stdout_thread and stderr_thread replace the
per-chunk lossy decoding and the catch-all break on Err(_) by accumulating raw
bytes into a Vec<u8> (e.g., stdout_bytes / stderr_bytes) while still writing
each chunk to the terminal, and when the loop ends convert the entire Vec<u8>
once with String::from_utf8_lossy() into stdout_output / stderr_output; also
treat Ok(0) as EOF but on Err(e) call debug! (or processLogger equivalent) with
the error before breaking so real I/O errors aren’t silently swallowed
(referencing stdout_thread, stderr_thread, stdout_reader, stderr_reader,
stdout_output, stderr_output).

In `@crates/nh-remote/src/remote.rs`:
- Around line 829-834: The failure handlers that call exec_with_streaming
(capturing exit_status and stderr) should not re-print the child's stderr
because exec_with_streaming already streamed it; instead, update each bail! call
(the call sites that bind exit_status, _stdout, stderr from exec_with_streaming)
to omit stderr from the message and report the command exit status (e.g.,
include exit_status or exit_status.code()). Specifically, in the places using
exec_with_streaming and then bail!("… failed:\n{}", stderr) replace that message
with a concise message including the exit_status (or exit code) only so the
already-streamed stderr isn't duplicated.

---

Outside diff comments:
In `@crates/nh-remote/src/remote.rs`:
- Around line 1043-1064: The spinner (ProgressBar::new_spinner +
enable_steady_tick) races with exec_with_streaming(cmd) because both write to
stderr; fix by suspending the spinner while the child is streamed: wrap the
exec_with_streaming(cmd) call inside spinner.suspend(|| { ... }) (using
ProgressBar::suspend) so the spinner's ticks are paused during streaming, then
finish_and_clear the spinner and handle exit_status/stderr as before;
alternatively, if you prefer no spinner during streaming, skip creating/enabling
the spinner when calling exec_with_streaming.

---

Nitpick comments:
In `@crates/nh-core/src/command.rs`:
- Around line 41-54: exec_with_streaming currently unconditionally calls
cmd.stdout(Redirection::Pipe).stderr(Redirection::Pipe) which clobbers any
redirections the caller set on the passed-in Exec; change the API so this
override is intentional: replace the pub fn exec_with_streaming(cmd: Exec, ...)
signature with one that takes the command name and args (e.g., fn
exec_with_streaming<S: AsRef<str>>(program: S, args: &[S], ...) or a small
builder type) and construct the Exec inside the function so you control
redirections, then update all call sites in remote.rs to pass program+args;
alternatively (if you prefer minimal churn) detect whether stdout/stderr
redirections are already set on the supplied Exec and only apply
.stdout(Redirection::Pipe)/.stderr(Redirection::Pipe) when none exist, and
update the exec_with_streaming docstring to clearly document the redirection
behavior.

In `@crates/nh-remote/src/remote.rs`:
- Around line 829-834: The code repeats the pattern of calling
exec_with_streaming(...).wrap_err(...)? followed by checking
exit_status.success() and bail!-ing with stderr (seen around exec_with_streaming
usages and the spinner variant); extract a small helper function (e.g.,
run_streamed(cmd: Command, ctx_msg: &str, label: &str) -> Result<()>) that wraps
exec_with_streaming, applies wrap_err(ctx_msg), checks the returned exit_status,
and returns an Err with the same bail!-style message including stderr when not
successful; replace each duplicated block (references: exec_with_streaming calls
around lines that build the nix-copy-closure call and the other three
occurrences) with calls to this helper so error shape and logging remain
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7913b9e1-55e1-4513-a119-dcfea5dbca07

📥 Commits

Reviewing files that changed from the base of the PR and between bb23118 and 0b3c0bb.

📒 Files selected for processing (3)
  • Cargo.toml
  • crates/nh-core/src/command.rs
  • crates/nh-remote/src/remote.rs

Comment thread crates/nh-core/src/command.rs
Comment thread crates/nh-core/src/command.rs Outdated
Comment thread crates/nh-remote/src/remote.rs Outdated
@NotAShelf NotAShelf force-pushed the notashelf/push-zlyknnslyqoz branch from 44dcf35 to fdd4098 Compare April 28, 2026 05:49
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/nh-core/src/command.rs (1)

827-859: Please pin the show_output fix with a regression test.

This branch is the actual fix path for the visibility regression, but the test module still doesn't exercise Command::run().show_output(true) / exec_with_streaming() on either success or failure. A focused regression test here would make #624/#620 much harder to reintroduce.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-core/src/command.rs` around lines 827 - 859, Add a focused
regression test that exercises the exec_with_streaming code path by calling
Command::run().show_output(true); specifically create (1) a test case where the
command succeeds to ensure the streaming-success branch (`exec_with_streaming`)
is exercised and returns Ok, and (2) a test case where the command fails to
ensure the failure branch returns an Err containing the message, exit status and
streamed stderr; locate tests near the existing tests for
Command::run()/command.rs, invoke the same command execution helpers used in
production, and assert on the returned Result and any captured stderr to prevent
future regressions of the show_output fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/nh-core/src/command.rs`:
- Around line 41-44: The streaming helper exec_with_streaming() currently forces
both stdout and stderr to Redirection::Pipe, which overwrites caller-configured
stderr (e.g., Redirection::Merge) and breaks ordering; update
exec_with_streaming() so it only sets stdout to Pipe but preserves the caller's
stderr redirection: detect the command's existing stderr redirection (or accept
it as a parameter) and avoid calling .stderr(Redirection::Pipe) when the caller
already set Redirection::Merge (or any non-default), ensuring stderr is left as
configured and only stdout is piped for streaming.

---

Nitpick comments:
In `@crates/nh-core/src/command.rs`:
- Around line 827-859: Add a focused regression test that exercises the
exec_with_streaming code path by calling Command::run().show_output(true);
specifically create (1) a test case where the command succeeds to ensure the
streaming-success branch (`exec_with_streaming`) is exercised and returns Ok,
and (2) a test case where the command fails to ensure the failure branch returns
an Err containing the message, exit status and streamed stderr; locate tests
near the existing tests for Command::run()/command.rs, invoke the same command
execution helpers used in production, and assert on the returned Result and any
captured stderr to prevent future regressions of the show_output fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7405c7de-4cc8-4a89-8b1e-af43e9e23e95

📥 Commits

Reviewing files that changed from the base of the PR and between 44dcf35 and fdd4098.

📒 Files selected for processing (3)
  • Cargo.toml
  • crates/nh-core/src/command.rs
  • crates/nh-remote/src/remote.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/nh-remote/src/remote.rs

Comment thread crates/nh-core/src/command.rs
@NotAShelf NotAShelf force-pushed the notashelf/push-zlyknnslyqoz branch from fdd4098 to 9632e57 Compare April 28, 2026 06:09
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/nh-core/src/command.rs (1)

827-835: ⚠️ Potential issue | 🟠 Major

Use join() for Command::run(show_output = true) instead of exec_with_streaming().

The show_output branch pipes stdout through exec_with_streaming(), which forces stdout(Redirection::Pipe) and buffers the entire output in memory (lines 53, 62, 77, 86), even though the captured output is immediately discarded (_stdout and _stderr at line 828). This also changes TTY-sensitive behavior: the command is configured with inherited stdout (no explicit redirection at line 803-804), but piping it changes how the process sees its output stream.

Using cmd.join() instead—which is already available on Exec and used elsewhere in the codebase (Build::run_nix)—avoids both the unnecessary buffering and the TTY behavior change, while maintaining the same error handling pattern.

Suggested change
     if self.show_output {
-      let (exit_status, _stdout, _stderr) =
-        exec_with_streaming(cmd).wrap_err(msg.clone())?;
+      let exit_status = cmd.join().wrap_err(msg.clone())?;
       if !exit_status.success() {
         return Err(eyre::eyre!(format!(
           "{} (exit status {:?})",
           msg, exit_status
         )));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-core/src/command.rs` around lines 827 - 835, In the show_output
branch of Command::run replace the exec_with_streaming call with cmd.join() so
you don't force stdout/stderr to be piped and buffer output; specifically, where
exec_with_streaming(cmd).wrap_err(msg.clone())? is used, call
cmd.join().wrap_err(msg.clone())? (or otherwise await the Join result) and keep
the same exit_status check and eyre error construction; reference
exec_with_streaming, cmd.join, and the show_output conditional to locate the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/nh-core/src/command.rs`:
- Around line 827-835: In the show_output branch of Command::run replace the
exec_with_streaming call with cmd.join() so you don't force stdout/stderr to be
piped and buffer output; specifically, where
exec_with_streaming(cmd).wrap_err(msg.clone())? is used, call
cmd.join().wrap_err(msg.clone())? (or otherwise await the Join result) and keep
the same exit_status check and eyre error construction; reference
exec_with_streaming, cmd.join, and the show_output conditional to locate the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b373680-75d3-48ec-9c53-e35a1ab3d7e5

📥 Commits

Reviewing files that changed from the base of the PR and between fdd4098 and 9632e57.

📒 Files selected for processing (3)
  • Cargo.toml
  • crates/nh-core/src/command.rs
  • crates/nh-remote/src/remote.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/nh-remote/src/remote.rs

@NotAShelf NotAShelf force-pushed the notashelf/push-zlyknnslyqoz branch from 9632e57 to 74d53e3 Compare April 28, 2026 06:41
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/nh-core/src/command.rs (1)

827-857: Please add a regression test for the show_output fix.

Line 827 is the #624 fix path, but there is no coverage here proving that a successful show_output(true) command still emits stdout after the subprocess upgrade. A small integration-style test around echo/printf would keep this from silently regressing again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-core/src/command.rs` around lines 827 - 857, Add an
integration-style regression test that exercises the show_output(true) code path
(the branch using cmd.join() in the current diff) to ensure stdout is still
emitted after the subprocess upgrade: spawn a simple command like echo or printf
that writes to stdout, call the same API that sets show_output = true and runs
the command (triggering cmd.join()), and assert that the test process
receives/observes the emitted stdout (or that the command succeeds and its
stdout is not empty). Place the test alongside other command tests, run the
branch that uses cmd.capture() vs cmd.join() as appropriate, and ensure the test
fails if stdout is suppressed to prevent future regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/nh-core/src/command.rs`:
- Around line 38-113: The exec_with_streaming helper currently accumulates all
stdout/stderr into memory (stdout_bytes/stderr_bytes) even when callers ignore
the returned strings; change it to avoid full-buffer capture by either adding a
capture flag or a separate status-only variant. Update exec_with_streaming (or
add exec_with_streaming_status_only) so that when capture_output is false the
reader threads write to stdout/stderr but do not extend
stdout_bytes/stderr_bytes and instead return early (or join with empty strings),
and ensure job.wait() and thread joins still happen to avoid races; adjust
callers to use the new flag/variant where they only need ExitStatus. Ensure
references to stdout_thread, stderr_thread, stdout_bytes, stderr_bytes and the
function name exec_with_streaming (or new exec_with_streaming_status_only) are
updated accordingly.

In `@crates/nh-remote/src/remote.rs`:
- Around line 1062-1068: Move the spinner.finish_and_clear() call to execute
immediately before invoking exec_with_streaming(cmd) so the spinner is cleared
before any child process output is streamed; specifically, locate the
exec_with_streaming(...) invocation and the spinner.finish_and_clear() call in
remote.rs and swap their order (ensure spinner.finish_and_clear() runs on the
current thread right before calling exec_with_streaming(cmd) and remove the old
call after the exec_with_streaming result handling).

---

Nitpick comments:
In `@crates/nh-core/src/command.rs`:
- Around line 827-857: Add an integration-style regression test that exercises
the show_output(true) code path (the branch using cmd.join() in the current
diff) to ensure stdout is still emitted after the subprocess upgrade: spawn a
simple command like echo or printf that writes to stdout, call the same API that
sets show_output = true and runs the command (triggering cmd.join()), and assert
that the test process receives/observes the emitted stdout (or that the command
succeeds and its stdout is not empty). Place the test alongside other command
tests, run the branch that uses cmd.capture() vs cmd.join() as appropriate, and
ensure the test fails if stdout is suppressed to prevent future regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 767f2b69-9d22-4d1e-9134-7289c593b5ec

📥 Commits

Reviewing files that changed from the base of the PR and between 9632e57 and 74d53e3.

📒 Files selected for processing (3)
  • Cargo.toml
  • crates/nh-core/src/command.rs
  • crates/nh-remote/src/remote.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml

Comment thread crates/nh-core/src/command.rs
Comment thread crates/nh-remote/src/remote.rs Outdated
Comment on lines 1062 to 1068
let (exit_status, _stdout, _stderr) = exec_with_streaming(cmd)
.wrap_err("Failed to copy closure to remote host")?;

// We finish and *clear*, because the log line needs to come next. If we try
// to make the spinner change the text, we cannot reliably match the `info!`
// or `error!` style.
spinner.finish_and_clear();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and read the relevant lines
head -1070 crates/nh-remote/src/remote.rs | tail -20

Repository: nix-community/nh

Length of output: 876


🏁 Script executed:

# Find the exec_with_streaming function definition
rg -n "fn exec_with_streaming" crates/nh-remote/

Repository: nix-community/nh

Length of output: 42


🏁 Script executed:

# Look at the context around line 1062 for spinner usage
sed -n '1050,1080p' crates/nh-remote/src/remote.rs

Repository: nix-community/nh

Length of output: 1047


🏁 Script executed:

# Search for exec_with_streaming definition across the codebase
rg -n "exec_with_streaming" crates/nh-remote/src/

Repository: nix-community/nh

Length of output: 521


🏁 Script executed:

# Also check if it might be in another crate
rg -n "fn exec_with_streaming"

Repository: nix-community/nh

Length of output: 121


🏁 Script executed:

# Look for where it's defined, might be in a different module
rg -B3 -A10 "exec_with_streaming"

Repository: nix-community/nh

Length of output: 4980


🏁 Script executed:

# Read the exec_with_streaming function
sed -n '38,120p' crates/nh-core/src/command.rs

Repository: nix-community/nh

Length of output: 2355


Move spinner.finish_and_clear() before exec_with_streaming() call.

exec_with_streaming streams live output from the child process by writing to stdout/stderr in real-time via background threads. The spinner continues redrawing every 80ms while the command runs, causing interleaved terminal output that degrades readability on interactive terminals. Clearing the spinner before streaming starts ensures clean output from the command.

Suggested fix
-  let (exit_status, _stdout, _stderr) = exec_with_streaming(cmd)
-    .wrap_err("Failed to copy closure to remote host")?;
-
-  // We finish and *clear*, because the log line needs to come next. If we try
-  // to make the spinner change the text, we cannot reliably match the `info!`
-  // or `error!` style.
   spinner.finish_and_clear();
+  let (exit_status, _stdout, _stderr) = exec_with_streaming(cmd)
+    .wrap_err("Failed to copy closure to remote host")?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let (exit_status, _stdout, _stderr) = exec_with_streaming(cmd)
.wrap_err("Failed to copy closure to remote host")?;
// We finish and *clear*, because the log line needs to come next. If we try
// to make the spinner change the text, we cannot reliably match the `info!`
// or `error!` style.
spinner.finish_and_clear();
spinner.finish_and_clear();
let (exit_status, _stdout, _stderr) = exec_with_streaming(cmd)
.wrap_err("Failed to copy closure to remote host")?;
// We finish and *clear*, because the log line needs to come next. If we try
// to make the spinner change the text, we cannot reliably match the `info!`
// or `error!` style.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/nh-remote/src/remote.rs` around lines 1062 - 1068, Move the
spinner.finish_and_clear() call to execute immediately before invoking
exec_with_streaming(cmd) so the spinner is cleared before any child process
output is streamed; specifically, locate the exec_with_streaming(...) invocation
and the spinner.finish_and_clear() call in remote.rs and swap their order
(ensure spinner.finish_and_clear() runs on the current thread right before
calling exec_with_streaming(cmd) and remove the old call after the
exec_with_streaming result handling).

I had hoped that it would fix the capture behaviour...

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I221b472749139d4292617c453cf467626a6a6964
Subprocess 1.0.x changed `.capture()` to redirect stdout/stderr to pipes
by default, hiding progress output from `nix-copy-closure` and `nix copy`
commands. Users would see no output during remote copies unless the
command failed.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0e1fcc7e19b0fdba857e4f5822449b586a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-zlyknnslyqoz branch from 74d53e3 to 3ab3f02 Compare April 28, 2026 07:49
@NotAShelf NotAShelf enabled auto-merge April 28, 2026 18:44
@NotAShelf NotAShelf disabled auto-merge April 28, 2026 18:44
@NotAShelf NotAShelf merged commit 022883e into master Apr 28, 2026
7 of 9 checks passed
@NotAShelf NotAShelf deleted the notashelf/push-zlyknnslyqoz branch April 28, 2026 18:44
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.

nh clean regression in 4.3.1: paths deleted / freed summary no longer prints

2 participants