@@ -5,7 +5,6 @@ use crate::agent_control::defaults::{STDERR_LOG_PREFIX, STDOUT_LOG_PREFIX};
55use crate :: sub_agent:: on_host:: command:: executable_data:: ExecutableData ;
66use std:: time:: { Duration , Instant } ;
77use std:: {
8- io,
98 path:: PathBuf ,
109 process:: { Child , Command , ExitStatus , Stdio } ,
1110} ;
@@ -18,6 +17,8 @@ use super::{
1817 logger:: Logger ,
1918 } ,
2019} ;
20+ #[ cfg( target_family = "windows" ) ]
21+ use crate :: sub_agent:: on_host:: command:: job_object:: JobObject ;
2122
2223const POLL_INTERVAL : Duration = Duration :: from_millis ( 100 ) ;
2324
@@ -36,6 +37,9 @@ pub struct CommandOSStarted {
3637 process : Child ,
3738 loggers : Option < FileSystemLoggers > ,
3839 shutdown_timeout : Duration ,
40+
41+ #[ cfg( target_family = "windows" ) ]
42+ job_object : Option < JobObject > ,
3943}
4044
4145////////////////////////////////////////////////////////////////////////////////////
@@ -54,9 +58,6 @@ impl CommandOSNotStarted {
5458 . stdout ( Stdio :: piped ( ) )
5559 . stderr ( Stdio :: piped ( ) ) ;
5660
57- #[ cfg( target_family = "windows" ) ]
58- Self :: create_process_group ( & mut cmd) ;
59-
6061 Self {
6162 agent_id,
6263 cmd,
@@ -74,32 +75,31 @@ impl CommandOSNotStarted {
7475 file_logger ( & agent_id, self . logging_path , STDERR_LOG_PREFIX ) ,
7576 )
7677 } ) ;
77- Ok ( CommandOSStarted {
78- agent_id,
79- process : self . cmd . spawn ( ) ?,
80- loggers,
81- shutdown_timeout : self . shutdown_timeout ,
82- } )
83- }
78+ let child = self . cmd . spawn ( ) ?;
8479
85- #[ cfg( target_family = "windows" ) ]
86- /// Sets the process creation flags to create a new process group for Windows processes.
87- ///
88- /// This enables sending CTRL+BREAK events to it via the [`GenerateConsoleCtrlEvent`](windows::Win32::System::Console::GenerateConsoleCtrlEvent) function,
89- /// which is the mechanism we use to gracefully shut down the process. Otherwise, the Agent Control process needs to attach itself to the
90- /// console of the process to send a CTRL+C event which would need synchronization (many sub-agents making AC attach and reattach concurrently).
91- ///
92- /// For details, see the [task termination mechanism for GitLab runners](https://gitlab.com/gitlab-org/gitlab-runner/-/blob/397ba5dc2685e7b13feaccbfed4c242646955334/helpers/process/killer_windows.go#L75-108), which can use either mechanism dependent on a flag to use the legacy method (attach and reattach the parent process).
93- ///
94- /// Additional reading:
95- /// - [`GenerateConsoleCtrlEvent` function](https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent), see second parameter `dwProcessGroupId`.
96- /// - [Process Creation Flags](https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags)
97- fn create_process_group ( cmd : & mut Command ) {
98- use std:: os:: windows:: process:: CommandExt ;
99- use windows:: Win32 :: System :: Threading :: CREATE_NEW_PROCESS_GROUP ;
100-
101- // Create new process group so we can send CTRL+BREAK events to it
102- cmd. creation_flags ( CREATE_NEW_PROCESS_GROUP . 0 ) ;
80+ #[ cfg( target_family = "unix" ) ]
81+ {
82+ Ok ( CommandOSStarted {
83+ agent_id,
84+ process : child,
85+ loggers,
86+ shutdown_timeout : self . shutdown_timeout ,
87+ } )
88+ }
89+ #[ cfg( target_family = "windows" ) ]
90+ {
91+ // Each started process gets its own JobObject. All sub-processes that the process spawns
92+ // will be assigned to the same JobObject, allowing for a graceful shutdown of the entire process tree.
93+ let job_object = JobObject :: new ( ) ?;
94+ job_object. assign_process ( & child) ?;
95+ Ok ( CommandOSStarted {
96+ agent_id,
97+ process : child,
98+ job_object : Some ( job_object) ,
99+ loggers,
100+ shutdown_timeout : self . shutdown_timeout ,
101+ } )
102+ }
103103 }
104104}
105105
@@ -164,12 +164,11 @@ impl CommandOSStarted {
164164 }
165165
166166 pub fn shutdown ( & mut self ) -> Result < ( ) , CommandError > {
167- let pid = self . get_pid ( ) ;
168167 // Attempt a graceful shutdown (platform-dependent).
169- let graceful_shutdown_result = Self :: graceful_shutdown ( pid ) ;
168+ let graceful_shutdown_result = self . graceful_shutdown ( ) ;
170169
171170 if let Err ( e) = & graceful_shutdown_result {
172- warn ! ( agent_id = %self . agent_id, "Graceful shutdown failed for process {pid }: {e}" ) ;
171+ warn ! ( agent_id = %self . agent_id, "Graceful shutdown failed for process {}: {e}" , self . get_pid ( ) ) ;
173172 }
174173
175174 if graceful_shutdown_result. is_err ( ) || self . is_running_after_timeout ( self . shutdown_timeout )
@@ -181,21 +180,25 @@ impl CommandOSStarted {
181180 }
182181
183182 #[ cfg( target_family = "unix" ) ]
184- fn graceful_shutdown ( pid : u32 ) -> Result < ( ) , CommandError > {
183+ fn graceful_shutdown ( & self ) -> Result < ( ) , CommandError > {
185184 use nix:: { sys:: signal, unistd:: Pid } ;
185+ let pid = self . get_pid ( ) ;
186186
187187 signal:: kill ( Pid :: from_raw ( pid as i32 ) , signal:: SIGTERM )
188- . map_err ( |e| CommandError :: from ( io:: Error :: from ( e) ) )
188+ . map_err ( |e| CommandError :: from ( std :: io:: Error :: from ( e) ) )
189189 }
190190
191191 #[ cfg( target_family = "windows" ) ]
192- fn graceful_shutdown ( pid : u32 ) -> Result < ( ) , CommandError > {
193- use windows:: Win32 :: System :: Console :: { CTRL_BREAK_EVENT , GenerateConsoleCtrlEvent } ;
194- // Graceful shutdown for console applications
195- // <https://stackoverflow.com/a/12899284>
196- // <https://gitlab.com/gitlab-org/gitlab-runner/-/blob/397ba5dc2685e7b13feaccbfed4c242646955334/helpers/process/killer_windows.go#L75-108>
197- unsafe { GenerateConsoleCtrlEvent ( CTRL_BREAK_EVENT , pid) }
198- . map_err ( |e| CommandError :: from ( io:: Error :: from ( e) ) )
192+ /// On Windows there is no direct equivalent to sending SIGTERM. Applications that runs as
193+ /// services handles stops signals via Service Control Manager (SCM), and console applications
194+ /// can handle Ctrl-C or Ctrl-Break events via attached consoles.
195+ /// The current implementation uses Job Objects to manage process groups, and there is no graceful
196+ /// shutdown signal sent to the processes. The Job Object will terminate all associated processes.
197+ fn graceful_shutdown ( & mut self ) -> Result < ( ) , CommandError > {
198+ if let Some ( job_object) = self . job_object . take ( ) {
199+ job_object. kill ( ) ?;
200+ }
201+ Ok ( ( ) )
199202 }
200203}
201204
0 commit comments