Stop local daemon when removing localhost (#1297)
* fix(app): stop local daemon when removing localhost * fix(app): handle daemon removal rollback * fix(app): return listen address in desktop e2e mock * fix(app): refresh daemon status after registration failure * fix(app): unslop daemon status error coverage * fix(app): preserve daemon toggle error context --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
9e2c75a8a3
commit
b5192e577a
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DaemonConnectionRegistrationError,
|
||||
DaemonManagementOperationError,
|
||||
getDaemonManagementErrorPresentation,
|
||||
} from "./daemon-management-error";
|
||||
|
||||
describe("getDaemonManagementErrorPresentation", () => {
|
||||
it("refreshes status when the daemon started but localhost registration failed", () => {
|
||||
const presentation = getDaemonManagementErrorPresentation(
|
||||
new DaemonConnectionRegistrationError("Desktop daemon did not return a listen address."),
|
||||
false,
|
||||
);
|
||||
|
||||
expect(presentation).toEqual({
|
||||
message:
|
||||
"Built-in daemon started, but Paseo could not save the localhost connection. Toggle daemon management off and on again, or add localhost manually.",
|
||||
refreshStatus: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not refresh status for daemon stop failures", () => {
|
||||
const presentation = getDaemonManagementErrorPresentation(new Error("stop failed"), true);
|
||||
|
||||
expect(presentation).toEqual({
|
||||
message: "Built-in daemon management was paused, but Paseo could not stop the daemon.",
|
||||
refreshStatus: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the pre-mutation daemon management state for operation failures", () => {
|
||||
const presentation = getDaemonManagementErrorPresentation(
|
||||
new DaemonManagementOperationError(new Error("stop failed"), true),
|
||||
false,
|
||||
);
|
||||
|
||||
expect(presentation).toEqual({
|
||||
message: "Built-in daemon management was paused, but Paseo could not stop the daemon.",
|
||||
refreshStatus: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not refresh status for generic update failures", () => {
|
||||
const presentation = getDaemonManagementErrorPresentation(new Error("settings failed"), false);
|
||||
|
||||
expect(presentation).toEqual({
|
||||
message: "Unable to update built-in daemon management.",
|
||||
refreshStatus: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
export class DaemonConnectionRegistrationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "DaemonConnectionRegistrationError";
|
||||
}
|
||||
}
|
||||
|
||||
export class DaemonManagementOperationError extends Error {
|
||||
readonly originalError: Error;
|
||||
readonly wasManagingDaemon: boolean;
|
||||
|
||||
constructor(error: Error, wasManagingDaemon: boolean) {
|
||||
super(error.message);
|
||||
this.name = error.name;
|
||||
this.cause = error;
|
||||
this.originalError = error;
|
||||
this.wasManagingDaemon = wasManagingDaemon;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DaemonManagementErrorPresentation {
|
||||
message: string;
|
||||
refreshStatus: boolean;
|
||||
}
|
||||
|
||||
export function getDaemonManagementErrorPresentation(
|
||||
error: Error,
|
||||
isManagingDaemon: boolean,
|
||||
): DaemonManagementErrorPresentation {
|
||||
const presentationError =
|
||||
error instanceof DaemonManagementOperationError ? error.originalError : error;
|
||||
const wasManagingDaemon =
|
||||
error instanceof DaemonManagementOperationError ? error.wasManagingDaemon : isManagingDaemon;
|
||||
|
||||
if (presentationError instanceof DaemonConnectionRegistrationError) {
|
||||
return {
|
||||
message:
|
||||
"Built-in daemon started, but Paseo could not save the localhost connection. Toggle daemon management off and on again, or add localhost manually.",
|
||||
refreshStatus: true,
|
||||
};
|
||||
}
|
||||
if (wasManagingDaemon) {
|
||||
return {
|
||||
message: "Built-in daemon management was paused, but Paseo could not stop the daemon.",
|
||||
refreshStatus: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: "Unable to update built-in daemon management.",
|
||||
refreshStatus: false,
|
||||
};
|
||||
}
|
||||
@@ -9,8 +9,15 @@ import {
|
||||
executeDaemonManagementToggle,
|
||||
type DaemonManagementToggleResult,
|
||||
} from "@/desktop/daemon/daemon-management-toggle";
|
||||
import {
|
||||
DaemonConnectionRegistrationError,
|
||||
DaemonManagementOperationError,
|
||||
getDaemonManagementErrorPresentation,
|
||||
} from "@/desktop/daemon/daemon-management-error";
|
||||
import { useDesktopIpcErrorReporter } from "@/desktop/hooks/desktop-ipc-error";
|
||||
import type { DesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
||||
import { upsertDesktopDaemonConnection } from "@/runtime/daemon-start-service";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
|
||||
type DesktopDaemonSettings = DesktopSettings["daemon"];
|
||||
@@ -37,27 +44,51 @@ export function useBuiltInDaemonManagement(
|
||||
DaemonManagementToggleResult,
|
||||
Error
|
||||
>({
|
||||
mutationFn: () =>
|
||||
executeDaemonManagementToggle(settings.manageBuiltInDaemon, daemonStatus, {
|
||||
confirm: () =>
|
||||
confirmDialog({
|
||||
title: "Pause built-in daemon",
|
||||
message:
|
||||
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
|
||||
confirmLabel: "Pause and stop",
|
||||
cancelLabel: "Cancel",
|
||||
destructive: true,
|
||||
}),
|
||||
persistSettings: (next) => updateSettings(next) as Promise<void>,
|
||||
startDaemon: startDesktopDaemon,
|
||||
stopDaemon: stopDesktopDaemon,
|
||||
}),
|
||||
mutationFn: async () => {
|
||||
const wasManagingDaemon = settings.manageBuiltInDaemon;
|
||||
try {
|
||||
const result = await executeDaemonManagementToggle(wasManagingDaemon, daemonStatus, {
|
||||
confirm: () =>
|
||||
confirmDialog({
|
||||
title: "Pause built-in daemon",
|
||||
message:
|
||||
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
|
||||
confirmLabel: "Pause and stop",
|
||||
cancelLabel: "Cancel",
|
||||
destructive: true,
|
||||
}),
|
||||
persistSettings: (next) => updateSettings(next) as Promise<void>,
|
||||
startDaemon: startDesktopDaemon,
|
||||
stopDaemon: stopDesktopDaemon,
|
||||
});
|
||||
if (result.kind === "enabled") {
|
||||
const upsertResult = await upsertDesktopDaemonConnection(
|
||||
getHostRuntimeStore(),
|
||||
result.newStatus,
|
||||
);
|
||||
if (!upsertResult.ok) {
|
||||
throw new DaemonConnectionRegistrationError(upsertResult.error);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DaemonManagementOperationError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
wasManagingDaemon,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
const presentation = getDaemonManagementErrorPresentation(
|
||||
error,
|
||||
settings.manageBuiltInDaemon,
|
||||
);
|
||||
if (presentation.refreshStatus) {
|
||||
refreshStatus();
|
||||
}
|
||||
reportError({
|
||||
error,
|
||||
message: settings.manageBuiltInDaemon
|
||||
? "Built-in daemon management was paused, but Paseo could not stop the daemon."
|
||||
: "Unable to update built-in daemon management.",
|
||||
message: presentation.message,
|
||||
logLabel: "[Settings] Failed to update built-in daemon management",
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user