fix(cli): use standalone script for service restart after update (#17225)
The updater was previously attempting to restart the service using the installed codebase, which could be in an inconsistent state during the update process. This caused the service to stall when the updater deleted its own files before the restart could complete. Changes: - restart-helper.ts: new module that writes a platform-specific restart script to os.tmpdir() before the update begins (Linux systemd, macOS launchctl, Windows schtasks). - update-command.ts: prepares the restart script before installing, then uses it for service restart instead of the standard runDaemonRestart. - restart-helper.test.ts: 12 tests covering all platforms, custom profiles, error cases, and shell injection safety. Review feedback addressed: - Use spawn(detached: true) + unref() so restart script survives parent process termination (Greptile). - Shell-escape profile values using single-quote wrapping to prevent injection via OPENCLAW_PROFILE (Greptile). - Reject unsafe batch characters on Windows. - Self-cleanup: scripts delete themselves after execution (Copilot). - Add tests for write failures and custom profiles (Copilot). Fixes #17225
This commit is contained in:
committed by
Peter Steinberger
parent
a62ff19a66
commit
b1d5c71609
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../commands/doctor-completion.js";
|
||||
import { doctorCommand } from "../../commands/doctor.js";
|
||||
import { readConfigFileSnapshot, writeConfigFile } from "../../config/config.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import {
|
||||
channelToNpmTag,
|
||||
DEFAULT_GIT_CHANNEL,
|
||||
@@ -34,6 +35,7 @@ import { formatCliCommand } from "../command-format.js";
|
||||
import { installCompletion } from "../completion-cli.js";
|
||||
import { runDaemonRestart } from "../daemon-cli.js";
|
||||
import { createUpdateProgress, printResult } from "./progress.js";
|
||||
import { prepareRestartScript, runRestartScript } from "./restart-helper.js";
|
||||
import {
|
||||
DEFAULT_PACKAGE_NAME,
|
||||
ensureGitCheckout,
|
||||
@@ -388,6 +390,7 @@ async function maybeRestartService(params: {
|
||||
shouldRestart: boolean;
|
||||
result: UpdateRunResult;
|
||||
opts: UpdateCommandOptions;
|
||||
restartScriptPath?: string | null;
|
||||
}): Promise<void> {
|
||||
if (params.shouldRestart) {
|
||||
if (!params.opts.json) {
|
||||
@@ -396,7 +399,14 @@ async function maybeRestartService(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const restarted = await runDaemonRestart();
|
||||
let restarted = false;
|
||||
if (params.restartScriptPath) {
|
||||
await runRestartScript(params.restartScriptPath);
|
||||
restarted = true;
|
||||
} else {
|
||||
restarted = await runDaemonRestart();
|
||||
}
|
||||
|
||||
if (!params.opts.json && restarted) {
|
||||
defaultRuntime.log(theme.success("Daemon restarted successfully."));
|
||||
defaultRuntime.log("");
|
||||
@@ -566,6 +576,18 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
const { progress, stop } = createUpdateProgress(showProgress);
|
||||
const startedAt = Date.now();
|
||||
|
||||
let restartScriptPath: string | null = null;
|
||||
if (shouldRestart) {
|
||||
try {
|
||||
const loaded = await resolveGatewayService().isLoaded({ env: process.env });
|
||||
if (loaded) {
|
||||
restartScriptPath = await prepareRestartScript(process.env);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors during pre-check; fallback to standard restart
|
||||
}
|
||||
}
|
||||
|
||||
const result = switchToPackage
|
||||
? await runPackageInstallUpdate({
|
||||
root,
|
||||
@@ -638,6 +660,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
shouldRestart,
|
||||
result,
|
||||
opts,
|
||||
restartScriptPath,
|
||||
});
|
||||
|
||||
if (!opts.json) {
|
||||
|
||||
Reference in New Issue
Block a user