diff --git a/scripts/restart-mac.sh b/scripts/restart-mac.sh index 3699d97983b..a5e7f7e99c2 100755 --- a/scripts/restart-mac.sh +++ b/scripts/restart-mac.sh @@ -14,6 +14,7 @@ LAUNCH_AGENT="${HOME}/Library/LaunchAgents/ai.openclaw.mac.plist" LOCK_KEY="$(printf '%s' "${ROOT_DIR}" | shasum -a 256 | cut -c1-8)" LOCK_DIR="${TMPDIR:-/tmp}/openclaw-restart-${LOCK_KEY}" LOCK_PID_FILE="${LOCK_DIR}/pid" +LOCK_HELD=0 WAIT_FOR_LOCK=0 LOG_PATH="${OPENCLAW_RESTART_LOG:-${TMPDIR:-/tmp}/openclaw-restart-${LOCK_KEY}.log}" NO_SIGN=0 @@ -38,7 +39,14 @@ run_step() { } cleanup() { - if [[ -d "${LOCK_DIR}" ]]; then + if [[ "${LOCK_HELD}" != "1" || ! -d "${LOCK_DIR}" ]]; then + return 0 + fi + local owner_pid="" + if [[ -f "${LOCK_PID_FILE}" ]]; then + owner_pid="$(cat "${LOCK_PID_FILE}" 2>/dev/null || true)" + fi + if [[ -z "${owner_pid}" || "${owner_pid}" == "$$" ]]; then rm -rf "${LOCK_DIR}" fi } @@ -46,6 +54,7 @@ cleanup() { acquire_lock() { while true; do if mkdir "${LOCK_DIR}" 2>/dev/null; then + LOCK_HELD=1 echo "$$" > "${LOCK_PID_FILE}" return 0 fi diff --git a/test/scripts/restart-mac.test.ts b/test/scripts/restart-mac.test.ts index 3069e65704b..e499773c9b4 100644 --- a/test/scripts/restart-mac.test.ts +++ b/test/scripts/restart-mac.test.ts @@ -163,6 +163,36 @@ function runRestartArgParser(...args: string[]) { return spawnSync("bash", [harnessPath, ...args], { encoding: "utf8" }); } +function runRestartLockHarness(lockDir: string) { + const root = mkdtempSync(join(tmpdir(), "openclaw-restart-mac-test-")); + tempRoots.push(root); + + const script = readFileSync(restartScriptPath, "utf8"); + const lockBlock = script.slice( + script.indexOf("cleanup()"), + script.indexOf("check_signing_keys()"), + ); + const harnessPath = join(root, "lock-harness.sh"); + writeFileSync( + harnessPath, + [ + "#!/usr/bin/env bash", + "set -euo pipefail", + `LOCK_DIR=${shellQuote(lockDir)}`, + 'LOCK_PID_FILE="${LOCK_DIR}/pid"', + "LOCK_HELD=0", + "WAIT_FOR_LOCK=0", + 'log() { printf "%s\\n" "$*"; }', + lockBlock, + "trap cleanup EXIT", + "acquire_lock", + ].join("\n"), + ); + chmodSync(harnessPath, 0o755); + + return spawnSync("bash", [harnessPath], { encoding: "utf8" }); +} + afterEach(() => { for (const root of tempRoots.splice(0)) { rmSync(root, { force: true, recursive: true }); @@ -227,6 +257,35 @@ describe("scripts/restart-mac.sh", () => { expect(script).not.toContain('LOG_PATH="${OPENCLAW_RESTART_LOG:-/tmp/openclaw-restart.log}"'); }); + it("does not remove a live restart lock it did not acquire", () => { + const root = mkdtempSync(join(tmpdir(), "openclaw-restart-mac-test-")); + tempRoots.push(root); + const lockDir = join(root, "openclaw-restart-lock"); + mkdirSync(lockDir); + writeFileSync(join(lockDir, "pid"), String(process.pid), "utf8"); + + const result = runRestartLockHarness(lockDir); + + expect(result.status).toBe(0); + expect(result.stdout).toContain(`Another restart is running (pid ${process.pid}); re-run with --wait.`); + expect(result.stderr).toBe(""); + expect(existsSync(lockDir)).toBe(true); + expect(readFileSync(join(lockDir, "pid"), "utf8")).toBe(String(process.pid)); + }); + + it("removes the restart lock it acquired", () => { + const root = mkdtempSync(join(tmpdir(), "openclaw-restart-mac-test-")); + tempRoots.push(root); + const lockDir = join(root, "openclaw-restart-lock"); + + const result = runRestartLockHarness(lockDir); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe(""); + expect(existsSync(lockDir)).toBe(false); + }); + it("prefers the freshly packaged app unless an explicit app bundle is set", () => { const script = readFileSync(restartScriptPath, "utf8"); const chooseBlock = script.slice(