mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 20:22:45 +00:00
Show Open in editor for a password-protected desktop daemon (#5335)
* fix(daemon): report the local daemon's server id when a password is set Since #4575, `paseo daemon status` only learned the server id from an authenticated connection. The desktop app runs that command without the daemon password, so a password-protected desktop daemon reported no server id, the app treated it as remote, and hid Open in editor. The worker now announces its server id with readiness and the supervisor publishes it in the instance record next to the listen address, so status reports the running daemon's identity from local state. Closes #5300 * fix(cli): omit serverId from status until a daemon publishes one * chore: rerun CI after macOS file observation flake --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import net from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { hashDaemonPassword } from "@getpaseo/server/auth";
|
||||
import { startDaemonInstance, readDaemonInstance } from "@getpaseo/server/daemon-control";
|
||||
import { expect, test } from "vitest";
|
||||
import { connectToDaemon } from "../../utils/client.js";
|
||||
@@ -196,6 +197,26 @@ test("removed flags and ambiguous targets fail before side effects; observation
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
test("status reports the server id of a password-protected daemon to a caller without the password", async () => {
|
||||
const f = await fixture();
|
||||
const home = f.homes[0]!;
|
||||
try {
|
||||
await f.configure(home, `127.0.0.1:${await port()}`);
|
||||
const configPath = path.join(home, "config.json");
|
||||
const config = JSON.parse(await readFile(configPath, "utf8"));
|
||||
config.daemon.auth = { password: await hashDaemonPassword("secret") };
|
||||
await writeFile(configPath, JSON.stringify(config));
|
||||
await f.ok(["start", "--home", home, "--timeout", "30"]);
|
||||
const authenticated = await f.liveStatus(home, { PASEO_PASSWORD: "secret" });
|
||||
expect(await f.ok(["daemon", "status", "--home", home])).toMatchObject({
|
||||
connectedDaemon: "auth_required",
|
||||
serverId: authenticated.serverId,
|
||||
});
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
test("an occupied initial or replacement address fails without false readiness or killing its owner", async () => {
|
||||
const f = await fixture();
|
||||
const [a, b] = f.homes as [string, string];
|
||||
|
||||
@@ -124,6 +124,7 @@ function localStatus(home: string, instance: Awaited<ReturnType<typeof readDaemo
|
||||
pid: instance?.pid ?? null,
|
||||
startedAt: instance?.startedAt ?? null,
|
||||
listen: instance?.listen ?? null,
|
||||
serverId: instance?.serverId ?? undefined,
|
||||
hostname: instance?.hostname ?? null,
|
||||
configuredListen: config.listen,
|
||||
localDaemon,
|
||||
|
||||
@@ -172,10 +172,11 @@ async function main(): Promise<void> {
|
||||
: undefined,
|
||||
restartOnCrash: true,
|
||||
logFile: supervisorLogFile,
|
||||
onWorkerReady: async ({ listen }) => {
|
||||
await updatePidLock(paseoHome, { listen }, { ownerPid: process.pid });
|
||||
onWorkerReady: async ({ listen, serverId }) => {
|
||||
await updatePidLock(paseoHome, { listen, serverId }, { ownerPid: process.pid });
|
||||
},
|
||||
onWorkerExit: () => updatePidLock(paseoHome, { listen: null }, { ownerPid: process.pid }),
|
||||
onWorkerExit: () =>
|
||||
updatePidLock(paseoHome, { listen: null, serverId: null }, { ownerPid: process.pid }),
|
||||
onSupervisorExit: releaseLock,
|
||||
});
|
||||
requestSupervisorShutdown = supervisor.requestShutdown;
|
||||
|
||||
@@ -38,7 +38,7 @@ async function runSupervisorFixture(options: {
|
||||
|
||||
await writeFile(
|
||||
workerPath,
|
||||
`process.send?.({ type: "paseo:ready", listen: "fixture" });\n${options.workerSource}`,
|
||||
`process.send?.({ type: "paseo:ready", listen: "fixture", serverId: "srv_fixture" });\n${options.workerSource}`,
|
||||
);
|
||||
await writeFile(
|
||||
runnerPath,
|
||||
|
||||
@@ -92,7 +92,7 @@ test("a replacement worker must independently reach ready", async () => {
|
||||
process.on("message", message => {
|
||||
if (message.type === "paseo:graceful-shutdown") process.exit(0);
|
||||
});
|
||||
process.send({ type: "paseo:ready", listen: "test-endpoint" });
|
||||
process.send({ type: "paseo:ready", listen: "test-endpoint", serverId: "srv_test" });
|
||||
process.send({ type: "paseo:restart" });
|
||||
`);
|
||||
expect(result.code).toBe(1);
|
||||
@@ -102,7 +102,7 @@ test("a replacement worker must independently reach ready", async () => {
|
||||
test("exit clears publication after an in-flight ready write", async () => {
|
||||
const result = await runWorker(
|
||||
`
|
||||
process.send({ type: "paseo:ready", listen: "test-endpoint" }, () => process.exit(0));
|
||||
process.send({ type: "paseo:ready", listen: "test-endpoint", serverId: "srv_test" }, () => process.exit(0));
|
||||
`,
|
||||
100,
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ type WorkerLifecycleMessage =
|
||||
| {
|
||||
type: "paseo:ready";
|
||||
listen: string;
|
||||
serverId: string;
|
||||
}
|
||||
| {
|
||||
type: "paseo:restart";
|
||||
@@ -50,7 +51,7 @@ interface SupervisorOptions {
|
||||
args: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
} | null;
|
||||
onWorkerReady?: (message: { listen: string }) => Promise<void> | void;
|
||||
onWorkerReady?: (message: { listen: string; serverId: string }) => Promise<void> | void;
|
||||
onWorkerExit?: () => Promise<void> | void;
|
||||
restartOnCrash?: boolean;
|
||||
onSupervisorExit?: () => Promise<void> | void;
|
||||
@@ -78,11 +79,14 @@ function parseLifecycleMessage(msg: unknown): WorkerLifecycleMessage | null {
|
||||
};
|
||||
}
|
||||
if (type === "paseo:ready") {
|
||||
const listen = (msg as { listen?: unknown }).listen;
|
||||
const { listen, serverId } = msg as { listen?: unknown; serverId?: unknown };
|
||||
if (typeof listen !== "string" || listen.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return { type: "paseo:ready", listen };
|
||||
if (typeof serverId !== "string" || serverId.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return { type: "paseo:ready", listen, serverId };
|
||||
}
|
||||
if (type === "paseo:restart") {
|
||||
const reason = (msg as { reason?: unknown }).reason;
|
||||
@@ -289,7 +293,12 @@ export function runSupervisor(options: SupervisorOptions): SupervisorController
|
||||
reachedReady = true;
|
||||
writeLifecycleLog("Worker ready", { listen: lifecycleMessage.listen });
|
||||
publication = publication
|
||||
.then(() => options.onWorkerReady?.({ listen: lifecycleMessage.listen }))
|
||||
.then(() =>
|
||||
options.onWorkerReady?.({
|
||||
listen: lifecycleMessage.listen,
|
||||
serverId: lifecycleMessage.serverId,
|
||||
}),
|
||||
)
|
||||
.catch((error) => {
|
||||
lifecycleFailed = true;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -471,6 +471,7 @@ export interface PaseoDaemon {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
getListenTarget(): ListenTarget | null;
|
||||
getServerId(): string;
|
||||
}
|
||||
|
||||
export interface PaseoDaemonDependencies {
|
||||
@@ -1831,6 +1832,7 @@ export async function createPaseoDaemon(
|
||||
start,
|
||||
stop,
|
||||
getListenTarget: () => boundListenTarget,
|
||||
getServerId: () => serverId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ type SupervisorLifecycleMessage =
|
||||
| {
|
||||
type: "paseo:ready";
|
||||
listen: string;
|
||||
serverId: string;
|
||||
}
|
||||
| {
|
||||
type: "paseo:restart";
|
||||
@@ -327,7 +328,11 @@ async function main() {
|
||||
if (!listen) {
|
||||
throw new Error("Daemon did not expose a listen target after startup");
|
||||
}
|
||||
sendSupervisorLifecycleMessage({ type: "paseo:ready", listen });
|
||||
sendSupervisorLifecycleMessage({
|
||||
type: "paseo:ready",
|
||||
listen,
|
||||
serverId: daemon.getServerId(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.fatal({ err }, "Daemon failed to start listening");
|
||||
throw err;
|
||||
|
||||
@@ -11,6 +11,7 @@ export const pidLockInfoSchema = z.object({
|
||||
hostname: z.string(),
|
||||
uid: z.number(),
|
||||
listen: z.string().nullable(),
|
||||
serverId: z.string().nullable().optional(),
|
||||
desktopManaged: z.boolean().optional(),
|
||||
heartbeat: z.literal(true).optional(),
|
||||
});
|
||||
@@ -319,7 +320,7 @@ export function startPidLockHeartbeat(
|
||||
|
||||
export async function updatePidLock(
|
||||
paseoHome: string,
|
||||
patch: { listen: string | null },
|
||||
patch: { listen: string; serverId: string } | { listen: null; serverId: null },
|
||||
options?: { ownerPid?: number },
|
||||
): Promise<void> {
|
||||
const pidPath = getPidFilePath(paseoHome);
|
||||
|
||||
Reference in New Issue
Block a user