@@ -104,26 +104,26 @@ impl McpClient {
104104 }
105105}
106106
107- /// Take the stdin and (buffered) stdout out of a freshly-spawned child.
108- /// Returning `Err` here is essentially unreachable because
109- /// `Command::spawn` with ` Stdio::piped()` on both ends always populates
110- /// `Child::stdin` / `Child::stdout`, but we still handle it — leaking
111- /// a live `Child` out to `Drop` would fail to reap it ( the std type
112- /// doesn 't wait).
113- fn take_child_pipes ( process : & mut Child , server_name : & str ) -> Result < ( ChildStdin , ChildStdout ) > {
114- let stdin = process . stdin . take ( ) . ok_or_else ( || {
115- SofosError :: McpError ( format ! (
116- "Failed to get stdin for MCP server '{}'" ,
117- server_name
118- ) )
119- } ) ? ;
120- let stdout = process . stdout . take ( ) . ok_or_else ( || {
121- SofosError :: McpError ( format ! (
122- "Failed to get stdout for MCP server '{}'" ,
123- server_name
124- ) )
125- } ) ? ;
126- Ok ( ( stdin , stdout ) )
107+ /// Take stdin/ stdout from a freshly-spawned MCP child. Reaching the
108+ /// `Err` branch is practically unreachable — `Command::spawn` with
109+ /// `Stdio::piped()` on both ends always populates the pipes — but the
110+ /// type system doesn't enforce it. Owning the `Child` lets us reap it
111+ /// internally on the impossible branch, so a `?` at the call site
112+ /// can 't accidentally leak a zombie via `Child::drop` (which doesn't
113+ /// wait).
114+ fn take_child_pipes (
115+ mut process : Child ,
116+ server_name : & str ,
117+ ) -> Result < ( Child , ChildStdin , ChildStdout ) > {
118+ if let ( Some ( stdin ) , Some ( stdout ) ) = ( process . stdin . take ( ) , process . stdout . take ( ) ) {
119+ return Ok ( ( process , stdin , stdout ) ) ;
120+ }
121+ let _ = process . kill ( ) ;
122+ let _ = process . wait ( ) ;
123+ Err ( SofosError :: McpError ( format ! (
124+ "Failed to acquire stdin/stdout for MCP server '{}'" ,
125+ server_name
126+ ) ) )
127127}
128128
129129/// Write a single stdio message to the server (JSON-RPC request *or*
@@ -239,26 +239,17 @@ impl StdioClient {
239239 cmd. env ( key, value) ;
240240 }
241241
242- let mut process = cmd. spawn ( ) . map_err ( |e| {
242+ let process = cmd. spawn ( ) . map_err ( |e| {
243243 SofosError :: McpError ( format ! (
244244 "Failed to start MCP server '{}': {}" ,
245245 server_name, e
246246 ) )
247247 } ) ?;
248248
249- // Take the pipes through a helper that reaps the child on
250- // error, so a (practically-impossible but not-type-proven)
251- // `None` from `take()` doesn't leak a zombie into the OS
252- // process table. Once the child is wrapped in `Self`, the
253- // `Drop` impl takes over.
254- let ( stdin, stdout) = match take_child_pipes ( & mut process, & server_name) {
255- Ok ( pair) => pair,
256- Err ( e) => {
257- let _ = process. kill ( ) ;
258- let _ = process. wait ( ) ;
259- return Err ( e) ;
260- }
261- } ;
249+ // `take_child_pipes` reaps the child if either pipe is missing,
250+ // so the `?` here can't leak a zombie. Once the child is
251+ // wrapped in `Self`, the `Drop` impl takes over.
252+ let ( process, stdin, stdout) = take_child_pipes ( process, & server_name) ?;
262253
263254 let client = Self {
264255 server_name : server_name. clone ( ) ,
0 commit comments