@@ -11,7 +11,7 @@ use std::{
1111 sync:: { Arc , Mutex } ,
1212 time:: { Duration , SystemTime , UNIX_EPOCH } ,
1313} ;
14- use tauri:: { AppHandle , Emitter , Manager } ;
14+ use tauri:: { AppHandle , Emitter , Manager , RunEvent } ;
1515use tauri_plugin_shell:: { process:: CommandEvent , ShellExt } ;
1616
1717enum SidecarMsg {
@@ -26,6 +26,7 @@ struct SidecarState {
2626 tx : SidecarSender ,
2727 pending_requests : PendingRequests ,
2828 next_request_id : AtomicU64 ,
29+ child_pid : u32 ,
2930}
3031
3132const LOG_FILE_ENV : & str = "MOUSETERM_LOG_FILE" ;
@@ -215,6 +216,28 @@ fn pty_get_scrollback(
215216#[ tauri:: command]
216217fn shutdown_sidecar ( state : tauri:: State < ' _ , SidecarState > ) {
217218 let _ = state. tx . send ( SidecarMsg :: Shutdown ) ;
219+ kill_process_tree ( state. child_pid ) ;
220+ }
221+
222+ /// Kill the sidecar process. On Windows, `taskkill /T` kills the entire
223+ /// process tree so that child shell processes don't outlive the sidecar.
224+ /// On Unix, a single SIGTERM to the sidecar is sufficient because node-pty
225+ /// manages its own child processes and cleans them up on exit.
226+ fn kill_process_tree ( pid : u32 ) {
227+ append_log ( format ! ( "[sidecar] killing process tree (pid={pid})" ) ) ;
228+ #[ cfg( target_os = "windows" ) ]
229+ {
230+ use std:: os:: windows:: process:: CommandExt ;
231+ const CREATE_NO_WINDOW : u32 = 0x08000000 ;
232+ let _ = std:: process:: Command :: new ( "taskkill" )
233+ . args ( [ "/F" , "/T" , "/PID" , & pid. to_string ( ) ] )
234+ . creation_flags ( CREATE_NO_WINDOW )
235+ . output ( ) ;
236+ }
237+ #[ cfg( unix) ]
238+ {
239+ unsafe { libc:: kill ( pid as i32 , libc:: SIGTERM ) ; }
240+ }
218241}
219242
220243#[ tauri:: command]
@@ -298,7 +321,8 @@ fn start_sidecar(app: &AppHandle) -> Result<SidecarState, String> {
298321 . set_raw_out ( false )
299322 . spawn ( )
300323 . map_err ( |err| format ! ( "failed to start Node.js sidecar: {err}" ) ) ?;
301- append_log ( "[sidecar] spawned Node.js runtime" ) ;
324+ let child_pid = child. pid ( ) ;
325+ append_log ( format ! ( "[sidecar] spawned Node.js runtime (pid={child_pid})" ) ) ;
302326
303327 let handle = app. clone ( ) ;
304328 let pending_requests: PendingRequests = Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ;
@@ -392,6 +416,7 @@ fn start_sidecar(app: &AppHandle) -> Result<SidecarState, String> {
392416 tx,
393417 pending_requests,
394418 next_request_id : AtomicU64 :: new ( 0 ) ,
419+ child_pid,
395420 } )
396421}
397422
@@ -437,8 +462,17 @@ pub fn run() {
437462 get_project_dir,
438463 get_available_shells,
439464 ] )
440- . run ( tauri:: generate_context!( ) )
441- . expect ( "error while running MouseTerm" ) ;
465+ . build ( tauri:: generate_context!( ) )
466+ . expect ( "error while building MouseTerm" )
467+ . run ( |app, event| {
468+ if let RunEvent :: Exit = event {
469+ if let Some ( state) = app. try_state :: < SidecarState > ( ) {
470+ append_log ( "[app] exit — killing sidecar" ) ;
471+ let _ = state. tx . send ( SidecarMsg :: Shutdown ) ;
472+ kill_process_tree ( state. child_pid ) ;
473+ }
474+ }
475+ } ) ;
442476}
443477
444478#[ cfg( test) ]
0 commit comments