fix(update): the monitor is this workspace's exact command line; create/resume take the pane workspace's admission; refusal snapshots compare the registry by rows (Issue #72, review round 3)
- daemon.find_running matches `<bash> <reconcile.sh> --subscribe[ ...]` exactly instead of an unanchored, unescaped pgrep regex: another workspace's daemon (ws.a vs wsXa) or a process naming the path in an argument is no longer signalled by the update. update.sh re-checks which PIDs are still the monitor before SIGKILL. - create and resume (spawn path) take the update admission of every installed workspace at or above the pane cwd, not only the control root's (lib.sh mam_update_admit_at, lib_py.update target-workspaces --cwd); the ~/.local/bin wrapper child drops all of them. - tests: snapshot() skips the SQLite -wal/-shm and compares the DB by its rows (macOS keeps the sidecars and a read rewrites -shm); regression tests for the monitor match and for create/resume in another installed workspace (marker and lock wait); evidence line refs shifted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
454b77215f
commit
cf1b693e85
9 files changed
+209
-32
No files matched your search
+32
-1
@@ -3266,7 +3266,8 @@ mam_control_plane_prefix() {
|
||||
#
|
||||
# mam_update_admit [workspace [fd]]: rc 0 admitted (fd 7, or the given one,
|
||||
# held unless the workspace has no .mam), 1 refused (stderr). Delegate submit
|
||||
# also takes the workspaces its target is in on fds 20 and up.
|
||||
# also takes the workspaces its target is in on fds 20 and up, create and
|
||||
# resume those their pane's cwd is in (mam_update_admit_at).
|
||||
mam_update_admit() {
|
||||
local root="${1:-$WORKSPACE_ROOT}" fd="${2:-7}"
|
||||
local lock="$root/.mam/update-admission.lock" marker="$root/.mam/update-in-progress" st
|
||||
@@ -3305,3 +3306,33 @@ mam_update_admit() {
|
||||
mam_update_admit_release() {
|
||||
eval "exec ${1:-7}>&-"
|
||||
}
|
||||
|
||||
# mam_update_admit_at <cwd>: what create and resume take for an agent they
|
||||
# start in <cwd> - mam_update_admit for the control root (fd 7) and for every
|
||||
# other installed workspace at or above <cwd> (fds 20 and up), whose update
|
||||
# counts that agent (decision 1). rc 1 refused, nothing held.
|
||||
# mam_update_admit_release_all drops them all; a long-lived child started
|
||||
# while they are held runs it first.
|
||||
MAM_UPDATE_ADMIT_FDS=""
|
||||
mam_update_admit_at() {
|
||||
local roots root fd=20
|
||||
roots="$(PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" "$(_delegate_py_bin)" -m lib_py.update target-workspaces \
|
||||
--workspace "$WORKSPACE_ROOT" --cwd "$1")" \
|
||||
|| { echo "ERROR: cannot resolve the installed workspaces at or above $1" >&2; return 1; }
|
||||
mam_update_admit || return 1
|
||||
while IFS= read -r root; do
|
||||
[ -n "$root" ] || continue
|
||||
if ! mam_update_admit "$root" "$fd"; then
|
||||
mam_update_admit_release_all
|
||||
return 1
|
||||
fi
|
||||
MAM_UPDATE_ADMIT_FDS="$MAM_UPDATE_ADMIT_FDS $fd"; fd=$((fd + 1))
|
||||
done <<< "$roots"
|
||||
}
|
||||
|
||||
mam_update_admit_release_all() {
|
||||
local fd
|
||||
mam_update_admit_release
|
||||
for fd in $MAM_UPDATE_ADMIT_FDS; do mam_update_admit_release "$fd"; done
|
||||
MAM_UPDATE_ADMIT_FDS=""
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Behaviour is byte-for-byte compatible with the shell original (issue #5 phase 1,
|
||||
D5): same monitor path, same stderr wording and exit status when it is missing,
|
||||
same `pgrep -f` pre-check pattern, same log file, same "print the PID" contract.
|
||||
the same daemons found by the pre-check (matched exactly since Issue #72, see
|
||||
find_running), same log file, same "print the PID" contract.
|
||||
Callers capture stdout with $( ), so nothing else may be printed there.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -39,20 +40,34 @@ def monitor_script_path(workdir: str) -> str:
|
||||
def find_running(monitor_script: str) -> List[str]:
|
||||
"""PIDs of subscribers already running for this monitor script.
|
||||
|
||||
Uses the exact `pgrep -f` pattern the shell helper used, so a daemon started
|
||||
by either implementation is found by the other. Multiple matches are returned
|
||||
in full: the legacy helper echoed every line and callers only ever echo it
|
||||
back, so trimming here would be a silent behaviour change.
|
||||
A subscriber is a process whose command line is `<bash> <monitor_script>
|
||||
--subscribe[ ...]`, argv0 a bash, as both this module and the shell helper
|
||||
start it. Issue #72: matched exactly, not as the shell helper's `pgrep -f`
|
||||
regex, because the update skill signals what this returns - a daemon of
|
||||
another workspace the unescaped path also matched, or a process that only
|
||||
names the path in an argument, is not this workspace's monitor. Multiple
|
||||
matches are returned in full: the legacy helper echoed every line and
|
||||
callers only ever echo it back, so trimming here would be a silent
|
||||
behaviour change.
|
||||
"""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["pgrep", "-f", "bash {} --subscribe".format(monitor_script)],
|
||||
["ps", "-A", "-ww", "-o", "pid=", "-o", "args="],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
except OSError as exc: # pragma: no cover - pgrep missing
|
||||
logger.warning("pgrep unavailable: %s", exc)
|
||||
except OSError as exc: # pragma: no cover - ps missing
|
||||
logger.warning("ps unavailable: %s", exc)
|
||||
return []
|
||||
return [line for line in res.stdout.split() if line.strip()]
|
||||
want = "{} --subscribe".format(monitor_script)
|
||||
pids = []
|
||||
for line in res.stdout.splitlines():
|
||||
parts = line.strip().split(" ", 1)
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
argv0, _, rest = parts[1].partition(" ")
|
||||
if os.path.basename(argv0) == "bash" and (rest == want or rest.startswith(want + " ")):
|
||||
pids.append(parts[0])
|
||||
return pids
|
||||
|
||||
|
||||
def _ignore_sighup(): # pragma: no cover - runs in the forked child
|
||||
|
||||
@@ -23,6 +23,9 @@ this module what needs parsing. Standard library only, Python 3.9.
|
||||
the live agent's on server V) and its row's control_plane
|
||||
workspace_root. Nothing for a non-herdr target. rc 1: herdr cannot
|
||||
answer for a rowless target.
|
||||
python -m lib_py.update target-workspaces --workspace W --cwd C
|
||||
The same for an agent create or resume starts in C: every installed
|
||||
workspace at or above C other than W.
|
||||
python -m lib_py.update backup-registry --workspace W --dest D
|
||||
Copies the registry into D under the registry's own write lock
|
||||
(agent-sessions.db.lock, as atomic_yaml takes it): the DB through the
|
||||
@@ -296,6 +299,12 @@ def target_workspaces(workspace: str, state: dict, agent_session: str, server: s
|
||||
if a.get("name") in (name, sanitize_herdr_agent_name(name)):
|
||||
cwds += [a.get("cwd") or "", a.get("foreground_cwd") or ""]
|
||||
break
|
||||
return _others(workspace, out + installed_above(cwds))
|
||||
|
||||
|
||||
def installed_above(cwds: Iterable[str]) -> List[str]:
|
||||
"""Every installed workspace at or above each cwd."""
|
||||
out = []
|
||||
for cwd in cwds:
|
||||
d = _real(cwd) if cwd else ""
|
||||
while d:
|
||||
@@ -304,8 +313,12 @@ def target_workspaces(workspace: str, state: dict, agent_session: str, server: s
|
||||
if os.path.dirname(d) == d:
|
||||
break
|
||||
d = os.path.dirname(d)
|
||||
return out
|
||||
|
||||
|
||||
def _others(workspace: str, dirs: List[str]) -> List[str]:
|
||||
me = _real(workspace)
|
||||
return [d for i, d in enumerate(out) if d != me and d not in out[:i]]
|
||||
return [d for i, d in enumerate(dirs) if d != me and d not in dirs[:i]]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- monitor
|
||||
@@ -367,7 +380,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
s.add_argument("--workspace", required=True)
|
||||
s = sub.add_parser("target-workspaces")
|
||||
s.add_argument("--workspace", required=True)
|
||||
s.add_argument("--agent-session", required=True)
|
||||
s.add_argument("--agent-session", default="")
|
||||
s.add_argument("--cwd", default="")
|
||||
s.add_argument("--server", default="default")
|
||||
s.add_argument("--herdr", default="")
|
||||
s = sub.add_parser("backup-registry")
|
||||
@@ -385,6 +399,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
for line in lines:
|
||||
print(line)
|
||||
return 1 if lines else 0
|
||||
if a.cmd == "target-workspaces" and a.cwd:
|
||||
for root in _others(ws, installed_above([a.cwd])):
|
||||
print(root)
|
||||
return 0
|
||||
if a.cmd == "target-workspaces":
|
||||
try:
|
||||
state = _state()
|
||||
|
||||
@@ -260,7 +260,7 @@ spawn() {
|
||||
SESSION_UUID=""
|
||||
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "" "true" 2>/dev/null || echo "${RESOLVED_BIN} --dangerously-skip-permissions")"
|
||||
[ -z "$CP_PREFIX" ] || echo "WARNING: the ~/.local/bin wrapper path bypasses herdr; the control-plane binding is recorded but not forwarded" >&2
|
||||
nohup "$WRAPPER" >/dev/null 2>&1 7>&- &
|
||||
( mam_update_admit_release_all; exec nohup "$WRAPPER" >/dev/null 2>&1 ) &
|
||||
disown
|
||||
else
|
||||
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
|
||||
@@ -285,9 +285,10 @@ if [ "$DRY_RUN" = "1" ]; then
|
||||
fi
|
||||
# Issue #63: a same-cwd session of this post-discovered agent records its id first (lib.sh mam_id_order_wait).
|
||||
mam_id_order_wait "$WORKSPACE" "$AGENT" "$SESSION_NAME" || exit 1
|
||||
# Issue #72: no new session while an update runs; held until the row is written
|
||||
# (lib.sh mam_update_admit).
|
||||
mam_update_admit || exit 1
|
||||
# Issue #72: no new session while an update of the control root or of an
|
||||
# installed workspace the pane's cwd is in runs; held until the row is written
|
||||
# (lib.sh mam_update_admit_at).
|
||||
mam_update_admit_at "$WORKSPACE" || exit 1
|
||||
HERDR_EPOCH=$(date +%s)
|
||||
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
spawn
|
||||
@@ -641,7 +642,7 @@ snap['cwd'] = os.environ['PANE_CWD']
|
||||
print(f"appended: {name}", flush=True)
|
||||
PYEOF
|
||||
ROW_APPENDED=1
|
||||
mam_update_admit_release
|
||||
mam_update_admit_release_all
|
||||
|
||||
echo
|
||||
echo "=== created ==="
|
||||
|
||||
@@ -254,9 +254,10 @@ if [ "${DRY_RUN:-0}" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Issue #72: no new session while an update runs; held until the row is written
|
||||
# (lib.sh mam_update_admit).
|
||||
mam_update_admit || exit 1
|
||||
# Issue #72: no new session while an update of the control root or of an
|
||||
# installed workspace the pane's cwd is in runs; held until the row is written
|
||||
# (lib.sh mam_update_admit_at).
|
||||
mam_update_admit_at "$WORKSPACE" || exit 1
|
||||
|
||||
# Issue #63: wait if another same-cwd session of this post-discovered agent
|
||||
# is still being created or has not recorded its id yet (lib.sh mam_id_order_wait).
|
||||
@@ -294,7 +295,7 @@ CONTROL_PLANE_JSON="$CP_JSON" bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_r
|
||||
--herdr-session "$HERDR_SESSION_NAME" --cmd-full "$CMD_FULL" \
|
||||
${DROP_FLAG:+$DROP_FLAG} \
|
||||
${HERDR_WORKSPACE_OPT:+--herdr-workspace "$HERDR_WORKSPACE_OPT"}
|
||||
mam_update_admit_release
|
||||
mam_update_admit_release_all
|
||||
_ensure_monitor_daemon
|
||||
|
||||
echo "Successfully resumed $SESSION_NAME ($AGENT)"
|
||||
@@ -185,6 +185,14 @@ if [ -n "$MON_PIDS" ]; then
|
||||
echo "monitor: stopping the daemon (PID $MON_PIDS)"
|
||||
kill -TERM "$@" 2>/dev/null || true
|
||||
if ! _wait_monitor_gone "$(mam_int_knob MAM_UPDATE_MONITOR_STOP_S 30 1)" "$@"; then
|
||||
# SIGKILL only what is still this workspace's monitor: a PID that ended
|
||||
# during the wait may already be another process's.
|
||||
still=""
|
||||
for p in $(_monitor_json | _json_field pids); do
|
||||
case " $* " in *" $p "*) still="$still $p" ;; esac
|
||||
done
|
||||
# shellcheck disable=SC2086
|
||||
set -- $still
|
||||
for p in "$@"; do kids="$kids $(pgrep -P "$p" 2>/dev/null | tr '\n' ' ' || true)"; done
|
||||
echo "WARNING: the monitor did not stop within MAM_UPDATE_MONITOR_STOP_S; sending SIGKILL to $* $kids" >&2
|
||||
# shellcheck disable=SC2086
|
||||
|
||||
Reference in new issue
Block a user