update skill, in-place install and update admission gate; resume starts the monitor (Issue #72) #75
No files matched your search
@@ -2297,10 +2297,16 @@ _start_watchdog_legacy() {
|
|||||||
elif [ -z "$pid" ]; then
|
elif [ -z "$pid" ]; then
|
||||||
# Start the wildcard monitor subscriber daemon with --idle-timeout 0 (never idle out)
|
# 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.
|
# 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
|
cd "$workdir" || return 1
|
||||||
|
set -m
|
||||||
nohup bash "$monitor_script" --subscribe --idle-timeout 0 </dev/null >> "$log_file" 2>&1 &
|
nohup bash "$monitor_script" --subscribe --idle-timeout 0 </dev/null >> "$log_file" 2>&1 &
|
||||||
pid=$!
|
pid=$!
|
||||||
|
[ "$had_m" = 1 ] || set +m
|
||||||
cd "$orig_pwd" || return 1
|
cd "$orig_pwd" || return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,18 @@ def _exact_monitor_pids(script) -> list:
|
|||||||
return pids
|
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:
|
def _lock_held(path: Path) -> bool:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return False
|
return False
|
||||||
@@ -206,12 +218,26 @@ def box(template, tmp_path):
|
|||||||
try:
|
try:
|
||||||
yield b
|
yield b
|
||||||
finally:
|
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:
|
for p in b.helpers:
|
||||||
try:
|
try:
|
||||||
os.killpg(p.pid, signal.SIGKILL)
|
os.killpg(p.pid, signal.SIGKILL)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
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 ---------------------------------------------------------------------
|
# --- 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):
|
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
|
"""MAM_DELEGATION_PY=0 keeps the legacy start_watchdog, the switch the
|
||||||
delegate skill documents. Its restart, after an install, after a stop
|
delegate skill documents. Its restart, after an install, after a stop
|
||||||
timeout or after a rollback, must start this workspace's monitor again and
|
timeout or after a rollback, must start this workspace's monitor again, out
|
||||||
not report a process that only names the monitor path in an argument."""
|
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}
|
env = {"MAM_DELEGATION_PY": mode}
|
||||||
box.stub_monitor()
|
box.stub_monitor()
|
||||||
box.start_monitor(env=dict(env, STUB_MONITOR_IGNORE_TERM="1") if case == "killed" else env)
|
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"
|
assert "PID %s)" % decoy.pid not in res.stdout, "the decoy is reported as the monitor"
|
||||||
_until(lambda: bool(box.monitor_pids()))
|
_until(lambda: bool(box.monitor_pids()))
|
||||||
assert not (set(box.monitor_pids()) & first) and box.monitor_starts() == 2
|
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"
|
assert decoy.poll() is None, "the decoy was not signalled"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user