Skip to content

Commit 59234bc

Browse files
committed
fix: add newline after task status output
The task status line was being output without a trailing newline using statusFmt, which caused subsequent opencode CLI output (like '| Todo') to appear on the same line. Changed to use sayFmt which adds a newline, ensuring opencode output starts on a fresh line. Signed-off-by: leocavalcante <[email protected]>
1 parent 81982b9 commit 59234bc

File tree

1 file changed

+99
-9
lines changed

1 file changed

+99
-9
lines changed

src/executor.zig

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! task execution, and evaluation phases.
55

66
const std = @import("std");
7+
const posix = std.posix;
78
const Allocator = std.mem.Allocator;
89

910
const 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
2630
pub 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

Comments
 (0)