@@ -5,9 +5,13 @@ use serde::{Deserialize, Serialize};
55use serde_json:: json;
66use std:: collections:: VecDeque ;
77use std:: env;
8+ #[ cfg( windows) ]
9+ use std:: ffi:: c_void;
810use std:: ffi:: OsStr ;
911use std:: fs;
1012use std:: io:: { BufRead , BufReader , Read , Write } ;
13+ #[ cfg( windows) ]
14+ use std:: mem:: { size_of, zeroed} ;
1115use std:: net:: TcpStream ;
1216#[ cfg( unix) ]
1317use std:: os:: unix:: process:: CommandExt ;
@@ -19,12 +23,95 @@ use std::thread;
1923use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
2024use tauri:: { webview:: cookie:: Cookie , AppHandle , Emitter , Manager , Url } ;
2125
26+ #[ cfg( windows) ]
27+ use std:: os:: windows:: io:: AsRawHandle ;
2228#[ cfg( windows) ]
2329use std:: os:: windows:: process:: CommandExt ;
30+ #[ cfg( windows) ]
31+ use windows_sys:: Win32 :: Foundation :: { CloseHandle , HANDLE } ;
32+ #[ cfg( windows) ]
33+ use windows_sys:: Win32 :: System :: JobObjects :: {
34+ AssignProcessToJobObject , CreateJobObjectW , JobObjectExtendedLimitInformation ,
35+ SetInformationJobObject , JOBOBJECT_EXTENDED_LIMIT_INFORMATION ,
36+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ,
37+ } ;
2438
2539#[ cfg( windows) ]
2640const CREATE_NO_WINDOW : u32 = 0x08000000 ;
2741
42+ #[ cfg( windows) ]
43+ #[ derive( Debug ) ]
44+ struct WindowsJobObject {
45+ // The desktop wrapper may observe only a short-lived Node wrapper PID while the real
46+ // server and workspace descendants continue running below it. KILL_ON_JOB_CLOSE gives
47+ // Tauri an OS-owned handle for the whole subtree instead of relying on a single PID.
48+ handle : HANDLE ,
49+ }
50+
51+ #[ cfg( windows) ]
52+ impl WindowsJobObject {
53+ fn create ( ) -> anyhow:: Result < Self > {
54+ let handle = unsafe { CreateJobObjectW ( std:: ptr:: null_mut ( ) , std:: ptr:: null ( ) ) } ;
55+ if handle. is_null ( ) {
56+ return Err ( anyhow:: anyhow!(
57+ "CreateJobObjectW failed: {}" ,
58+ std:: io:: Error :: last_os_error( )
59+ ) ) ;
60+ }
61+
62+ let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed ( ) } ;
63+ info. BasicLimitInformation . LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ;
64+
65+ let ok = unsafe {
66+ SetInformationJobObject (
67+ handle,
68+ JobObjectExtendedLimitInformation ,
69+ & mut info as * mut _ as * mut c_void ,
70+ size_of :: < JOBOBJECT_EXTENDED_LIMIT_INFORMATION > ( ) as u32 ,
71+ )
72+ } ;
73+ if ok == 0 {
74+ let err = std:: io:: Error :: last_os_error ( ) ;
75+ unsafe {
76+ CloseHandle ( handle) ;
77+ }
78+ return Err ( anyhow:: anyhow!( "SetInformationJobObject failed: {}" , err) ) ;
79+ }
80+
81+ Ok ( Self { handle } )
82+ }
83+
84+ fn assign_child ( & self , child : & Child ) -> anyhow:: Result < ( ) > {
85+ let process_handle = child. as_raw_handle ( ) as HANDLE ;
86+ let ok = unsafe { AssignProcessToJobObject ( self . handle , process_handle) } ;
87+ if ok == 0 {
88+ return Err ( anyhow:: anyhow!(
89+ "AssignProcessToJobObject failed: {}" ,
90+ std:: io:: Error :: last_os_error( )
91+ ) ) ;
92+ }
93+
94+ Ok ( ( ) )
95+ }
96+ }
97+
98+ #[ cfg( windows) ]
99+ impl Drop for WindowsJobObject {
100+ fn drop ( & mut self ) {
101+ if !self . handle . is_null ( ) {
102+ unsafe {
103+ CloseHandle ( self . handle ) ;
104+ }
105+ }
106+ }
107+ }
108+
109+ #[ cfg( windows) ]
110+ unsafe impl Send for WindowsJobObject { }
111+
112+ #[ cfg( windows) ]
113+ unsafe impl Sync for WindowsJobObject { }
114+
28115fn log_line ( message : & str ) {
29116 println ! ( "[tauri-cli] {message}" ) ;
30117}
@@ -363,6 +450,8 @@ impl Default for CliStatus {
363450pub struct CliProcessManager {
364451 status : Arc < Mutex < CliStatus > > ,
365452 child : Arc < Mutex < Option < Child > > > ,
453+ #[ cfg( windows) ]
454+ job : Arc < Mutex < Option < WindowsJobObject > > > ,
366455 ready : Arc < AtomicBool > ,
367456 bootstrap_token : Arc < Mutex < Option < String > > > ,
368457}
@@ -372,6 +461,8 @@ impl CliProcessManager {
372461 Self {
373462 status : Arc :: new ( Mutex :: new ( CliStatus :: default ( ) ) ) ,
374463 child : Arc :: new ( Mutex :: new ( None ) ) ,
464+ #[ cfg( windows) ]
465+ job : Arc :: new ( Mutex :: new ( None ) ) ,
375466 ready : Arc :: new ( AtomicBool :: new ( false ) ) ,
376467 bootstrap_token : Arc :: new ( Mutex :: new ( None ) ) ,
377468 }
@@ -394,13 +485,17 @@ impl CliProcessManager {
394485
395486 let status_arc = self . status . clone ( ) ;
396487 let child_arc = self . child . clone ( ) ;
488+ #[ cfg( windows) ]
489+ let job_arc = self . job . clone ( ) ;
397490 let ready_flag = self . ready . clone ( ) ;
398491 let token_arc = self . bootstrap_token . clone ( ) ;
399492 thread:: spawn ( move || {
400493 if let Err ( err) = Self :: spawn_cli (
401494 app. clone ( ) ,
402495 status_arc. clone ( ) ,
403496 child_arc,
497+ #[ cfg( windows) ]
498+ job_arc,
404499 ready_flag,
405500 token_arc,
406501 dev,
@@ -420,11 +515,12 @@ impl CliProcessManager {
420515 }
421516
422517 pub fn stop ( & self ) -> anyhow:: Result < ( ) > {
518+ #[ cfg( windows) ]
519+ let _job = self . job . lock ( ) . take ( ) ;
520+
423521 let mut child_opt = self . child . lock ( ) ;
424522 if let Some ( mut child) = child_opt. take ( ) {
425523 log_line ( & format ! ( "stopping CLI pid={}" , child. id( ) ) ) ;
426- #[ cfg( windows) ]
427- let mut forced_tree_shutdown = false ;
428524 #[ cfg( unix) ]
429525 unsafe {
430526 let pid = child. id ( ) as i32 ;
@@ -446,18 +542,16 @@ impl CliProcessManager {
446542 Ok ( Some ( _) ) => break ,
447543 Ok ( None ) => {
448544 #[ cfg( windows) ]
449- if !forced_tree_shutdown
450- && start. elapsed ( ) > Duration :: from_millis ( CLI_WINDOWS_FORCE_GRACE_MS )
451- {
545+ if start. elapsed ( ) > Duration :: from_millis ( CLI_WINDOWS_FORCE_GRACE_MS ) {
452546 log_line ( & format ! (
453547 "regular Windows shutdown still running after {}ms; escalating pid={}" ,
454548 CLI_WINDOWS_FORCE_GRACE_MS ,
455549 child. id( )
456550 ) ) ;
457- forced_tree_shutdown = true ;
458551 if !kill_process_tree_windows ( child. id ( ) , true ) {
459552 let _ = child. kill ( ) ;
460553 }
554+ break ;
461555 }
462556
463557 if start. elapsed ( ) > Duration :: from_secs ( CLI_STOP_GRACE_SECS ) {
@@ -476,11 +570,7 @@ impl CliProcessManager {
476570 }
477571 #[ cfg( windows) ]
478572 {
479- if !forced_tree_shutdown
480- && !kill_process_tree_windows ( child. id ( ) , true )
481- {
482- let _ = child. kill ( ) ;
483- } else if forced_tree_shutdown {
573+ if !kill_process_tree_windows ( child. id ( ) , true ) {
484574 let _ = child. kill ( ) ;
485575 }
486576 }
@@ -491,6 +581,9 @@ impl CliProcessManager {
491581 Err ( _) => break ,
492582 }
493583 }
584+ } else {
585+ #[ cfg( windows) ]
586+ log_line ( "tracked CLI process already exited; dropping Windows job object to reap descendants" ) ;
494587 }
495588
496589 let mut status = self . status . lock ( ) ;
@@ -511,6 +604,7 @@ impl CliProcessManager {
511604 app : AppHandle ,
512605 status : Arc < Mutex < CliStatus > > ,
513606 child_holder : Arc < Mutex < Option < Child > > > ,
607+ #[ cfg( windows) ] job_holder : Arc < Mutex < Option < WindowsJobObject > > > ,
514608 ready : Arc < AtomicBool > ,
515609 bootstrap_token : Arc < Mutex < Option < String > > > ,
516610 dev : bool ,
@@ -592,6 +686,22 @@ impl CliProcessManager {
592686
593687 let pid = child. id ( ) ;
594688 log_line ( & format ! ( "spawned pid={pid}" ) ) ;
689+ #[ cfg( windows) ]
690+ match WindowsJobObject :: create ( ) . and_then ( |job| {
691+ job. assign_child ( & child) ?;
692+ Ok ( job)
693+ } ) {
694+ Ok ( job) => {
695+ log_line ( & format ! ( "attached pid={pid} to Windows job object" ) ) ;
696+ * job_holder. lock ( ) = Some ( job) ;
697+ }
698+ Err ( err) => {
699+ log_line ( & format ! (
700+ "failed to attach pid={pid} to Windows job object; falling back to taskkill-only cleanup: {err}"
701+ ) ) ;
702+ }
703+ }
704+
595705 {
596706 let mut locked = status. lock ( ) ;
597707 locked. pid = Some ( pid) ;
@@ -665,6 +775,8 @@ impl CliProcessManager {
665775 let status_clone = status. clone ( ) ;
666776 let ready_clone = ready. clone ( ) ;
667777 let child_holder_clone = child_holder. clone ( ) ;
778+ #[ cfg( windows) ]
779+ let job_holder_clone = job_holder. clone ( ) ;
668780 thread:: spawn ( move || {
669781 let timeout = Duration :: from_secs ( 60 ) ;
670782 thread:: sleep ( timeout) ;
@@ -719,6 +831,10 @@ impl CliProcessManager {
719831 // Drop the handle after the process exits so other callers
720832 // don't attempt to stop/kill a finished process.
721833 * guard = None ;
834+ #[ cfg( windows) ]
835+ {
836+ let _ = job_holder_clone. lock ( ) . take ( ) ;
837+ }
722838 Some ( status)
723839 }
724840 None => None ,
@@ -776,7 +892,8 @@ impl CliProcessManager {
776892 auth_cookie_name : & str ,
777893 ) {
778894 let mut buffer = String :: new ( ) ;
779- let local_url_regex = Regex :: new ( r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)\s*$" ) . ok ( ) ;
895+ let local_url_regex =
896+ Regex :: new ( r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)\s*$" ) . ok ( ) ;
780897 let token_prefix = "CODENOMAD_BOOTSTRAP_TOKEN:" ;
781898
782899 loop {
@@ -818,7 +935,6 @@ impl CliProcessManager {
818935 ) ;
819936 continue ;
820937 }
821-
822938 }
823939 }
824940 Err ( _) => break ,
@@ -1022,15 +1138,23 @@ fn resolve_tsx(_app: &AppHandle) -> Option<String> {
10221138 let cwd = std:: env:: current_dir ( ) . ok ( ) ;
10231139 let workspace = workspace_root ( ) ;
10241140 let mut candidates = vec ! [
1025- cwd. as_ref( ) . map( |p| p. join( "node_modules/tsx/dist/cli.mjs" ) ) ,
1026- cwd. as_ref( ) . map( |p| p. join( "node_modules/tsx/dist/cli.cjs" ) ) ,
1141+ cwd. as_ref( )
1142+ . map( |p| p. join( "node_modules/tsx/dist/cli.mjs" ) ) ,
1143+ cwd. as_ref( )
1144+ . map( |p| p. join( "node_modules/tsx/dist/cli.cjs" ) ) ,
10271145 cwd. as_ref( ) . map( |p| p. join( "node_modules/tsx/dist/cli.js" ) ) ,
1028- cwd. as_ref( ) . map( |p| p. join( "../node_modules/tsx/dist/cli.mjs" ) ) ,
1029- cwd. as_ref( ) . map( |p| p. join( "../node_modules/tsx/dist/cli.cjs" ) ) ,
1030- cwd. as_ref( ) . map( |p| p. join( "../node_modules/tsx/dist/cli.js" ) ) ,
1031- cwd. as_ref( ) . map( |p| p. join( "../../node_modules/tsx/dist/cli.mjs" ) ) ,
1032- cwd. as_ref( ) . map( |p| p. join( "../../node_modules/tsx/dist/cli.cjs" ) ) ,
1033- cwd. as_ref( ) . map( |p| p. join( "../../node_modules/tsx/dist/cli.js" ) ) ,
1146+ cwd. as_ref( )
1147+ . map( |p| p. join( "../node_modules/tsx/dist/cli.mjs" ) ) ,
1148+ cwd. as_ref( )
1149+ . map( |p| p. join( "../node_modules/tsx/dist/cli.cjs" ) ) ,
1150+ cwd. as_ref( )
1151+ . map( |p| p. join( "../node_modules/tsx/dist/cli.js" ) ) ,
1152+ cwd. as_ref( )
1153+ . map( |p| p. join( "../../node_modules/tsx/dist/cli.mjs" ) ) ,
1154+ cwd. as_ref( )
1155+ . map( |p| p. join( "../../node_modules/tsx/dist/cli.cjs" ) ) ,
1156+ cwd. as_ref( )
1157+ . map( |p| p. join( "../../node_modules/tsx/dist/cli.js" ) ) ,
10341158 workspace
10351159 . as_ref( )
10361160 . map( |p| p. join( "node_modules/tsx/dist/cli.mjs" ) ) ,
0 commit comments