fix(update): restart prepared daemon when coordination fails
24 unmodified lines2526272829303132331586 unmodified linesExpand all162016211622162316241625162616231624162516261627162716281629163016311632163316341635163616 unmodified lines1653165416551656165716581659166016611662166316641652165316541655165616651666166716681669167016711672167316748 unmodified lines24 unmodified lines type DaemonUpdateRestartProcessIdentity, type DaemonUpdateRestartStatus, DaemonUpdateRestartStatusWriter, launchDaemonUpdateRestartCoordinator, restartPreparedDaemon, waitForActiveDaemonUpdateRestartCoordinator,} from "./cli/daemon-update-restart.js";import { APP_NAME,1586 unmodified linesExpand all originActiveSessionId: process.env[DAEMON_WORKER_ACTIVE_SESSION_ID_ENV], }); reportDaemonUpdateRestartStatus(status); } catch (restartError: unknown) { try { await restartPreparedDaemon(daemonSocketPath); } catch (fallbackError: unknown) { console.error( chalk.yellow( `Warning: package update failed and daemon recovery also failed (${formatUnknownError(restartError)}).`, ), ); console.error( chalk.yellow( `Warning: package update failed and daemon recovery also failed (${formatUnknownError(restartError)}; fallback restart: ${formatUnknownError(fallbackError)}).`, ), ); } } } const message = error instanceof Error ? error.message : "Unknown package command error"; console.error(chalk.red(`Error: ${message}`));16 unmodified lines originActiveSessionId: process.env[DAEMON_WORKER_ACTIVE_SESSION_ID_ENV], }); reportDaemonUpdateRestartStatus(status); } catch (error: unknown) { try { await restartPreparedDaemon(daemonSocketPath); console.error( chalk.yellow( `Warning: updated, but automatic session restoration failed (${formatUnknownError(error)}). Restarted the daemon without restoration.`, ), ); } catch (fallbackError: unknown) { console.error( chalk.yellow( `Warning: updated, but could not coordinate the daemon restart (${formatUnknownError(error)}).`, ), ); console.error( chalk.yellow( `Warning: updated, but could not coordinate or restart the daemon (${formatUnknownError(error)}; fallback restart: ${formatUnknownError(fallbackError)}).`, ), ); } } } return true; }8 unmodified lines119 unmodified linesExpand all12012112212312412512612712875 unmodified lines204205206207208209210211212274 unmodified linesExpand all487488489490491492493494495137 unmodified linesExpand all633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664815 unmodified linesExpand all119 unmodified linesExpand alltype MockDaemonResponse = { success: true; data?: unknown } | { success: false; error: string };
const mockState = vi.hoisted(() => ({ calls: [] as string[], coordinatorLaunchError: undefined as string | undefined, createActiveSessionIds: [] as string[], createThrowSessionPaths: [] as string[], daemonProbe: { reachable: true, activeSessions: [] } as MockRunningDaemonProbe, daemonProbeAfterShutdown: undefined as MockRunningDaemonProbe | undefined,75 unmodified lines return { ...original, launchDaemonUpdateRestartCoordinator: vi.fn(async (options: { socketPath: string }) => { mockState.calls.push(`launch-coordinator:${options.socketPath}`); if (mockState.coordinatorLaunchError) throw new Error(mockState.coordinatorLaunchError); return { version: 1, requestId: "test-request", socketPath: options.socketPath,274 unmodified linesExpand all mockState.socketPath = join(tempDir, "daemon.sock"); mockState.successorProcessStartId = "replacement-start"; mockState.successorSocketPath = undefined; mockState.calls = []; mockState.coordinatorLaunchError = undefined; mockState.createActiveSessionIds = []; mockState.createThrowSessionPaths = []; mockState.daemonProbe = { reachable: true, activeSessions: [] }; mockState.daemonProbeAfterShutdown = undefined;137 unmodified linesExpand all logSpy.mockRestore(); } });
it("restarts the prepared daemon when the post-install coordinator cannot launch", async () => { mockState.coordinatorLaunchError = "coordinator executable missing"; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect( handlePackageCommand(["update", "--self", "--daemon-socket", mockState.socketPath]), ).resolves.toBe(true);
expect(process.exitCode).toBeUndefined(); const prepareIndex = mockState.calls.indexOf("daemon-request:prepare_update_restart"); const spawnIndex = mockState.calls.findIndex((call) => call.startsWith("spawn:npm ")); const launchIndex = mockState.calls.indexOf(`launch-coordinator:${mockState.socketPath}`); const fallbackIndex = mockState.calls.indexOf("daemon-request:restart"); expect(spawnIndex).toBeGreaterThan(prepareIndex); expect(launchIndex).toBeGreaterThan(spawnIndex); expect(fallbackIndex).toBeGreaterThan(launchIndex); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("defers the exact custom-socket restart to the interactive parent", async () => { process.env[SELF_UPDATE_INTERACTIVE_CHILD_ENV] = "1"; const customSocketPath = join(tempDir, "custom", "daemon.sock");
815 unmodified linesExpand all4 unmodified lines5678910111213513 unmodified linesExpand all52752852953053153253353453553653753853954054154254354454567 unmodified lines4 unmodified linesimport lockfile from "proper-lockfile";import { ENV_AGENT_DIR, SELF_UPDATE_INTERACTIVE_CHILD_ENV } from "../config.js";import { ORPHAN_PROCESS_JOURNAL_ENV } from "../core/orphan-process-journal.js";import { getProcessStartId, SESSION_LEASE_OWNER_ID_ENV, SESSION_LEASES_ENABLED_ENV } from "../core/session-lease.js";import { DaemonClient } from "../modes/daemon/daemon-client.js";import { defaultDaemonSocketDir, defaultDaemonSocketPath, normalizeSocketPath } from "../modes/daemon/daemon-socket.js";import { DAEMON_WORKER_ACTIVE_SESSION_ID_ENV, DAEMON_WORKER_RECOVERY_JOURNAL_ENV,513 unmodified linesExpand all delete environment[SESSION_LEASE_OWNER_ID_ENV]; return environment;}
export async function restartPreparedDaemon(socketPath: string): Promise<void> { const client = new DaemonClient(resolveDaemonUpdateRestartSocketPath(socketPath)); try { await client.connect(1000); const response = await client.request({ type: "restart" }, 5000); if (!response.success) throw new Error(response.error); } finally { client.close(); }}
export async function launchDaemonUpdateRestartCoordinator( options: LaunchDaemonUpdateRestartCoordinatorOptions,): Promise<DaemonUpdateRestartStatus> { const requestId = randomUUID();67 unmodified lines