update skill, in-place install and update admission gate; resume starts the monitor (Issue #72) #75

Merged
Godopu merged 16 commits from fix/issue-72 into main 2026-10-27 22:50:05 +00:00
2 changed files with 39 additions and 4 deletions
Showing only changes of commit ec7f5c4e57 - Show all commits

No files matched your search

+7 -1
View File
@@ -2297,10 +2297,16 @@ _start_watchdog_legacy() {
elif [ -z "$pid" ]; then
# Start the wildcard monitor subscriber daemon with --idle-timeout 0 (never idle out)
# and ensure it runs with $workdir as cwd to anchor relative log paths.
local orig_pwd="$PWD"
# Issue #72 (decision 2): job control (`set -m`) starts it in its own
# process group, out of the caller's, as the python body's new session
# does; macOS has no setsid(1). The caller's option is restored.
local orig_pwd="$PWD" had_m=0
case $- in *m*) had_m=1 ;; esac
cd "$workdir" || return 1
set -m
nohup bash "$monitor_script" --subscribe --idle-timeout 0 </dev/null >> "$log_file" 2>&1 &
pid=$!
[ "$had_m" = 1 ] || set +m
cd "$orig_pwd" || return 1
fi
+32 -3
View File
@@ -181,6 +181,18 @@ def _exact_monitor_pids(script) -> list:
return pids
def _descendants(pid) -> list:
kids = subprocess.run(["pgrep", "-P", str(pid)], capture_output=True, text=True).stdout.split()
return [d for k in kids for d in [k] + _descendants(k)]
def _processes_naming(path) -> list:
"""Live processes whose command line names PATH (read only, to report leftovers)."""
out = subprocess.run(["ps", "-A", "-ww", "-o", "pid=", "-o", "args="], capture_output=True, text=True).stdout
return [line.strip() for line in out.splitlines()
if str(path) in line and line.split(None, 1)[0] != str(os.getpid())]
def _lock_held(path: Path) -> bool:
if not path.exists():
return False
@@ -206,12 +218,26 @@ def box(template, tmp_path):
try:
yield b
finally:
# Only what this test started: its helpers' process groups, and the
# monitors whose exact command line is this test's own workspace
# (detached, so in a group of their own), with their children. No
# pattern kill: the host runs other workspaces' monitors.
for p in b.helpers:
try:
os.killpg(p.pid, signal.SIGKILL)
except OSError:
pass
subprocess.run(["pkill", "-KILL", "-f", str(ws)], capture_output=True)
p.wait()
for pid in _exact_monitor_pids(ws / MONITOR_REL):
for q in [pid] + _descendants(pid):
try:
os.kill(int(q), signal.SIGKILL)
except OSError:
pass
try:
_until(lambda: not _processes_naming(ws), timeout=10)
except AssertionError:
raise AssertionError("processes of this test outlived it: %s" % _processes_naming(ws))
# --- helpers ---------------------------------------------------------------------
@@ -673,8 +699,9 @@ def test_only_this_workspaces_exact_monitor_command_line_is_its_monitor(tmp_path
def test_every_restart_starts_a_real_monitor_in_both_delegation_modes(box, mode, case):
"""MAM_DELEGATION_PY=0 keeps the legacy start_watchdog, the switch the
delegate skill documents. Its restart, after an install, after a stop
timeout or after a rollback, must start this workspace's monitor again and
not report a process that only names the monitor path in an argument."""
timeout or after a rollback, must start this workspace's monitor again, out
of the caller's process group (decision 2), and not report a process that
only names the monitor path in an argument."""
env = {"MAM_DELEGATION_PY": mode}
box.stub_monitor()
box.start_monitor(env=dict(env, STUB_MONITOR_IGNORE_TERM="1") if case == "killed" else env)
@@ -693,6 +720,8 @@ def test_every_restart_starts_a_real_monitor_in_both_delegation_modes(box, mode,
assert "PID %s)" % decoy.pid not in res.stdout, "the decoy is reported as the monitor"
_until(lambda: bool(box.monitor_pids()))
assert not (set(box.monitor_pids()) & first) and box.monitor_starts() == 2
# box.update runs update.sh in this test's process group: the caller's.
assert all(os.getpgid(int(p)) != os.getpgrp() for p in box.monitor_pids()), "left in the caller's group"
assert decoy.poll() is None, "the decoy was not signalled"