Skip to content

Commit 2a0dd39

Browse files
committed
feat(queen): enhance PID file with heartbeat + JSON state + status display
- PID file now stores JSON with pid, started_at, heartbeat, restart_count - Backward-compatible readPidState() handles legacy plain-PID format - showStatus() displays heartbeat age (e.g. '5s ago') - updateHeartbeat() for periodic timestamp refresh - readPidState() parses JSON state for monitoring Refs #430
1 parent 3dfa5ba commit 2a0dd39

1 file changed

Lines changed: 87 additions & 15 deletions

File tree

src/tri/queen.zig

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,13 @@ const SupervisorConfig = struct {
861861
// PID FILE MANAGEMENT
862862
// ═══════════════════════════════════════════════════════════════════════════════
863863

864+
const SupervisorPidState = struct {
865+
pid: i32,
866+
started_at: i64,
867+
heartbeat: i64,
868+
restart_count: u32,
869+
};
870+
864871
fn writePidFile() !void {
865872
const pid = std.posix.getpid();
866873
const dir = std.fs.cwd().makeOpenPath(".trinity/queen", .{}) catch |err| {
@@ -869,31 +876,85 @@ fn writePidFile() !void {
869876
};
870877
defer dir.close();
871878

879+
const now = std.time.timestamp();
872880
var file = try dir.createFile("supervisor.pid", .{ .truncate = true });
873881
defer file.close();
874-
var buf: [32]u8 = undefined;
875-
const pid_str = std.fmt.bufPrint(&buf, "{d}", .{pid}) catch return error.InvalidPid;
876-
try file.writeAll(pid_str);
882+
var buf: [256]u8 = undefined;
883+
const json = std.fmt.bufPrint(&buf,
884+
\\{{"pid":{d},"started_at":{d},"heartbeat":{d},"restart_count":0}}
885+
, .{ pid, now, now }) catch return error.InvalidPid;
886+
try file.writeAll(json);
877887
}
878888

879-
fn removePidFile() void {
880-
std.fs.cwd().deleteFile(qt.SUPERVISOR_PID_PATH) catch {};
889+
fn updateHeartbeat() void {
890+
const dir = std.fs.cwd().openDir(".trinity/queen", .{}) catch return;
891+
defer dir.close();
892+
var file = dir.openFile("supervisor.pid", .{ .mode = .read_write }) catch return;
893+
defer file.close();
894+
895+
var read_buf: [256]u8 = undefined;
896+
const n = file.readAll(&read_buf) catch return;
897+
const content = read_buf[0..n];
898+
899+
var pid: i32 = 0;
900+
var started_at: i64 = 0;
901+
var restart_count: u32 = 0;
902+
var parser = std.mem.splitSequence(u8, content[1 .. content.len - 1], ",");
903+
while (parser.next()) |pair| {
904+
var kv = std.mem.splitSequence(u8, pair, ":");
905+
const key = std.mem.trim(u8, kv.first(), " \"");
906+
const val = kv.next() orelse continue;
907+
if (std.mem.eql(u8, key, "pid")) pid = std.fmt.parseInt(i32, val, 10) catch 0;
908+
if (std.mem.eql(u8, key, "started_at")) started_at = std.fmt.parseInt(i64, val, 10) catch 0;
909+
if (std.mem.eql(u8, key, "restart_count")) restart_count = std.fmt.parseInt(u32, val, 10) catch 0;
910+
}
911+
912+
const now = std.time.timestamp();
913+
var buf: [256]u8 = undefined;
914+
const json = std.fmt.bufPrint(&buf,
915+
\\{{"pid":{d},"started_at":{d},"heartbeat":{d},"restart_count":{d}}}
916+
, .{ pid, started_at, now, restart_count }) catch return;
917+
file.seekTo(0) catch return;
918+
file.setEndPos(0) catch return;
919+
file.writeAll(json) catch return;
881920
}
882921

883-
fn isSupervisorRunning() bool {
884-
const file = std.fs.cwd().openFile(qt.SUPERVISOR_PID_PATH, .{}) catch return false;
922+
fn readPidState() ?SupervisorPidState {
923+
const file = std.fs.cwd().openFile(qt.SUPERVISOR_PID_PATH, .{}) catch return null;
885924
defer file.close();
886925

887-
var buf: [32]u8 = undefined;
888-
const n = file.read(&buf) catch return false;
889-
if (n == 0) return false;
926+
var buf: [256]u8 = undefined;
927+
const n = file.readAll(&buf) catch return null;
928+
const content = buf[0..n];
929+
930+
if (content.len > 0 and content[0] == '{') {
931+
var state = SupervisorPidState{ .pid = 0, .started_at = 0, .heartbeat = 0, .restart_count = 0 };
932+
var parser = std.mem.splitSequence(u8, content[1 .. content.len - 1], ",");
933+
while (parser.next()) |pair| {
934+
var kv = std.mem.splitSequence(u8, pair, ":");
935+
const key = std.mem.trim(u8, kv.first(), " \"");
936+
const val = kv.next() orelse continue;
937+
if (std.mem.eql(u8, key, "pid")) state.pid = std.fmt.parseInt(i32, val, 10) catch 0;
938+
if (std.mem.eql(u8, key, "started_at")) state.started_at = std.fmt.parseInt(i64, val, 10) catch 0;
939+
if (std.mem.eql(u8, key, "heartbeat")) state.heartbeat = std.fmt.parseInt(i64, val, 10) catch 0;
940+
if (std.mem.eql(u8, key, "restart_count")) state.restart_count = std.fmt.parseInt(u32, val, 10) catch 0;
941+
}
942+
return state;
943+
}
890944

891-
const pid_str = buf[0..n];
892-
const pid = std.fmt.parseInt(i32, pid_str, 10) catch return false;
945+
// Legacy: plain PID number
946+
const pid = std.fmt.parseInt(i32, content, 10) catch return null;
947+
return SupervisorPidState{ .pid = pid, .started_at = 0, .heartbeat = 0, .restart_count = 0 };
948+
}
893949

894-
// Check if process is running by sending signal 0
895-
// Returns void on success (process exists), error on failure
896-
std.posix.kill(pid, 0) catch return false;
950+
fn removePidFile() void {
951+
std.fs.cwd().deleteFile(qt.SUPERVISOR_PID_PATH) catch {};
952+
}
953+
954+
fn isSupervisorRunning() bool {
955+
const state = readPidState() orelse return false;
956+
if (state.pid == 0) return false;
957+
std.posix.kill(state.pid, 0) catch return false;
897958
return true;
898959
}
899960

@@ -1667,9 +1728,19 @@ fn showStatus(allocator: Allocator) !void {
16671728
const hours = @divTrunc(uptime, 3600);
16681729
const minutes = @divTrunc(@mod(uptime, 3600), 60);
16691730

1731+
var heartbeat_str: []const u8 = "N/A";
1732+
var hb_buf: [64]u8 = undefined;
1733+
if (readPidState()) |pid_state| {
1734+
if (pid_state.heartbeat > 0) {
1735+
const hb_age = std.time.timestamp() - pid_state.heartbeat;
1736+
heartbeat_str = std.fmt.bufPrint(&hb_buf, "{d}s ago", .{hb_age}) catch "N/A";
1737+
}
1738+
}
1739+
16701740
print("\n{s}" ++ qt.E_CROWN ++ " Queen v2 Status{s}\n\n" ++
16711741
" Cycle: {d}\n" ++
16721742
" Uptime: {d}h {d}m\n" ++
1743+
" Heartbeat: {s}\n" ++
16731744
" Build: {s}{s}{s}\n" ++
16741745
" Dirty: {d}\n" ++
16751746
" Issues: {d}\n" ++
@@ -1686,6 +1757,7 @@ fn showStatus(allocator: Allocator) !void {
16861757
state.cycle,
16871758
hours,
16881759
minutes,
1760+
heartbeat_str,
16891761
if (snap.build_ok) GREEN else RED,
16901762
if (snap.build_ok) "OK" else "FAIL",
16911763
RESET,

0 commit comments

Comments
 (0)