Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions src/remote/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
))
Expand Down Expand Up @@ -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()
));
Comment thread
greptile-apps[bot] marked this conversation as resolved.
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 {
Expand Down Expand Up @@ -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",
),
];

Expand Down
Loading