mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
harden update restart script creation [AI] (#84088)
* fix: harden update restart script creation * docs: add changelog entry for PR merge
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- harden update restart script creation [AI]. (#84088) Thanks @pgondhi987.
|
||||
- Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.
|
||||
- CLI/channels: preserve the first line of `openclaw channels logs` output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed `readLogSlice` behavior in `src/logging/log-tail.ts`.
|
||||
- Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)
|
||||
|
||||
@@ -34,6 +34,11 @@ describe("restart-helper", () => {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
await fs.rmdir(path.dirname(scriptPath)).catch((error: unknown) => {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function makeTempDir(prefix: string) {
|
||||
@@ -135,9 +140,63 @@ exit 0
|
||||
expect(content).toContain("systemctl --user restart 'openclaw-gateway.service'");
|
||||
// Script should self-cleanup
|
||||
expect(content).toContain('rm -f "$0"');
|
||||
expect(content).toContain('rmdir "$script_dir" 2>/dev/null || true');
|
||||
await cleanupScript(scriptPath);
|
||||
});
|
||||
|
||||
it("creates restart scripts in a private temp directory with exclusive creation", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
const timestamp = 1_727_201_234_567;
|
||||
const oldCandidatePath = path.join(os.tmpdir(), `openclaw-restart-${timestamp}.sh`);
|
||||
const victimDir = await makeTempDir("openclaw-restart-helper-victim-");
|
||||
const victimPath = path.join(victimDir, "restart.sh");
|
||||
await fs.rm(oldCandidatePath, { force: true });
|
||||
await fs.writeFile(victimPath, "preexisting script\n", "utf-8");
|
||||
|
||||
let candidateIsSymlink = false;
|
||||
try {
|
||||
await fs.symlink(victimPath, oldCandidatePath);
|
||||
candidateIsSymlink = true;
|
||||
} catch {
|
||||
await fs.writeFile(oldCandidatePath, "preexisting script\n", { flag: "wx" });
|
||||
}
|
||||
|
||||
const dateSpy = vi.spyOn(Date, "now").mockReturnValue(timestamp);
|
||||
const writeFileSpy = vi.spyOn(fs, "writeFile");
|
||||
|
||||
try {
|
||||
const { scriptPath } = await prepareAndReadScript({
|
||||
OPENCLAW_PROFILE: "default",
|
||||
});
|
||||
const scriptDir = path.dirname(scriptPath);
|
||||
const relativeScriptDir = path.relative(os.tmpdir(), scriptDir);
|
||||
|
||||
expect(scriptPath).not.toBe(oldCandidatePath);
|
||||
expect(scriptDir).not.toBe(os.tmpdir());
|
||||
expect(relativeScriptDir).not.toBe("");
|
||||
expect(relativeScriptDir.startsWith("..")).toBe(false);
|
||||
expect(path.isAbsolute(relativeScriptDir)).toBe(false);
|
||||
expect(path.basename(scriptDir)).toMatch(/^openclaw-restart-/);
|
||||
expect(writeFileSpy).toHaveBeenLastCalledWith(
|
||||
scriptPath,
|
||||
expect.any(String),
|
||||
expect.objectContaining({ flag: "wx", mode: 0o755 }),
|
||||
);
|
||||
await expect(fs.readFile(victimPath, "utf-8")).resolves.toBe("preexisting script\n");
|
||||
if (!candidateIsSymlink) {
|
||||
await expect(fs.readFile(oldCandidatePath, "utf-8")).resolves.toBe(
|
||||
"preexisting script\n",
|
||||
);
|
||||
}
|
||||
await cleanupScript(scriptPath);
|
||||
} finally {
|
||||
dateSpy.mockRestore();
|
||||
writeFileSpy.mockRestore();
|
||||
await fs.rm(oldCandidatePath, { force: true });
|
||||
await fs.rm(victimDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_SYSTEMD_UNIT override for systemd scripts", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
const { scriptPath, content } = await prepareAndReadScript({
|
||||
@@ -203,6 +262,7 @@ exit 1
|
||||
expect(content).toContain("launchctl bootstrap 'gui/501'");
|
||||
expect(content).toContain("Bootstrap loads RunAtLoad agents");
|
||||
expect(content).toContain('rm -f "$0"');
|
||||
expect(content).toContain('rmdir "$script_dir" 2>/dev/null || true');
|
||||
await cleanupScript(scriptPath);
|
||||
});
|
||||
|
||||
@@ -379,6 +439,7 @@ exit 0
|
||||
expect(content).toContain("openclaw restart launched startup fallback");
|
||||
expectWindowsRestartWaitOrdering(content);
|
||||
expect(content).toContain('del "%~f0" >nul 2>&1');
|
||||
expect(content).toContain('rmdir "%OPENCLAW_RESTART_SCRIPT_DIR%" >nul 2>&1');
|
||||
await cleanupScript(scriptPath);
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ export async function prepareRestartScript(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
gatewayPort: number = DEFAULT_GATEWAY_PORT,
|
||||
): Promise<string | null> {
|
||||
const tmpDir = os.tmpdir();
|
||||
const timestamp = Date.now();
|
||||
const platform = process.platform;
|
||||
|
||||
@@ -110,8 +109,10 @@ else
|
||||
fi
|
||||
fi
|
||||
# Self-cleanup
|
||||
script_dir=$(dirname "$0")
|
||||
exec 3>&-
|
||||
rm -f "$0"
|
||||
rmdir "$script_dir" 2>/dev/null || true
|
||||
exit "$status"
|
||||
`;
|
||||
} else if (platform === "darwin") {
|
||||
@@ -157,7 +158,9 @@ else
|
||||
printf '[%s] openclaw restart failed source=update status=%s\\n' "$(date -u +%FT%TZ)" "$status" >&2
|
||||
fi
|
||||
# Self-cleanup (log is retained under the OpenClaw state logs directory).
|
||||
script_dir=$(dirname "$0")
|
||||
rm -f "$0"
|
||||
rmdir "$script_dir" 2>/dev/null || true
|
||||
exit "$status"
|
||||
`;
|
||||
} else if (platform === "win32") {
|
||||
@@ -177,9 +180,11 @@ REM Keep this as a cmd wrapper so Group Policy script execution policies
|
||||
REM cannot block the update restart handoff before schtasks.exe runs.
|
||||
setlocal
|
||||
set "OPENCLAW_RESTART_SCRIPT=%~f0"
|
||||
set "OPENCLAW_RESTART_SCRIPT_DIR=%~dp0."
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$p=$env:OPENCLAW_RESTART_SCRIPT; $s=Get-Content -Raw -LiteralPath $p; $m='# POWERSHELL'; $i=$s.IndexOf($m); if ($i -lt 0) { exit 1 }; Invoke-Expression $s.Substring($i)"
|
||||
set "status=%ERRORLEVEL%"
|
||||
del "%~f0" >nul 2>&1
|
||||
rmdir "%OPENCLAW_RESTART_SCRIPT_DIR%" >nul 2>&1
|
||||
exit /b %status%
|
||||
# POWERSHELL
|
||||
# Wait briefly to ensure file locks are released after update.
|
||||
@@ -370,8 +375,14 @@ exit $status
|
||||
return null;
|
||||
}
|
||||
|
||||
const scriptPath = path.join(tmpDir, filename);
|
||||
await fs.writeFile(scriptPath, scriptContent, { mode: 0o755 });
|
||||
const scriptDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-restart-"));
|
||||
const scriptPath = path.join(scriptDir, filename);
|
||||
try {
|
||||
await fs.writeFile(scriptPath, scriptContent, { mode: 0o755, flag: "wx" });
|
||||
} catch (error) {
|
||||
await fs.rm(scriptDir, { recursive: true, force: true }).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
return scriptPath;
|
||||
} catch {
|
||||
// If we can't write the script, we'll fall back to the standard restart method
|
||||
|
||||
Reference in New Issue
Block a user