aboutsummaryrefslogtreecommitdiff
path: root/src/remote
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-11 20:12:49 +0300
committerGitHub <noreply@github.com>2026-09-11 21:12:49 +0400
commit1a7c691559bb6ea8ad366bce68f87f8c3f6db098 (patch)
treede5785ba8cb29394d921df8185ce4045bf790333 /src/remote
parenteba7758cedd1bd3a86c527b8a458075e8ef1efc2 (diff)
fix(remote): show SSH authentication output (#3609)
* fix(remote): show SSH authentication output refs #3606 * test(remote): stabilize SSH authentication output regression refs #3606 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/remote')
-rw-r--r--src/remote/attach.rs68
1 files changed, 60 insertions, 8 deletions
diff --git a/src/remote/attach.rs b/src/remote/attach.rs
index 042676d0..4f8d2b7c 100644
--- a/src/remote/attach.rs
+++ b/src/remote/attach.rs
@@ -4,9 +4,9 @@ use super::{args::*, process::wait_with_output_timeout, restart_policy::*, shell
use base64::Engine as _;
use std::collections::BTreeMap;
use std::fs::{self, File};
-use std::io::{self, IsTerminal, Write as _};
+use std::io::{self, IsTerminal, Read as _, Write as _};
use std::path::{Path, PathBuf};
-use std::process::{Command, Output, Stdio};
+use std::process::{Child, Command, Output, Stdio};
use interprocess::local_socket::traits::Listener as _;
#[cfg(all(test, unix))]
@@ -594,6 +594,13 @@ impl RemoteSsh {
.stderr(Stdio::piped())
.spawn()?;
+ if !self.noninteractive {
+ return normalize_remote_output(output_with_forwarded_stderr(
+ child,
+ Some(script.as_bytes()),
+ )?);
+ }
+
let write_result = if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(script.as_bytes())
} else {
@@ -602,11 +609,7 @@ impl RemoteSsh {
"ssh bootstrap stdin missing",
))
};
- let output = if self.noninteractive {
- wait_with_output_timeout(child, NONINTERACTIVE_SSH_COMMAND_TIMEOUT)?
- } else {
- child.wait_with_output()?
- };
+ let output = wait_with_output_timeout(child, NONINTERACTIVE_SSH_COMMAND_TIMEOUT)?;
write_result?;
normalize_remote_output(output)
}
@@ -623,7 +626,7 @@ impl RemoteSsh {
let output = if self.noninteractive {
wait_with_output_timeout(command.spawn()?, NONINTERACTIVE_SSH_COMMAND_TIMEOUT)
} else {
- command.output()
+ output_with_forwarded_stderr(command.spawn()?, None)
}?;
normalize_remote_output(output)
}
@@ -685,6 +688,55 @@ impl RemoteSsh {
}
}
+// Only interactive setup uses this relay. Background probes retain their
+// capture-only timeout path so SSH diagnostics cannot overwrite the active TUI.
+fn output_with_forwarded_stderr(mut child: Child, stdin: Option<&[u8]>) -> io::Result<Output> {
+ let mut child_stderr = child
+ .stderr
+ .take()
+ .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "ssh command stderr missing"))?;
+ let stderr_relay = thread::spawn(move || -> io::Result<Vec<u8>> {
+ let mut captured = Vec::new();
+ let mut buffer = [0_u8; 8 * 1024];
+ let mut destination = io::stderr();
+
+ loop {
+ let read = child_stderr.read(&mut buffer)?;
+ if read == 0 {
+ break;
+ }
+ captured.extend_from_slice(&buffer[..read]);
+ if destination.write_all(&buffer[..read]).is_ok() {
+ let _ = destination.flush();
+ }
+ }
+
+ Ok(captured)
+ });
+
+ let write_result = if let Some(bytes) = stdin {
+ if let Some(mut child_stdin) = child.stdin.take() {
+ child_stdin.write_all(bytes)
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::BrokenPipe,
+ "ssh bootstrap stdin missing",
+ ))
+ }
+ } else {
+ Ok(())
+ };
+ let output_result = child.wait_with_output();
+ let stderr_result = stderr_relay
+ .join()
+ .map_err(|_| io::Error::other("ssh stderr relay panicked"))?;
+
+ let mut output = output_result?;
+ write_result?;
+ output.stderr = stderr_result?;
+ Ok(output)
+}
+
fn normalize_remote_output(mut output: Output) -> io::Result<Output> {
normalize_remote_stdout(&mut output.stdout, output.status.success())?;
Ok(output)