Skip to content

Commit 25cdf07

Browse files
authored
feat(windows): implement multi platform process kill (#1875)
1 parent 79059f4 commit 25cdf07

File tree

12 files changed

+445
-131
lines changed

12 files changed

+445
-131
lines changed

.cargo/config.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[target.x86_64-pc-windows-msvc]
2+
rustflags = [
3+
"-C", "link-arg=/STACK:8000000"
4+
]
5+
6+
# 64 bit Mingw
7+
[target.x86_64-pc-windows-gnu]
8+
rustflags = [
9+
"-C", "link-arg=-Wl,--stack,8000000"
10+
]

Cargo.lock

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/chat-cli/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ security-framework = "3.2.0"
178178
[target.'cfg(windows)'.dependencies]
179179
windows = { version = "0.61.1", features = [
180180
"Foundation",
181+
"Win32_System_ProcessStatus",
182+
"Win32_System_Kernel",
181183
"Win32_System_Threading",
184+
"Wdk_System_Threading",
182185
] }
183186
winreg = "0.55.0"
184187

crates/chat-cli/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
//! This lib.rs is only here for testing purposes.
33
//! `test_mcp_server/test_server.rs` is declared as a separate binary and would need a way to
44
//! reference types defined inside of this crate, hence the export.
5+
pub mod api_client;
6+
pub mod auth;
7+
pub mod aws_common;
8+
pub mod cli;
9+
pub mod database;
10+
pub mod logging;
511
pub mod mcp_client;
12+
pub mod platform;
13+
pub mod request;
14+
pub mod telemetry;
15+
pub mod util;
616

717
pub use mcp_client::*;

crates/chat-cli/src/mcp_client/client.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ use std::sync::{
1111
};
1212
use std::time::Duration;
1313

14-
use nix::sys::signal::Signal;
15-
use nix::unistd::Pid;
1614
use serde::{
1715
Deserialize,
1816
Serialize,
@@ -46,6 +44,10 @@ use super::{
4644
ServerCapabilities,
4745
ToolsListResult,
4846
};
47+
use crate::util::process::{
48+
Pid,
49+
terminate_process,
50+
};
4951

5052
pub type ClientInfo = serde_json::Value;
5153
pub type StdioTransport = JsonRpcStdioTransport;
@@ -165,23 +167,22 @@ impl Client<StdioTransport> {
165167
.stdin(Stdio::piped())
166168
.stdout(Stdio::piped())
167169
.stderr(Stdio::piped())
168-
.process_group(0)
169170
.envs(std::env::vars());
171+
172+
#[cfg(not(windows))]
173+
command.process_group(0);
174+
170175
if let Some(env) = env {
171176
for (env_name, env_value) in env {
172177
command.env(env_name, env_value);
173178
}
174179
}
175180
command.args(args).spawn()?
176181
};
182+
177183
let server_process_id = child.id().ok_or(ClientError::MissingProcessId)?;
178-
#[allow(clippy::map_err_ignore)]
179-
let server_process_id = Pid::from_raw(
180-
server_process_id
181-
.try_into()
182-
.map_err(|_| ClientError::MissingProcessId)?,
183-
);
184-
let server_process_id = Some(server_process_id);
184+
let server_process_id = Some(Pid::from_u32(server_process_id));
185+
185186
let transport = Arc::new(transport::stdio::JsonRpcStdioTransport::client(child)?);
186187
Ok(Self {
187188
server_name,
@@ -205,7 +206,7 @@ where
205206
// This drop trait is here as a fail safe to ensure we don't leave behind any orphans.
206207
fn drop(&mut self) {
207208
if let Some(process_id) = self.server_process_id {
208-
let _ = nix::sys::signal::kill(process_id, Signal::SIGTERM);
209+
let _ = terminate_process(process_id);
209210
}
210211
}
211212
}

crates/chat-cli/src/util/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
mod cli_context;
2+
pub mod consts;
23
pub mod directories;
34
pub mod open;
5+
pub mod process;
46
pub mod spinner;
57
pub mod system_info;
68

7-
pub mod consts;
8-
99
use std::fmt::Display;
1010
use std::io::{
1111
ErrorKind,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
pub use sysinfo::Pid;
2+
3+
#[cfg(target_os = "windows")]
4+
mod windows;
5+
#[cfg(target_os = "windows")]
6+
pub use windows::*;
7+
8+
#[cfg(not(windows))]
9+
mod unix;
10+
#[cfg(not(windows))]
11+
pub use unix::*;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use nix::sys::signal::Signal;
2+
use sysinfo::Pid;
3+
4+
pub fn terminate_process(pid: Pid) -> Result<(), String> {
5+
let nix_pid = nix::unistd::Pid::from_raw(pid.as_u32() as i32);
6+
nix::sys::signal::kill(nix_pid, Signal::SIGTERM).map_err(|e| format!("Failed to terminate process: {}", e))
7+
}
8+
9+
#[cfg(test)]
10+
#[cfg(not(windows))]
11+
mod tests {
12+
use std::process::Command;
13+
use std::time::Duration;
14+
15+
use super::*;
16+
17+
// Helper to create a long-running process for testing
18+
fn spawn_test_process() -> std::process::Child {
19+
let mut command = Command::new("sleep");
20+
command.arg("30");
21+
command.spawn().expect("Failed to spawn test process")
22+
}
23+
24+
#[test]
25+
fn test_terminate_process() {
26+
// Spawn a test process
27+
let mut child = spawn_test_process();
28+
let pid = Pid::from_u32(child.id());
29+
30+
// Terminate the process
31+
let result = terminate_process(pid);
32+
33+
// Verify termination was successful
34+
assert!(result.is_ok(), "Process termination failed: {:?}", result.err());
35+
36+
// Give it a moment to terminate
37+
std::thread::sleep(Duration::from_millis(100));
38+
39+
// Verify the process is actually terminated
40+
match child.try_wait() {
41+
Ok(Some(_)) => {
42+
// Process exited, which is what we expect
43+
},
44+
Ok(None) => {
45+
panic!("Process is still running after termination");
46+
},
47+
Err(e) => {
48+
panic!("Error checking process status: {}", e);
49+
},
50+
}
51+
}
52+
53+
#[test]
54+
fn test_terminate_nonexistent_process() {
55+
// Use a likely invalid PID
56+
let invalid_pid = Pid::from_u32(u32::MAX - 1);
57+
58+
// Attempt to terminate a non-existent process
59+
let result = terminate_process(invalid_pid);
60+
61+
// Should return an error
62+
assert!(result.is_err(), "Terminating non-existent process should fail");
63+
}
64+
}

0 commit comments

Comments
 (0)