44//! task execution, and evaluation phases.
55
66const std = @import ("std" );
7+ const posix = std .posix ;
78const Allocator = std .mem .Allocator ;
89
910const config = @import ("config.zig" );
@@ -22,13 +23,17 @@ pub const IdeaSelection = struct {
2223 reason : []const u8 ,
2324};
2425
26+ /// Graceful shutdown timeout in seconds
27+ const GRACEFUL_SHUTDOWN_TIMEOUT_SECS : u64 = 5 ;
28+
2529/// Executor for running opencode CLI commands
2630pub const Executor = struct {
2731 cfg : * const config.Config ,
2832 log : * Logger ,
2933 allocator : Allocator ,
3034 opencode_cmd : []const u8 ,
3135 current_child_pid : ? std.posix.pid_t ,
36+ current_child_pgid : ? std.posix.pid_t ,
3237
3338 /// Initialize executor
3439 pub fn init (cfg : * const config.Config , log : * Logger , allocator : Allocator ) Executor {
@@ -38,6 +43,7 @@ pub const Executor = struct {
3843 .allocator = allocator ,
3944 .opencode_cmd = "opencode" ,
4045 .current_child_pid = null ,
46+ .current_child_pgid = null ,
4147 };
4248 }
4349
@@ -49,6 +55,7 @@ pub const Executor = struct {
4955 .allocator = allocator ,
5056 .opencode_cmd = opencode_cmd ,
5157 .current_child_pid = null ,
58+ .current_child_pgid = null ,
5259 };
5360 }
5461
@@ -72,7 +79,7 @@ pub const Executor = struct {
7279
7380 /// Run task execution
7481 pub fn runTask (self : * Executor , task_desc : []const u8 , cycle : u32 , task_num : u32 , total_tasks : u32 ) ! ExecutionResult {
75- self .log .statusFmt ("[Cycle {d}] Task {d}/{d}: {s}" , .{ cycle , task_num , total_tasks , task_desc });
82+ self .log .sayFmt ("[Cycle {d}] Task {d}/{d}: {s}" , .{ cycle , task_num , total_tasks , task_desc });
7683
7784 const prompt = try plan .generateExecutionPrompt (task_desc , self .cfg .user_hint , self .allocator );
7885 defer self .allocator .free (prompt );
@@ -210,14 +217,90 @@ pub const Executor = struct {
210217 return try self .runWithRetry (self .cfg .planning_model , title , prompt );
211218 }
212219
213- /// Kill current child process if running
214- pub fn killCurrentChild (self : * Executor ) void {
220+ /// Check if a child process is still running
221+ pub fn isChildRunning (self : * Executor ) bool {
215222 if (self .current_child_pid ) | pid | {
216- self .log .logFmt ("Terminating child process (PID: {d})" , .{pid });
217- std .posix .kill (pid , std .posix .SIG .TERM ) catch | err | {
218- self .log .logErrorFmt ("Failed to kill child process: {s}" , .{@errorName (err )});
223+ return posix .kill (pid , 0 ) == null ;
224+ }
225+ return false ;
226+ }
227+
228+ /// Kill current child process gracefully (SIGTERM, then SIGKILL if needed)
229+ pub fn killCurrentChild (self : * Executor ) void {
230+ if (self .current_child_pid == null ) return ;
231+
232+ // Try graceful shutdown first
233+ const gracefully_terminated = self .terminateChildGracefully ();
234+
235+ if (! gracefully_terminated ) {
236+ self .log .logError ("Graceful termination timed out, forcing kill..." );
237+ self .killCurrentChildForce ();
238+ }
239+
240+ self .current_child_pid = null ;
241+ self .current_child_pgid = null ;
242+ }
243+
244+ /// Attempt to gracefully terminate the child process with timeout
245+ /// Returns true if process terminated gracefully, false if force kill needed
246+ fn terminateChildGracefully (self : * Executor ) bool {
247+ const pid = self .current_child_pid orelse return true ;
248+ const pgid = self .current_child_pgid ;
249+
250+ // Send SIGTERM to the entire process group
251+ if (pgid ) | group | {
252+ posix .kill (- group , posix .SIG .TERM ) catch {};
253+ } else {
254+ posix .kill (pid , posix .SIG .TERM ) catch {};
255+ }
256+
257+ // Wait for process to terminate with timeout
258+ const timeout_ns = GRACEFUL_SHUTDOWN_TIMEOUT_SECS * std .time .ns_per_s ;
259+ const start = std .time .nanoTimestamp ();
260+
261+ while (std .time .nanoTimestamp () - start < timeout_ns ) {
262+ // Check if process is still running
263+ if (posix .kill (pid , 0 ) == error .ProcessNotFound ) {
264+ self .log .logFmt ("Child process (PID: {d}) terminated gracefully" , .{pid });
265+ return true ;
266+ }
267+ // Sleep for a short interval before checking again
268+ std .Thread .sleep (50 * std .time .ns_per_ms );
269+ }
270+
271+ self .log .logErrorFmt ("Child process (PID: {d}) did not terminate within {d}s, force killing..." , .{
272+ pid ,
273+ GRACEFUL_SHUTDOWN_TIMEOUT_SECS ,
274+ });
275+ return false ;
276+ }
277+
278+ /// Force kill the current child process and its entire process group
279+ fn killCurrentChildForce (self : * Executor ) void {
280+ const pid = self .current_child_pid orelse return ;
281+ const pgid = self .current_child_pgid ;
282+
283+ // Send SIGKILL to the entire process group
284+ if (pgid ) | group | {
285+ posix .kill (- group , posix .SIG .KILL ) catch | err | {
286+ self .log .logErrorFmt ("Failed to kill process group {d}: {s}" , .{ group , @errorName (err ) });
287+ };
288+ } else {
289+ posix .kill (pid , posix .SIG .KILL ) catch | err | {
290+ self .log .logErrorFmt ("Failed to kill process {d}: {s}" , .{ pid , @errorName (err ) });
219291 };
220- self .current_child_pid = null ;
292+ }
293+
294+ // Wait for the process to be reaped
295+ _ = posix .waitpid (pid , 0 );
296+
297+ self .log .logFmt ("Force killed child process (PID: {d})" , .{pid });
298+ }
299+
300+ /// Kill all child processes (for emergency shutdown)
301+ pub fn killAllChildren (self : * Executor ) void {
302+ if (self .current_child_pid != null ) {
303+ self .killCurrentChild ();
221304 }
222305 }
223306
@@ -285,11 +368,18 @@ pub const Executor = struct {
285368 child .stderr_behavior = .Inherit ;
286369 child .stdout_behavior = .Pipe ;
287370
371+ // Create a new process group for the child so we can kill all descendants
372+ child .pgid = 0 ; // 0 means child creates its own process group
373+
288374 try child .spawn ();
289375
290- // Store PID for potential termination
376+ // Store PID and PGID for potential termination
291377 self .current_child_pid = child .id ;
292- defer self .current_child_pid = null ;
378+ self .current_child_pgid = child .id ; // Child is leader of its own process group
379+ defer {
380+ self .current_child_pid = null ;
381+ self .current_child_pgid = null ;
382+ }
293383
294384 // Read stdout
295385 var stdout_list = std .ArrayListUnmanaged (u8 ){};
0 commit comments