fix: finish windows ssh setup after bridge exit (#3719)

refs #3651
This commit is contained in:
JJ Liebig
2026-09-12 19:24:34 +04:00
committed by GitHub
parent a5d5f6f654
commit 6cf4bbb031
+75 -5
View File
@@ -350,8 +350,10 @@ fn windows_powershell_streaming_application_command(path: &str, args: &[&str]) -
.map(|arg| crate::platform::quote_windows_command_line_arg(arg))
.collect::<Vec<_>>()
.join(" ");
// Start-Process -Wait waits for descendants, including a cold-started server.
// Retain the handle so Windows PowerShell 5.1 keeps the application's exit code.
windows_powershell_script_command(&format!(
"$process = Start-Process -FilePath {} -ArgumentList {} -NoNewWindow -Wait -PassThru -ErrorAction Stop; exit $process.ExitCode",
"$process = Start-Process -FilePath {} -ArgumentList {} -NoNewWindow -PassThru -ErrorAction Stop; $null = $process.Handle; $process.WaitForExit(); exit $process.ExitCode",
crate::platform::quote_powershell_arg(path),
crate::platform::quote_powershell_arg(&command_line),
))
@@ -3615,6 +3617,74 @@ mod tests {
}
}
#[cfg(windows)]
#[test]
fn windows_bridge_returns_application_exit_while_descendant_is_running() {
let pid_file = std::env::temp_dir().join(format!(
"herdr bridge descendant {}-{}.pid",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("current time")
.as_nanos()
));
let script = format!(
"$child = Start-Process powershell.exe -ArgumentList '-NoProfile -NonInteractive -Command Start-Sleep -Seconds 30' -NoNewWindow -PassThru; Set-Content -LiteralPath {} -Value $child.Id; exit 23",
crate::platform::quote_powershell_arg(&pid_file.to_string_lossy())
);
let encoded = base64::engine::general_purpose::STANDARD.encode(
script
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>(),
);
let command = windows_powershell_streaming_application_command(
"powershell.exe",
&["-NoProfile", "-NonInteractive", "-EncodedCommand", &encoded],
);
let mut launcher = Command::new("powershell.exe");
launcher
.args(["-NoProfile", "-NonInteractive", "-EncodedCommand"])
.arg(command.split_whitespace().last().unwrap())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
crate::platform::configure_background_command(&mut launcher);
let mut launcher = launcher.spawn().expect("launch Windows bridge command");
let deadline = Instant::now() + Duration::from_secs(10);
let status = loop {
let status = launcher.try_wait().expect("poll bridge launcher");
if status.is_some() || Instant::now() >= deadline {
break status;
}
thread::sleep(Duration::from_millis(20));
};
if status.is_none() {
let mut cleanup = Command::new("taskkill.exe");
cleanup.args(["/PID", &launcher.id().to_string(), "/T", "/F"]);
crate::platform::configure_background_command(&mut cleanup);
let _ = cleanup.output();
let _ = launcher.kill();
}
let _ = launcher.wait();
// Clean up the launcher before a missing PID can fail the test.
let descendant = fs::read_to_string(&pid_file);
let _ = fs::remove_file(pid_file);
let descendant = descendant.expect("descendant PID");
let descendant = descendant.trim().parse::<u32>().expect("numeric PID");
let mut cleanup = Command::new("powershell.exe");
cleanup
.args(["-NoProfile", "-NonInteractive", "-Command"])
.arg(format!("Stop-Process -Id {descendant} -ErrorAction Stop"));
crate::platform::configure_background_command(&mut cleanup);
let descendant_was_running = cleanup.status().expect("stop test descendant").success();
assert!(
descendant_was_running,
"descendant must outlive the application"
);
assert_eq!(status.and_then(|status| status.code()), Some(23));
}
#[test]
fn windows_remote_commands_use_one_encoded_powershell_grammar() {
fn decode(command: &str) -> String {
@@ -3661,22 +3731,22 @@ mod tests {
(
"direct bridge",
executable.bridge_command("agents"),
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session agents remote-client-bridge' -NoNewWindow -Wait -PassThru -ErrorAction Stop; exit $process.ExitCode",
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session agents remote-client-bridge' -NoNewWindow -PassThru -ErrorAction Stop; $null = $process.Handle; $process.WaitForExit(); exit $process.ExitCode",
),
(
"API bridge with explicit default session",
remote_api_bridge_command(&RemoteHerdr::for_platform(RemotePlatform { os: "windows", arch: "x86_64" }), "default", false),
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session default remote-api-bridge' -NoNewWindow -Wait -PassThru -ErrorAction Stop; exit $process.ExitCode",
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session default remote-api-bridge' -NoNewWindow -PassThru -ErrorAction Stop; $null = $process.Handle; $process.WaitForExit(); exit $process.ExitCode",
),
(
"API bridge capability probe",
remote_api_bridge_command(&RemoteHerdr::for_platform(RemotePlatform { os: "windows", arch: "x86_64" }), "agents", true),
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session agents remote-api-bridge --check' -NoNewWindow -Wait -PassThru -ErrorAction Stop; exit $process.ExitCode",
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session agents remote-api-bridge --check' -NoNewWindow -PassThru -ErrorAction Stop; $null = $process.Handle; $process.WaitForExit(); exit $process.ExitCode",
),
(
"saved bridge with closed stdin",
executable.saved_bridge_command("agents"),
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session agents remote-client-bridge' -NoNewWindow -Wait -PassThru -ErrorAction Stop; exit $process.ExitCode",
"$process = Start-Process -FilePath herdr.exe -ArgumentList '--session agents remote-client-bridge' -NoNewWindow -PassThru -ErrorAction Stop; $null = $process.Handle; $process.WaitForExit(); exit $process.ExitCode",
),
];