diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh index 4b16e848a..ed5b41d46 100755 --- a/scripts/stack-release.sh +++ b/scripts/stack-release.sh @@ -143,20 +143,26 @@ process.exit(1);' -- "$LOG" "$1" "$P_COPY" show() { systemctl --user show "$1" -p LoadState -p WorkingDirectory -p ExecStart -p Environment -p MainPID -p DropInPaths 2>/dev/null; } argv_of() { sed -n 's/^ExecStart=.*argv\[\]=\([^;]*\) ;.*/\1/p' <<< "$1"; } +# systemctl show quotes a value that holds a space and escapes with a backslash, and the read -a +# split below can't undo either, so such a line is unreadable rather than read wrong (row 166 item 4). +unquoted() { [[ "$1" != *[\"\'\\]* ]]; } # Sets U_PM U_CLAUDE U_CONSOLE (array) from the effective units; returns 1 if a unit can't be read. read_units() { - local bus console tok + local bus console tok bus_argv console_argv env bus="$(show "$BUS_UNIT")" && console="$(show "$CONSOLE_UNIT")" || return 1 [ "$(sed -n 's/^LoadState=//p' <<< "$bus")" = loaded ] && [ "$(sed -n 's/^LoadState=//p' <<< "$console")" = loaded ] || return 1 + bus_argv="$(argv_of "$bus")" console_argv="$(argv_of "$console")" env="$(sed -n 's/^Environment=//p' <<< "$bus")" + unquoted "$bus_argv" && unquoted "$console_argv" || return 1 + unquoted "$env" || return 1 local -a argv - read -r -a argv <<< "$(argv_of "$bus")" + read -r -a argv <<< "$bus_argv" U_PM=0 for tok in "${argv[@]:1}"; do [ "$tok" != --pm ] || U_PM=1; done U_CLAUDE="" - read -r -a argv <<< "$(sed -n 's/^Environment=//p' <<< "$bus")" + read -r -a argv <<< "$env" for tok in "${argv[@]}"; do case "$tok" in MOSAIC_CLAUDE_BIN=*) U_CLAUDE="${tok#MOSAIC_CLAUDE_BIN=}" ;; esac; done - read -r -a argv <<< "$(argv_of "$console")" + read -r -a argv <<< "$console_argv" U_CONSOLE=() local i serve=0 for ((i = 0; i < ${#argv[@]}; i++)); do @@ -371,7 +377,10 @@ process.stdout.write(why);' -- "$out" "$BUSINESS" "$main" "$START_MS" 2>&1)" || node -e ' fetch(`http://127.0.0.1:${process.argv[1]}/healthz`,{signal:AbortSignal.timeout(5000)}).then((r)=>process.exit(r.status===200?0:1),()=>process.exit(1));' -- "$(console_port)" || return 1 GATE=copy-check - scripts/stack-copy.sh check --copies "$COPIES" --bin "$BIN" --business "$BUSINESS" > /dev/null 2>&1 || return 1 + out="$(scripts/stack-copy.sh check --copies "$COPIES" --bin "$BIN" --business "$BUSINESS" 2>/dev/null)" || return 1 + # check passes when the three agree on one copy; this run needs it to be the target (row 166 item 3). + out="$(sed -n 's/^bus: //p' <<< "$out")" + [ "$out" = "$copy" ] || { GATE_WHY="the bus, Console and CLI agree on $out, not $copy"; return 1; } if [ "$pm" = 1 ]; then GATE=pm-launch-unconfirmed pm_launched "$copy" || return 1 @@ -499,17 +508,23 @@ instance_busy() { done <<< "$out" } -# install_guard's drop-in scan (bus-service.sh), done before anything stops. +# install_guard's drop-in scan (bus-service.sh), done before anything stops. A path it can't read +# refuses here, as install_guard would refuse it after the stop (row 166 item 1). drop_in_override() { - local paths directory dropdir file + local paths directory dropdir file content paths="$(systemd-analyze --user unit-paths 2>/dev/null)" && [ -n "$paths" ] || refuse drop-in-override "systemd unit paths are unknown" while IFS= read -r directory; do - [ -n "$directory" ] && [ -d "$directory" ] || continue + [ -n "$directory" ] || continue + case "$directory" in /*) ;; *) refuse drop-in-override "invalid systemd unit path $directory" ;; esac + [ -e "$directory" ] || [ -L "$directory" ] || continue + [ -d "$directory" ] && [ -r "$directory" ] && [ -x "$directory" ] || refuse drop-in-override "cannot inspect unit path $directory" for dropdir in "$directory"/mosaic-bus@*.service.d "$directory"/mosaic-.service.d "$directory"/service.d; do - [ -d "$dropdir" ] || continue + [ -e "$dropdir" ] || [ -L "$dropdir" ] || continue + [ -d "$dropdir" ] && [ -r "$dropdir" ] && [ -x "$dropdir" ] || refuse drop-in-override "cannot inspect drop-ins $dropdir" for file in "$dropdir"/*.conf; do - [ -e "$file" ] || continue - if grep -Eq '^[[:space:]]*(ExecStart|WorkingDirectory)[[:space:]]*=' "$file" 2>/dev/null; then + [ -e "$file" ] || [ -L "$file" ] || continue + content="$(cat "$file" 2>/dev/null)" || refuse drop-in-override "cannot read drop-in $file" + if grep -Eq '^[[:space:]]*(ExecStart|WorkingDirectory)[[:space:]]*=' <<< "$content"; then refuse drop-in-override "$file overrides ExecStart or WorkingDirectory; the operator removes it as in TOOLS.md step 4" fi done @@ -572,17 +587,21 @@ do_adopt() { # pointer's schema, and says so, when there is no store file or it can't be read. plan_store() { local src="" why="" - S_SCHEMA="" S_FROM="" + S_SCHEMA="" S_FROM="" S_CHANGED="" if [ ! -e "$STORE" ]; then why="there is no store file" elif systemctl --user is-active --quiet "$BUS_UNIT"; then src="the store the running bus holds" S_SCHEMA="$(node -e "$STORE_JS" -- heldversion "$STORE" 2>/dev/null)" || S_SCHEMA="" + # A bus that stops during the readOnly open can leave -wal or -shm beside the stopped + # store, so the version read can't be planned on (row 166 item 2). + systemctl --user is-active --quiet "$BUS_UNIT" || S_CHANGED=1 elif store_unclean; then why="the store was not read: $UNCLEAN is present" else src="the stopped store" S_SCHEMA="$(store_read version 2>/dev/null)" || S_SCHEMA="" fi - if [ -n "$S_SCHEMA" ]; then S_FROM="read from $src" + if [ -n "$S_CHANGED" ]; then S_SCHEMA=unknown S_FROM="the bus stopped during the read, so the store state changed; run --plan again" + elif [ -n "$S_SCHEMA" ]; then S_FROM="read from $src" else S_SCHEMA="$P_SCHEMA" S_FROM="from the pointer; ${why:-$src could not be read}" fi } @@ -631,8 +650,10 @@ plan() { echo "to: $TO" echo "store schema: $S_SCHEMA ($S_FROM)" echo "target schema: $T_SCHEMA" - [ "$S_SCHEMA" = "$P_SCHEMA" ] || echo "note: the pointer says $P_SCHEMA; the run reads the stopped store and goes by that" - if [ "$S_SCHEMA" = "$T_SCHEMA" ]; then + [ "$S_SCHEMA" = "$P_SCHEMA" ] || [ -n "$S_CHANGED" ] || echo "note: the pointer says $P_SCHEMA; the run reads the stopped store and goes by that" + if [ -n "$S_CHANGED" ]; then + : # no migration or restore line from a read the bus stopped under + elif [ "$S_SCHEMA" = "$T_SCHEMA" ]; then echo "migration: none; the store is at the target's version" elif [ "$OP" = activate ] && in_chain "$S_SCHEMA"; then echo "migration: $S_SCHEMA to $T_SCHEMA, after a backup of the stopped store" @@ -715,6 +736,8 @@ do_rollback() { # no file (the probe); a stopped one falls under the store_unclean rule. if systemctl --user is-active --quiet "$BUS_UNIT"; then COUNT="$(node -e "$STORE_JS" -- heldcount "$STORE" "$(cat "$o/STAMP")")" || rc=1 + # plan_store's recheck (row 166 item 2), before anything stops. + systemctl --user is-active --quiet "$BUS_UNIT" || refuse restore-mismatch "the bus stopped while the events after $(cat "$o/STAMP") were counted, so the store state changed; nothing was stopped" else ! store_unclean || refuse store-unclean "$UNCLEAN is present; the bus did not close the store cleanly" COUNT="$(store_read count "$(cat "$o/STAMP")")" || rc=1 @@ -778,7 +801,7 @@ LOG="$STATE/stack-activation-log.jsonl" STORE="$DATA/bus/bus.sqlite" BUS_UNIT="mosaic-bus@$BUSINESS.service" FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL="" -STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" +STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED="" P_COPY="" P_PM=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() case "$OP" in adopt) do_adopt ;;