Skip to content

Commit 147250d

Browse files
Peter MarreckPeter Marreck
authored andcommitted
feat: add watch list and watch prune commands for managing watchers
`watch list` discovers all running codescan watchers system-wide via /bin/ps, cross-references with lsof to identify which have active terminal sessions, and displays a status table. `watch prune` shows orphaned watchers (dry-run by default), and with `--confirm` stops them via SIGTERM.
1 parent c4316f5 commit 147250d

4 files changed

Lines changed: 390 additions & 3 deletions

File tree

build.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,18 @@ pub fn build(b: *std.Build) void {
412412
linkCommon(watcher_tests, sqlite3_lib, vec_static_lib, pcre2_lib, ts_lib, &ts_langs);
413413
test_step.dependOn(&b.addRunArtifact(watcher_tests).step);
414414

415+
416+
const watcher_mgmt_tests = b.addTest(.{
417+
.root_module = b.createModule(.{
418+
.root_source_file = b.path("src/watcher_mgmt.zig"),
419+
.target = target,
420+
.optimize = optimize,
421+
}),
422+
});
423+
addTreeSitterIncludes(b, watcher_mgmt_tests.root_module);
424+
addPcre2Includes(watcher_mgmt_tests.root_module, pcre2_lib);
425+
linkCommon(watcher_mgmt_tests, sqlite3_lib, vec_static_lib, pcre2_lib, ts_lib, &ts_langs);
426+
test_step.dependOn(&b.addRunArtifact(watcher_mgmt_tests).step);
415427
const scan_tests = b.addTest(.{
416428
.root_module = b.createModule(.{
417429
.root_source_file = b.path("src/scan.zig"),

src/cli.zig

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ pub const WatchAction = enum {
4848
restart,
4949
status,
5050
pid,
51+
list, // list all running watchers system-wide
52+
prune, // stop orphaned watchers
5153
};
52-
5354
pub const Seen = struct {
5455
output: bool = false,
5556
show_comments: bool = false,
@@ -375,8 +376,13 @@ pub fn parse(allocator: std.mem.Allocator, args: []const []const u8) !Parsed {
375376
} else if (std.mem.eql(u8, sub, "pid")) {
376377
parsed.watch_action = .pid;
377378
i += 1;
378-
}
379-
}
379+
} else if (std.mem.eql(u8, sub, "list")) {
380+
parsed.watch_action = .list;
381+
i += 1;
382+
} else if (std.mem.eql(u8, sub, "prune")) {
383+
parsed.watch_action = .prune;
384+
i += 1;
385+
} }
380386
} else if (std.mem.eql(u8, cmd, "status")) {
381387
parsed.command = .status;
382388
help_topic_default = "status";

src/main.zig

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const lsp = @import("lsp.zig");
2121
const pcre2 = @import("pcre2.zig");
2222
const watcher = @import("watcher.zig");
2323
const pidfile = @import("pidfile.zig");
24+
const watcher_mgmt = @import("watcher_mgmt.zig");
2425
const progress_mod = @import("progress.zig");
2526
const fs_watch = @import("fs_watch.zig");
2627
const weights = @import("weights.zig");
@@ -960,6 +961,110 @@ pub fn main() !void {
960961
}
961962
try stdout.flush();
962963
},
964+
.list => {
965+
var watchers = watcher_mgmt.discoverWatchers(allocator) catch |err| {
966+
try stdout.print("error: failed to discover watchers: {}\n", .{err});
967+
try stdout.flush();
968+
std.process.exit(1);
969+
};
970+
defer {
971+
for (watchers.items) |*w| w.deinit(allocator);
972+
watchers.deinit(allocator);
973+
}
974+
975+
if (watchers.items.len == 0) {
976+
try stdout.print("No codescan watchers running.\n", .{});
977+
try stdout.flush();
978+
} else {
979+
// Get active cwds to mark orphans
980+
var cwds = watcher_mgmt.getActiveCwds(allocator) catch std.ArrayListUnmanaged(watcher_mgmt.LsofEntry){};
981+
defer {
982+
for (cwds.items) |e| e.deinit(allocator);
983+
cwds.deinit(allocator);
984+
}
985+
watcher_mgmt.markActiveWatchers(watchers.items, cwds.items);
986+
987+
try stdout.print("{s:<8}{s:<7}{s:<14}{s:<6}{s}\n", .{ "PID", "CPU%", "UPTIME", "USED", "ROOT" });
988+
for (watchers.items) |w| {
989+
try stdout.print("{:<8}{s:<7}{s:<14}{s:<6}{s}\n", .{
990+
@as(u32, @intCast(w.pid)),
991+
w.cpu_pct,
992+
w.elapsed,
993+
if (w.active) "yes" else "no",
994+
w.root,
995+
}); }
996+
var orphan_count: usize = 0;
997+
for (watchers.items) |w| {
998+
if (!w.active) orphan_count += 1;
999+
}
1000+
try stdout.print("\n{d} watcher{s}, {d} orphaned\n", .{
1001+
watchers.items.len,
1002+
if (watchers.items.len != 1) "s" else "",
1003+
orphan_count,
1004+
});
1005+
try stdout.flush();
1006+
}
1007+
},
1008+
.prune => {
1009+
var watchers = watcher_mgmt.discoverWatchers(allocator) catch |err| {
1010+
try stdout.print("error: failed to discover watchers: {}\n", .{err});
1011+
try stdout.flush();
1012+
std.process.exit(1);
1013+
};
1014+
defer {
1015+
for (watchers.items) |*w| w.deinit(allocator);
1016+
watchers.deinit(allocator);
1017+
}
1018+
1019+
var cwds = watcher_mgmt.getActiveCwds(allocator) catch std.ArrayListUnmanaged(watcher_mgmt.LsofEntry){};
1020+
defer {
1021+
for (cwds.items) |e| e.deinit(allocator);
1022+
cwds.deinit(allocator);
1023+
}
1024+
watcher_mgmt.markActiveWatchers(watchers.items, cwds.items);
1025+
1026+
var orphan_count: usize = 0;
1027+
for (watchers.items) |w| {
1028+
if (!w.active) orphan_count += 1;
1029+
}
1030+
1031+
if (orphan_count == 0) {
1032+
try stdout.print("No orphaned watchers found.\n", .{});
1033+
try stdout.flush();
1034+
} else if (!parsed.confirm) {
1035+
try stdout.print("Orphaned watchers (no active sessions):\n", .{});
1036+
for (watchers.items) |w| {
1037+
if (!w.active) {
1038+
try stdout.print(" PID {d} {s}\n", .{ w.pid, w.root });
1039+
}
1040+
}
1041+
try stdout.print("\nRun with --confirm to stop {d} orphaned watcher{s}.\n", .{
1042+
orphan_count,
1043+
if (orphan_count != 1) "s" else "",
1044+
});
1045+
try stdout.flush();
1046+
} else {
1047+
var stopped: usize = 0;
1048+
for (watchers.items) |w| {
1049+
if (!w.active) {
1050+
if (watcher_mgmt.stopWatcher(w.pid)) {
1051+
try stdout.print("Stopped watcher for {s} (PID {d})\n", .{ w.root, w.pid });
1052+
stopped += 1;
1053+
} else {
1054+
try stdout.print("Failed to stop watcher for {s} (PID {d})\n", .{ w.root, w.pid });
1055+
}
1056+
}
1057+
}
1058+
const remaining = watchers.items.len - stopped;
1059+
try stdout.print("\nStopped {d} orphaned watcher{s}. {d} active watcher{s} remain.\n", .{
1060+
stopped,
1061+
if (stopped != 1) "s" else "",
1062+
remaining,
1063+
if (remaining != 1) "s" else "",
1064+
});
1065+
try stdout.flush();
1066+
}
1067+
},
9631068
.run => {
9641069
try ensureParentDir(settings.db_path);
9651070
// Open existing DB or create new one (don't destroy existing index)

0 commit comments

Comments
 (0)