1- use anyhow:: Result ;
1+ use std:: time:: Duration ;
2+
3+ use tokio:: process:: Command ;
4+ use tokio:: time:: timeout;
5+
6+ use crate :: channel:: IpcChannel ;
7+ use crate :: message:: IpcMessage ;
28
39#[ derive( Debug , Clone , Copy , PartialEq ) ]
410pub enum ProcessKind {
@@ -7,6 +13,149 @@ pub enum ProcessKind {
713 Network ,
814}
915
10- pub fn spawn_child ( _kind : ProcessKind ) -> Result < ( ) > {
11- todo ! ( "Spawn sandboxed child process" )
16+ impl ProcessKind {
17+ pub fn as_str ( & self ) -> & ' static str {
18+ match self {
19+ ProcessKind :: Browser => "browser" ,
20+ ProcessKind :: Renderer => "renderer" ,
21+ ProcessKind :: Network => "network" ,
22+ }
23+ }
24+
25+ pub fn parse ( s : & str ) -> Option < Self > {
26+ match s {
27+ "browser" => Some ( Self :: Browser ) ,
28+ "renderer" => Some ( Self :: Renderer ) ,
29+ "network" => Some ( Self :: Network ) ,
30+ _ => None ,
31+ }
32+ }
33+ }
34+
35+ pub struct ChildHandle {
36+ process : tokio:: process:: Child ,
37+ channel : IpcChannel ,
38+ kind : ProcessKind ,
39+ }
40+
41+ const SHUTDOWN_TIMEOUT : Duration = Duration :: from_secs ( 5 ) ;
42+
43+ impl ChildHandle {
44+ pub fn channel ( & mut self ) -> & mut IpcChannel {
45+ & mut self . channel
46+ }
47+
48+ pub fn kind ( & self ) -> ProcessKind {
49+ self . kind
50+ }
51+
52+ pub fn process_id ( & self ) -> u32 {
53+ self . process . id ( ) . unwrap_or ( 0 )
54+ }
55+
56+ pub fn is_alive ( & mut self ) -> bool {
57+ matches ! ( self . process. try_wait( ) , Ok ( None ) )
58+ }
59+
60+ /// Graceful shutdown: send Shutdown, wait, kill if timeout.
61+ pub async fn shutdown ( & mut self ) -> anyhow:: Result < ( ) > {
62+ let _ = self . channel . send ( & IpcMessage :: Shutdown ) . await ;
63+ match timeout ( SHUTDOWN_TIMEOUT , self . process . wait ( ) ) . await {
64+ Ok ( Ok ( status) ) => {
65+ tracing:: info!( "{:?} process exited with status: {}" , self . kind, status) ;
66+ Ok ( ( ) )
67+ }
68+ Ok ( Err ( e) ) => {
69+ tracing:: error!( "{:?} process wait error: {e}" , self . kind) ;
70+ Err ( e. into ( ) )
71+ }
72+ Err ( _) => {
73+ tracing:: warn!(
74+ "{:?} process did not exit within {:?}, killing" ,
75+ self . kind,
76+ SHUTDOWN_TIMEOUT
77+ ) ;
78+ self . process . kill ( ) . await ?;
79+ Ok ( ( ) )
80+ }
81+ }
82+ }
83+
84+ pub async fn kill ( & mut self ) -> anyhow:: Result < ( ) > {
85+ self . process . kill ( ) . await ?;
86+ Ok ( ( ) )
87+ }
88+ }
89+
90+ impl Drop for ChildHandle {
91+ fn drop ( & mut self ) {
92+ // Best-effort kill to avoid zombies
93+ let _ = self . process . start_kill ( ) ;
94+ }
95+ }
96+
97+ /// Spawn a child process that communicates via IPC.
98+ /// Uses the current executable by default.
99+ #[ cfg( unix) ]
100+ pub async fn spawn_child ( kind : ProcessKind ) -> anyhow:: Result < ChildHandle > {
101+ spawn_child_with_exe ( kind, std:: env:: current_exe ( ) ?) . await
102+ }
103+
104+ /// Spawn a child process using a specific executable path.
105+ #[ cfg( unix) ]
106+ pub async fn spawn_child_with_exe (
107+ kind : ProcessKind ,
108+ exe_path : std:: path:: PathBuf ,
109+ ) -> anyhow:: Result < ChildHandle > {
110+ let ( parent_channel, child_fd) = IpcChannel :: pair_for_spawn ( ) ?;
111+
112+ let mut cmd = Command :: new ( exe_path) ;
113+ cmd. arg ( "--subprocess-kind" ) . arg ( kind. as_str ( ) ) ;
114+ cmd. env ( "IE_IPC_FD" , child_fd. to_string ( ) ) ;
115+ // Inherit stderr for child tracing output, null stdin/stdout
116+ cmd. stdin ( std:: process:: Stdio :: null ( ) ) ;
117+ cmd. stdout ( std:: process:: Stdio :: null ( ) ) ;
118+ cmd. stderr ( std:: process:: Stdio :: inherit ( ) ) ;
119+
120+ // Ensure child fd survives exec (clear CLOEXEC was done in pair_for_spawn)
121+ // After spawn, close child fd in parent
122+ unsafe {
123+ cmd. pre_exec ( move || {
124+ // The fd is already non-CLOEXEC from pair_for_spawn, nothing more needed
125+ Ok ( ( ) )
126+ } ) ;
127+ }
128+
129+ let process = cmd. spawn ( ) ?;
130+
131+ // Close the child fd in the parent process
132+ unsafe {
133+ libc:: close ( child_fd) ;
134+ }
135+
136+ Ok ( ChildHandle {
137+ process,
138+ channel : parent_channel,
139+ kind,
140+ } )
141+ }
142+
143+ // Spawn tests live in crates/ie-shell/tests/subprocess.rs because
144+ // spawn_child re-executes the binary which needs --subprocess-kind CLI support.
145+
146+ #[ cfg( test) ]
147+ mod tests {
148+ use super :: * ;
149+
150+ #[ test]
151+ fn process_kind_round_trip ( ) {
152+ for kind in [
153+ ProcessKind :: Browser ,
154+ ProcessKind :: Renderer ,
155+ ProcessKind :: Network ,
156+ ] {
157+ assert_eq ! ( ProcessKind :: parse( kind. as_str( ) ) , Some ( kind) ) ;
158+ }
159+ assert_eq ! ( ProcessKind :: parse( "invalid" ) , None ) ;
160+ }
12161}
0 commit comments