use std::io::Write;
use std::time::{Duration, Instant};
use processkit::{Command, Outcome, ProcessGroup, ProcessGroupOptions};
use crate::common::completes_within;
const PS_HANDLER_TEMPLATE: &str = r#"Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class Ctrl {
public delegate bool Handler(uint sig);
[DllImport("kernel32.dll")] public static extern bool SetConsoleCtrlHandler(Handler handler, bool add);
static Handler _h;
public static void Arm() { _h = s => { HANDLER_BODY; return true; }; SetConsoleCtrlHandler(_h, true); }
}
'@
[Ctrl]::Arm()
Write-Output 'ready'
Start-Sleep -Seconds 30
"#;
fn ctrl_break_powershell(handler_body: &str) -> (Command, tempfile::TempPath) {
let script = PS_HANDLER_TEMPLATE.replace("HANDLER_BODY", handler_body);
let mut f = tempfile::Builder::new()
.suffix(".ps1")
.tempfile()
.expect("create temp script");
f.write_all(script.as_bytes()).expect("write temp script");
f.flush().expect("flush temp script");
let temp_path = f.into_temp_path();
let cmd = Command::new("powershell")
.args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
&temp_path.to_string_lossy(),
])
.windows_graceful_ctrl_break();
(cmd, temp_path)
}
fn ensure_console() {
use windows_sys::Win32::System::Console::AllocConsole;
unsafe {
let _ = AllocConsole();
}
}
#[tokio::test]
#[ignore = "spawns a real console subprocess; opt-in CTRL_BREAK graceful shutdown"]
async fn windows_ctrl_break_lets_a_handling_child_exit_within_the_grace() {
ensure_console();
let group = ProcessGroup::with_options(
ProcessGroupOptions::default().shutdown_timeout(Duration::from_secs(25)),
)
.expect("create group");
let (cmd, _script) = ctrl_break_powershell("Environment.Exit(42)");
let mut run = group.start(&cmd).await.expect("start");
run.wait_for_line(|l| l.contains("ready"), Duration::from_secs(30))
.await
.expect("handler armed");
let start = Instant::now();
completes_within(
Duration::from_secs(30),
"ctrl-break graceful shutdown",
group.shutdown(),
)
.await
.expect("shutdown ok");
assert!(
start.elapsed() < Duration::from_secs(12),
"a CTRL_BREAK-handling child must exit early, not ride the 25s grace (took {:?})",
start.elapsed()
);
let outcome = completes_within(Duration::from_secs(5), "reap", run.wait())
.await
.expect("wait");
assert_eq!(
outcome,
Outcome::Exited(42),
"the child exited via its CTRL_BREAK handler (code 42), not the hard-kill fallback"
);
}
#[tokio::test]
#[ignore = "spawns a CTRL_BREAK-ignoring subprocess; escalates to TerminateJobObject"]
async fn windows_ctrl_break_escalates_to_terminate_for_an_ignoring_child() {
ensure_console();
let group = ProcessGroup::with_options(
ProcessGroupOptions::default().shutdown_timeout(Duration::from_secs(1)),
)
.expect("create group");
let (cmd, _script) = ctrl_break_powershell("");
let mut run = group.start(&cmd).await.expect("start");
run.wait_for_line(|l| l.contains("ready"), Duration::from_secs(30))
.await
.expect("handler armed");
let start = Instant::now();
completes_within(
Duration::from_secs(15),
"ctrl-break escalation shutdown",
group.shutdown(),
)
.await
.expect("shutdown ok");
assert!(
start.elapsed() >= Duration::from_millis(800),
"an ignoring child must ride the ~1s grace before TerminateJobObject (took {:?})",
start.elapsed()
);
let outcome = completes_within(Duration::from_secs(5), "reap", run.wait())
.await
.expect("wait");
assert!(
!matches!(outcome, Outcome::Exited(42)),
"the child did NOT exit via its handler — it was hard-killed by the fallback (got {outcome:?})"
);
}
#[tokio::test]
#[ignore = "spawns a real off-console subprocess; CTRL_BREAK falls back to TerminateJobObject"]
async fn windows_ctrl_break_falls_back_to_terminate_for_an_off_console_child() {
let group = ProcessGroup::with_options(
ProcessGroupOptions::default().shutdown_timeout(Duration::from_millis(700)),
)
.expect("create group");
let run = group
.start(
&Command::new("ping")
.args(["-n", "30", "127.0.0.1"])
.windows_graceful_ctrl_break()
.create_no_window(),
)
.await
.expect("start");
tokio::time::sleep(Duration::from_millis(300)).await;
let start = Instant::now();
completes_within(
Duration::from_secs(10),
"ctrl-break fallback shutdown",
group.shutdown(),
)
.await
.expect("shutdown ok");
assert!(
start.elapsed() >= Duration::from_millis(500),
"an off-console child rides the grace before the TerminateJobObject fallback (took {:?})",
start.elapsed()
);
let outcome = completes_within(Duration::from_secs(5), "reap", run.wait())
.await
.expect("wait");
assert!(
!matches!(outcome, Outcome::Exited(0)),
"the off-console child was hard-killed by the fallback (got {outcome:?})"
);
}