feat: multi-agent-mux-update skill and in-place install (Issue #72)
The update is a skill (decision 4); deploy/update.sh and the installed .mam_deploy/update.sh are gone. - Phase 1 (installed code, update.sh): publish the update marker under the exclusive admission lock; refuse before any change while a running row, a live herdr agent in the workspace on any running server, an open job (unreadable JSON included, dry_run not), a live loop marker or a monitor.lock held by a monitor the skills did not start (U3) is there - the calling agent (server + pane, its row and one running job) excepted (lib_py/update.py precheck, decisions 1 and 7). Stop the monitor (TERM, then KILL and restart it if it does not end; decision 2), handle a leftover backup by its state (U4), back up .agents, .mam_deploy, root files, .venv, the installer's .mam files and the registry (SQLite backup API under the registry lock) into .mam/update-backup/, then exec a copy of install_phase.sh outside the tree (decision 5). - The copy (no lib.sh/lib_py, bash 3.2) runs the installer in place; on failure it restores the backup and restarts the monitor from the restored tree; on success it runs phase 2, post_update.sh of the new tree, which converts the registry with lib_py.registry_format and restarts the monitor if it ran. The backup and the marker are removed at the end. - install.sh updates in place (decisions 3, 4, 9): install_state keys are set, one line each; no updater is installed and an old one (.mam_deploy/update.sh, or a root update.sh the manifest lists) is removed; framework files the new version no longer ships are deleted when unmodified, kept and listed when modified, and dropped from base/, the manifest and asset_hashes.txt; a local source's .mam is not staged. remove.sh knows the new skill; CI shellchecks its scripts; docs point at the skill. - test_issue8_update_state and test_deploy_layout td6/td27 run the skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6359d65999
commit
47fe24e07c
19 files changed
+1820
-314
No files matched your search
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: multi-agent-mux-update
|
||||
description: "Update an installed MAM workspace in place to the latest version. Refuses while a session, an open job, a review loop or an unknown monitor is live; stops the monitor daemon and starts it again afterwards; rolls everything back if the install fails; converts the registry with the newly installed code. Use instead of the removed .mam_deploy/update.sh."
|
||||
version: 4.11.0
|
||||
author: godopu
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
environments: [terminal, herdr]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [update, install, registry, migration]
|
||||
related_skills: [multi-agent-mux-stop, multi-agent-mux-resume, multi-agent-mux-monitor]
|
||||
---
|
||||
|
||||
# Multi-Agent Update
|
||||
|
||||
```bash
|
||||
bash .agents/skills/multi-agent-mux-update/scripts/update.sh --yes
|
||||
```
|
||||
|
||||
| Item | Behaviour |
|
||||
|---|---|
|
||||
| Where | An installed workspace only (`.mam_deploy/remove.sh` present). A git checkout updates with `git pull`. |
|
||||
| Confirmation | `[y/N]` prompt; without a terminal `-y`/`--yes` is required. There is no force option. |
|
||||
| Refused when | a running registry row, a live herdr agent whose cwd is in the workspace (any running server), an open job (unreadable job JSON included, `dry_run` excluded), a live loop marker, `.mam/monitor.lock` held by a monitor the skills did not start, or another update. The calling agent (its herdr server + pane, row and one running job) is not counted. Each refusal lists the cleanup; nothing is changed. |
|
||||
| During the update | create, resume, `run_loop.sh`, delegate submit, stop and orc-onboard refuse (`.mam/update-in-progress`, `.mam/update-admission.lock`). |
|
||||
| Monitor | stopped first, started again afterwards only if it ran (`MAM_NO_SUBSCRIBER_DAEMON=1`: not started). |
|
||||
| Install | the installer at `MAM_INSTALLER_URL` (default: Gitea `main`) runs in place; `.mam` is kept. On failure `.agents/`, `.mam_deploy/`, root files, `.venv`, the installer's `.mam` files and the registry are restored from `.mam/update-backup/`. |
|
||||
| Registry | converted by the new code (`lib_py.registry_format`); a failed conversion keeps the install and exits nonzero. |
|
||||
| Exit | `0` updated, `1` refused or failed, `2` bad arguments. |
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# install_phase.sh — multi-agent-mux-update, the part that changes the tree (Issue #72, decision 5).
|
||||
#
|
||||
# install_phase.sh run <workspace> <monitor_was_running 0|1> <tmpdir>
|
||||
# install_phase.sh restore <workspace> [--skip-registry]
|
||||
#
|
||||
# update.sh copies this file outside the workspace and execs the copy (same
|
||||
# pid and start time, so its .mam/update-in-progress marker stays live). From
|
||||
# here on the tree is being replaced, so this calls no lib.sh or lib_py: it runs
|
||||
# the installer in place, and on failure restores .mam/update-backup/ and starts
|
||||
# the monitor again with the restored (old, whole) tree's start_watchdog. On
|
||||
# success phase 2 is the NEW tree's post_update.sh, in a child process. It ends
|
||||
# by removing the backup and the marker (only its own: pid + lstart).
|
||||
# Must run on macOS bash 3.2.
|
||||
set -uo pipefail
|
||||
|
||||
MODE="${1:-}"
|
||||
WS="${2:-}"
|
||||
[ -n "$MODE" ] && [ -d "$WS" ] || { echo "usage: $0 run|restore <workspace> ..." >&2; exit 2; }
|
||||
BK="$WS/.mam/update-backup"
|
||||
|
||||
set_state() { printf '%s\n' "$1" > "$BK/state.tmp" && mv -f "$BK/state.tmp" "$BK/state"; }
|
||||
|
||||
# Puts back every backed-up item, removes the ones that did not exist before,
|
||||
# then (unless --skip-registry) the registry: the DB from the backup, its
|
||||
# -wal/-shm removed (macOS keeps them after close).
|
||||
restore() {
|
||||
local rel n
|
||||
[ -f "$BK/items" ] || { echo "ERROR: $BK/items is missing" >&2; return 1; }
|
||||
while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
rm -rf "${WS:?}/$rel" || return 1
|
||||
grep -Fqx -- "$rel" "$BK/absent" 2>/dev/null && continue
|
||||
mkdir -p "$(dirname "$WS/$rel")" && cp -pPR "$BK/files/$rel" "$WS/$rel" || return 1
|
||||
done < "$BK/items"
|
||||
[ "${1:-}" = "--skip-registry" ] && return 0
|
||||
for n in agent-sessions.yaml agent-sessions.yaml.bak agent-sessions.db; do
|
||||
rm -f "$WS/.mam/$n" || return 1
|
||||
if [ -f "$BK/registry/$n" ]; then
|
||||
cp -p "$BK/registry/$n" "$WS/.mam/$n" || return 1
|
||||
fi
|
||||
done
|
||||
rm -f "$WS/.mam/agent-sessions.db-wal" "$WS/.mam/agent-sessions.db-shm"
|
||||
}
|
||||
|
||||
release_marker() {
|
||||
local m="$WS/.mam/update-in-progress" pid lstart cur
|
||||
[ -f "$m" ] || return 0
|
||||
pid="$(grep '^pid=' "$m" | head -n 1 | cut -d= -f2-)"
|
||||
lstart="$(grep '^lstart=' "$m" | head -n 1 | cut -d= -f2-)"
|
||||
cur="$(ps -p $$ -o lstart= 2>/dev/null | tr -s ' ' | sed 's/^ *//;s/ *$//')"
|
||||
if [ "$pid" = "$$" ] && [ "$lstart" = "$cur" ]; then
|
||||
rm -f "$m"
|
||||
fi
|
||||
}
|
||||
|
||||
restart_old_monitor() {
|
||||
[ "$1" = "1" ] || return 0
|
||||
if [ "${MAM_NO_SUBSCRIBER_DAEMON:-0}" = "1" ]; then
|
||||
echo "monitor: not restarted (MAM_NO_SUBSCRIBER_DAEMON=1)"
|
||||
return 0
|
||||
fi
|
||||
local pid
|
||||
if pid="$(bash -c 'source "$1/.agents/skills/lib.sh" && start_watchdog update "$1"' _ "$WS")"; then
|
||||
echo "monitor: restarted (PID $pid)"
|
||||
else
|
||||
echo "ERROR: could not restart the monitor daemon" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
restore)
|
||||
restore "${3:-}"
|
||||
exit $?
|
||||
;;
|
||||
run) ;;
|
||||
*) echo "usage: $0 run|restore <workspace> ..." >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
MON="${3:-0}"
|
||||
TMPD="${4:-}"
|
||||
trap '[ -z "$TMPD" ] || rm -rf "$TMPD"' EXIT
|
||||
|
||||
set_state installing || { echo "ERROR: cannot write $BK/state" >&2; release_marker; exit 1; }
|
||||
URL="${MAM_INSTALLER_URL:-https://git.godopu.com/tmpl/multi-agent-mux/raw/branch/main/deploy/install.sh}"
|
||||
echo "install: $URL"
|
||||
irc=0
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$URL" | bash -s -- "$WS" || irc=$?
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- "$URL" | bash -s -- "$WS" || irc=$?
|
||||
else
|
||||
echo "ERROR: neither curl nor wget is available to fetch the installer" >&2
|
||||
irc=1
|
||||
fi
|
||||
|
||||
if [ "$irc" -ne 0 ]; then
|
||||
echo "ERROR: the install failed (rc $irc); rolling back" >&2
|
||||
if restore; then
|
||||
rm -rf "$BK"
|
||||
echo "rolled back: the workspace is at the version it had before the update"
|
||||
restart_old_monitor "$MON"
|
||||
else
|
||||
echo "ERROR: the rollback failed; the backup is kept at $BK (the next update restores it)" >&2
|
||||
fi
|
||||
release_marker
|
||||
exit 1
|
||||
fi
|
||||
set_state installed
|
||||
|
||||
POST="$WS/.agents/skills/multi-agent-mux-update/scripts/post_update.sh"
|
||||
prc=0
|
||||
if [ -f "$POST" ]; then
|
||||
bash "$POST" --monitor "$MON" || prc=$?
|
||||
else
|
||||
echo "ERROR: the installed version has no $POST; the registry converts on its next write and the monitor was not restarted" >&2
|
||||
prc=1
|
||||
fi
|
||||
rm -rf "$BK"
|
||||
release_marker
|
||||
[ "$prc" -eq 0 ] && echo "Update complete."
|
||||
exit "$prc"
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# post_update.sh — multi-agent-mux-update phase 2 (Issue #72, decision 5).
|
||||
# post_update.sh --monitor <0|1>
|
||||
#
|
||||
# Run by install_phase.sh from the NEWLY installed tree, in its own process:
|
||||
# converts the registry with the new code's entry point (lib_py.registry_format,
|
||||
# #71) and starts the monitor again if it ran before the update. Exit 0, or
|
||||
# nonzero when the conversion or the monitor restart failed (the install is
|
||||
# kept either way: the registry converts again on its next write).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WS="$(cd -P "$SCRIPT_DIR/../../../.." && pwd -P)"
|
||||
export WORKSPACE_ROOT="$WS"
|
||||
export AGENT_SESSIONS_YAML="$WS/.mam/agent-sessions.yaml"
|
||||
# shellcheck source=../../lib.sh
|
||||
source "$WS/.agents/skills/lib.sh"
|
||||
|
||||
MON=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--monitor) MON="$2"; shift 2 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
rc=0
|
||||
if [ ! -f "$AGENT_SESSIONS_YAML" ] && [ ! -f "$WS/.mam/agent-sessions.db" ]; then
|
||||
echo "registry: nothing to convert (no registry)"
|
||||
else
|
||||
"$(_delegate_py_bin)" -m lib_py.registry_format --yaml "$AGENT_SESSIONS_YAML" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "ERROR: the registry was not converted (rc $rc); the install is kept and the next registry write converts it" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MON" = "1" ]; then
|
||||
if [ "${MAM_NO_SUBSCRIBER_DAEMON:-0}" = "1" ]; then
|
||||
echo "monitor: not restarted (MAM_NO_SUBSCRIBER_DAEMON=1)"
|
||||
elif pid="$(start_watchdog update "$WS")"; then
|
||||
echo "monitor: restarted (PID $pid)"
|
||||
else
|
||||
echo "ERROR: could not restart the monitor daemon" >&2
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
exit "$rc"
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env bash
|
||||
# update.sh — multi-agent-mux-update (Issue #72): update an installed workspace in place.
|
||||
# Usage:
|
||||
# bash .agents/skills/multi-agent-mux-update/scripts/update.sh [-y|--yes]
|
||||
#
|
||||
# Phase 1 (this script, the installed code): publish .mam/update-in-progress
|
||||
# under the admission lock (lib.sh mam_update_admit is the other side), refuse
|
||||
# while anything live could see the tree change (lib_py/update.py precheck, a
|
||||
# live loop marker, a monitor.lock held by a monitor the skills did not start),
|
||||
# stop the monitor daemon, back up what the install can change into
|
||||
# .mam/update-backup/, then exec a copy of install_phase.sh outside the tree.
|
||||
# That copy runs the installer (MAM_INSTALLER_URL) in place, rolls back on
|
||||
# failure, and runs phase 2 (post_update.sh of the NEW tree) on success.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = updated (registry converted or nothing to convert)
|
||||
# 1 = refused or failed; what was changed and restored is printed
|
||||
# 2 = invalid args
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WS="$(cd -P "$SCRIPT_DIR/../../../.." && pwd -P)"
|
||||
export WORKSPACE_ROOT="$WS"
|
||||
export AGENT_SESSIONS_YAML="$WS/.mam/agent-sessions.yaml"
|
||||
# shellcheck source=../../lib.sh
|
||||
source "$WS/.agents/skills/lib.sh"
|
||||
# shellcheck source=../../multi-agent-mux-loop/scripts/loop_lock.sh
|
||||
source "$SKILL_DIR/multi-agent-mux-loop/scripts/loop_lock.sh"
|
||||
|
||||
YES=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-y|--yes) YES=1; shift ;;
|
||||
-h|--help) sed -n '2,4p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
MAM="$WS/.mam"
|
||||
MARKER="$MAM/update-in-progress"
|
||||
BK="$MAM/update-backup"
|
||||
PY="$(_delegate_py_bin)"
|
||||
|
||||
if [ ! -f "$WS/.mam_deploy/remove.sh" ] && [ ! -f "$WS/remove.sh" ]; then
|
||||
echo "ERROR: $WS is not an installed MAM workspace (no .mam_deploy/remove.sh); a git checkout updates with git pull" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$YES" != "1" ]; then
|
||||
if [ ! -t 0 ]; then
|
||||
echo "ERROR: not a terminal; run with -y/--yes" >&2
|
||||
exit 1
|
||||
fi
|
||||
ans=""
|
||||
read -r -p "Update MAM in $WS to the latest version? [y/N]: " ans || ans=""
|
||||
case "$ans" in
|
||||
y|Y|yes|Yes|YES) ;;
|
||||
*) echo "Update cancelled."; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- the marker, published under the exclusive admission lock (decision 8) ---
|
||||
mkdir -p "$MAM"
|
||||
exec 7>>"$MAM/update-admission.lock"
|
||||
case "$("$PY" -m lib_py.pane_lock acquire --fd 7 --wait-env MAM_UPDATE_ADMISSION_WAIT_S)" in
|
||||
locked) ;;
|
||||
timeout)
|
||||
echo "ERROR: a create, resume, loop, delegate submit, stop or orc-onboard kept the admission lock past MAM_UPDATE_ADMISSION_WAIT_S; nothing was changed, try again" >&2
|
||||
exit 1 ;;
|
||||
*)
|
||||
echo "ERROR: the admission lock $MAM/update-admission.lock could not be decided (lib_py.pane_lock); nothing was changed" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
if [ -f "$MARKER" ] && [ "$(_mam_lock_holder_state "$MARKER")" = "stale" ]; then
|
||||
echo "NOTE: stale update marker (pid $(_mam_lock_field "$MARKER" pid)) taken over; its update is gone"
|
||||
fi
|
||||
marker_rc=0
|
||||
mam_acquire_loop_lock "$MARKER" || marker_rc=$?
|
||||
exec 7>&-
|
||||
case "$marker_rc" in
|
||||
0) ;;
|
||||
3) echo "ERROR: another update is in progress (pid $(_mam_lock_field "$MARKER" pid)); nothing was changed" >&2; exit 1 ;;
|
||||
*) echo "ERROR: cannot tell whether another update is in progress (ps unavailable?); nothing was changed" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
TMPD=""
|
||||
MONITOR_WAS_RUNNING=0
|
||||
RESTART_MONITOR=0
|
||||
_phase1_exit() {
|
||||
local rc=$?
|
||||
if [ "$RESTART_MONITOR" = "1" ]; then
|
||||
_restart_monitor || true
|
||||
fi
|
||||
mam_release_loop_lock "$MARKER" || true
|
||||
[ -z "$TMPD" ] || rm -rf "$TMPD"
|
||||
exit "$rc"
|
||||
}
|
||||
trap _phase1_exit EXIT
|
||||
|
||||
_restart_monitor() {
|
||||
if [ "${MAM_NO_SUBSCRIBER_DAEMON:-0}" = "1" ]; then
|
||||
echo "monitor: not restarted (MAM_NO_SUBSCRIBER_DAEMON=1)"
|
||||
return 0
|
||||
fi
|
||||
local pid
|
||||
pid=$(start_watchdog update "$WS") || { echo "ERROR: could not restart the monitor daemon" >&2; return 1; }
|
||||
echo "monitor: restarted (PID $pid)"
|
||||
}
|
||||
|
||||
_monitor_json() {
|
||||
"$PY" -m lib_py.update monitor --workspace "$WS"
|
||||
}
|
||||
_json_field() {
|
||||
"$PY" -c 'import json,sys; v=json.load(sys.stdin)[sys.argv[1]]; print(" ".join(v) if isinstance(v, list) else v)' "$1"
|
||||
}
|
||||
|
||||
# --- pre-check: refuse before any change (decisions 1, 7; U3) ---
|
||||
BLOCKERS=""
|
||||
_block() { BLOCKERS="${BLOCKERS} - $1
|
||||
"; }
|
||||
|
||||
if [ -f "$MAM/loop-guard-active" ]; then
|
||||
loop_st="$(_mam_lock_holder_state "$MAM/loop-guard-active")"
|
||||
case "$loop_st" in
|
||||
stale) echo "NOTE: stale loop marker $MAM/loop-guard-active (its loop is gone); not a reason to refuse" ;;
|
||||
*) _block "a review loop is running (pid $(_mam_lock_field "$MAM/loop-guard-active" pid), holder $loop_st); wait for it to end" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
STATE_JSON=""
|
||||
if ! STATE_JSON="$(load_state_json strict)"; then
|
||||
_block "the registry $AGENT_SESSIONS_YAML cannot be read; fix it first"
|
||||
STATE_JSON='{}'
|
||||
fi
|
||||
REAL_HERDR="$(_resolve_real_herdr_path || true)"
|
||||
if [ -z "$REAL_HERDR" ]; then
|
||||
_block "no herdr binary found on PATH, so the live agents cannot be listed"
|
||||
else
|
||||
pre_out=""
|
||||
pre_rc=0
|
||||
pre_out="$(MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.update precheck --workspace "$WS" --herdr "$REAL_HERDR")" || pre_rc=$?
|
||||
if [ "$pre_rc" -ne 0 ]; then
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && _block "$line"
|
||||
done <<< "${pre_out:-lib_py.update precheck failed (rc $pre_rc)}"
|
||||
fi
|
||||
fi
|
||||
|
||||
MON_JSON="$(_monitor_json)" || { _block "cannot inspect the monitor daemon (lib_py.update monitor)"; MON_JSON='{"pids": [], "lock": "free"}'; }
|
||||
MON_PIDS="$(printf '%s' "$MON_JSON" | _json_field pids)"
|
||||
MON_LOCK="$(printf '%s' "$MON_JSON" | _json_field lock)"
|
||||
if [ -z "$MON_PIDS" ] && [ "$MON_LOCK" = "held" ]; then
|
||||
_block "$MAM/monitor.lock is held by a monitor the skills did not start (not 'bash $WS/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe'); stop it (find it with: pgrep -af reconcile.sh) and try again"
|
||||
fi
|
||||
|
||||
if [ -n "$BLOCKERS" ]; then
|
||||
echo "ERROR: update refused; nothing was changed:" >&2
|
||||
printf '%s' "$BLOCKERS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- stop the monitor daemon (decision 2) ---
|
||||
_monitor_gone() {
|
||||
local p
|
||||
for p in "$@"; do
|
||||
kill -0 "$p" 2>/dev/null && return 1
|
||||
done
|
||||
[ "$(_monitor_json | _json_field lock)" = "free" ]
|
||||
}
|
||||
_wait_monitor_gone() {
|
||||
local deadline=$(( $(date +%s) + $1 ))
|
||||
shift
|
||||
while :; do
|
||||
_monitor_gone "$@" && return 0
|
||||
[ "$(date +%s)" -lt "$deadline" ] || return 1
|
||||
sleep 0.5
|
||||
done
|
||||
}
|
||||
if [ -n "$MON_PIDS" ]; then
|
||||
MONITOR_WAS_RUNNING=1
|
||||
# shellcheck disable=SC2086
|
||||
set -- $MON_PIDS
|
||||
kids=""
|
||||
for p in "$@"; do kids="$kids $(pgrep -P "$p" 2>/dev/null | tr '\n' ' ' || true)"; done
|
||||
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
|
||||
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
|
||||
kill -KILL "$@" $kids 2>/dev/null || true
|
||||
# shellcheck disable=SC2086
|
||||
if _wait_monitor_gone 5 "$@" $kids; then
|
||||
echo "ERROR: the monitor did not stop in time; it was killed and is started again. Nothing else was changed." >&2
|
||||
_restart_monitor || true
|
||||
exit 1
|
||||
fi
|
||||
echo "ERROR: could not restore the monitor state: PIDs still alive or monitor.lock still held ($*$kids; lock $(_monitor_json | _json_field lock)). Nothing else was changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
RESTART_MONITOR=1
|
||||
fi
|
||||
|
||||
TMPD="$(mktemp -d "${TMPDIR:-/tmp}/mam-update.XXXXXX")"
|
||||
cp "$SCRIPT_DIR/install_phase.sh" "$TMPD/install_phase.sh"
|
||||
|
||||
# --- a leftover backup of an update that died (U4) ---
|
||||
if [ -e "$BK" ]; then
|
||||
left_st="$(cat "$BK/state" 2>/dev/null || true)"
|
||||
case "$left_st" in
|
||||
""|backing-up|installed)
|
||||
echo "NOTE: removing the backup a previous update left (state ${left_st:-none}); nothing to restore"
|
||||
rm -rf "$BK" ;;
|
||||
installing)
|
||||
echo "NOTE: a previous update died during its install; restoring its backup (the registry is left as it is)"
|
||||
if ! bash "$TMPD/install_phase.sh" restore "$WS" --skip-registry; then
|
||||
RESTART_MONITOR=0
|
||||
echo "ERROR: restoring the previous update's backup failed; it is kept at $BK. Nothing more was changed; the monitor stays stopped." >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$BK"
|
||||
echo "the previous update was rolled back" ;;
|
||||
*)
|
||||
echo "ERROR: $BK has an unknown state '$left_st'; it is kept and nothing was changed. Check it and remove it by hand." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- the rollback backup (decisions 3, 10; U4) ---
|
||||
_set_state() { printf '%s\n' "$1" > "$BK/state.tmp" && mv -f "$BK/state.tmp" "$BK/state"; }
|
||||
ITEMS=".agents .mam_deploy AGENTS.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md .mam.env.example .mam.env .gitignore remove.sh update.sh .venv .mam/install_manifest.txt .mam/install_state .mam/asset_hashes.txt .mam/version.txt .mam/base .mam/skill-backups"
|
||||
_backup() {
|
||||
mkdir -p "$BK/files" || return 1
|
||||
_set_state backing-up || return 1
|
||||
: > "$BK/items" && : > "$BK/absent" || return 1
|
||||
local rel
|
||||
for rel in $ITEMS; do
|
||||
printf '%s\n' "$rel" >> "$BK/items" || return 1
|
||||
if [ -e "$WS/$rel" ] || [ -L "$WS/$rel" ]; then
|
||||
mkdir -p "$BK/files/$(dirname "$rel")" && cp -pPR "$WS/$rel" "$BK/files/$rel" || return 1
|
||||
else
|
||||
printf '%s\n' "$rel" >> "$BK/absent" || return 1
|
||||
fi
|
||||
done
|
||||
"$PY" -m lib_py.update backup-registry --workspace "$WS" --dest "$BK/registry" >/dev/null
|
||||
}
|
||||
echo "backup: $BK"
|
||||
if ! _backup; then
|
||||
rm -rf "$BK"
|
||||
echo "ERROR: the backup could not be made; nothing was changed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- hand over to the copy (decision 5): same pid, so the marker stays live ---
|
||||
RESTART_MONITOR=0
|
||||
trap - EXIT
|
||||
exec bash "$TMPD/install_phase.sh" run "$WS" "$MONITOR_WAS_RUNNING" "$TMPD"
|
||||
Reference in new issue
Block a user