feat(desktop): simplify quit lifecycle with sync PID check and shutdown dialog
Replace the async daemon status resolution during quit with a synchronous PID file check, avoiding races when the event loop is shutting down. Show a blocking dialog so users know the daemon is stopping. Add --kill-timeout to the CLI stop command for the desktop app to control SIGKILL wait time.
This commit is contained in:
@@ -27,6 +27,7 @@ export function createDaemonCommand(): Command {
|
||||
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
|
||||
.option("--timeout <seconds>", "Wait timeout before failing (default: 15)")
|
||||
.option("--force", "Send SIGKILL if graceful stop times out")
|
||||
.option("--kill-timeout <seconds>", "Wait after SIGKILL before failing (default: 3)")
|
||||
.action(withOutput(runStopCommand));
|
||||
|
||||
addJsonOption(daemon.command("restart").description("Restart the local daemon"))
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface DetachedStartResult {
|
||||
export interface StopLocalDaemonOptions {
|
||||
home?: string;
|
||||
timeoutMs?: number;
|
||||
killTimeoutMs?: number;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
@@ -64,11 +65,11 @@ type DetachedStartupResult = { exitedEarly: false } | ({ exitedEarly: true } & P
|
||||
|
||||
const DETACHED_STARTUP_GRACE_MS = 1200;
|
||||
const PID_POLL_INTERVAL_MS = 100;
|
||||
const KILL_TIMEOUT_MS = 3000;
|
||||
const DAEMON_LOG_FILENAME = "daemon.log";
|
||||
const DAEMON_PID_FILENAME = "paseo.pid";
|
||||
|
||||
export const DEFAULT_STOP_TIMEOUT_MS = 15_000;
|
||||
export const DEFAULT_KILL_TIMEOUT_MS = 3_000;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
@@ -494,6 +495,7 @@ export async function stopLocalDaemon(
|
||||
options: StopLocalDaemonOptions = {},
|
||||
): Promise<StopLocalDaemonResult> {
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
|
||||
const killTimeoutMs = options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
|
||||
const state = resolveLocalDaemonState({ home: options.home });
|
||||
|
||||
if (!state.pidInfo || !state.running) {
|
||||
@@ -530,7 +532,7 @@ export async function stopLocalDaemon(
|
||||
if (!stopped && options.force) {
|
||||
forced = true;
|
||||
signalProcessGroupSafely(pid, "SIGKILL");
|
||||
stopped = await waitForPidExit(pid, KILL_TIMEOUT_MS);
|
||||
stopped = await waitForPidExit(pid, killTimeoutMs);
|
||||
}
|
||||
|
||||
if (!stopped) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { Command } from "commander";
|
||||
import { stopLocalDaemon, DEFAULT_STOP_TIMEOUT_MS } from "./local-daemon.js";
|
||||
import {
|
||||
stopLocalDaemon,
|
||||
DEFAULT_STOP_TIMEOUT_MS,
|
||||
DEFAULT_KILL_TIMEOUT_MS,
|
||||
} from "./local-daemon.js";
|
||||
import type {
|
||||
CommandOptions,
|
||||
SingleResult,
|
||||
@@ -30,17 +34,17 @@ const stopResultSchema: OutputSchema<StopResult> = {
|
||||
|
||||
export type StopCommandResult = SingleResult<StopResult>;
|
||||
|
||||
function parseTimeoutMs(raw: unknown): number {
|
||||
function parseSecondsOption(raw: unknown, fallbackMs: number, label: string): number {
|
||||
if (typeof raw !== "string" || raw.trim().length === 0) {
|
||||
return DEFAULT_STOP_TIMEOUT_MS;
|
||||
return fallbackMs;
|
||||
}
|
||||
|
||||
const seconds = Number(raw);
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) {
|
||||
const error: CommandError = {
|
||||
code: "INVALID_TIMEOUT",
|
||||
message: `Invalid timeout value: ${raw}`,
|
||||
details: "Timeout must be a positive number of seconds",
|
||||
message: `Invalid ${label} value: ${raw}`,
|
||||
details: `${label} must be a positive number of seconds`,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
@@ -54,10 +58,15 @@ export async function runStopCommand(
|
||||
): Promise<StopCommandResult> {
|
||||
const home = typeof options.home === "string" ? options.home : undefined;
|
||||
const force = options.force === true;
|
||||
const timeoutMs = parseTimeoutMs(options.timeout);
|
||||
const timeoutMs = parseSecondsOption(options.timeout, DEFAULT_STOP_TIMEOUT_MS, "timeout");
|
||||
const killTimeoutMs = parseSecondsOption(
|
||||
options.killTimeout,
|
||||
DEFAULT_KILL_TIMEOUT_MS,
|
||||
"kill-timeout",
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await stopLocalDaemon({ home, force, timeoutMs });
|
||||
const result = await stopLocalDaemon({ home, force, timeoutMs, killTimeoutMs });
|
||||
return {
|
||||
type: "single",
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user