fix(migrate): validate live source state

This commit is contained in:
Mohamed Boudra
2026-07-19 02:12:29 +02:00
parent af244d0f02
commit 4ed944db2f
5 changed files with 70 additions and 4 deletions
@@ -34,6 +34,7 @@ export function useMigrationAvailability(source: string) {
const [visible, setVisible] = useState(false);
useEffect(() => {
let active = true;
let retry: ReturnType<typeof setTimeout> | null = null;
async function loadAvailability() {
const next = await getDesktopHost()?.migrations?.getAvailability?.({ source });
if (active && next) {
@@ -43,11 +44,15 @@ export function useMigrationAvailability(source: string) {
? t(`desktop.integrations.migration.availability.${next.reason}`)
: null,
});
if (next.reason === "host-not-running") {
retry = setTimeout(() => void loadAvailability(), 2_000);
}
}
}
void loadAvailability();
return () => {
active = false;
if (retry) clearTimeout(retry);
};
}, [source, t]);
return {
+30 -3
View File
@@ -19,7 +19,10 @@ test("discovers the configured local socket before TCP fallback", () => {
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-node-client-"));
cleanup.push(paseoHome);
mkdirSync(paseoHome, { recursive: true });
writeFileSync(path.join(paseoHome, "paseo.pid"), JSON.stringify({ listen: "/tmp/paseo.sock" }));
writeFileSync(
path.join(paseoHome, "paseo.pid"),
JSON.stringify({ pid: process.pid, listen: "/tmp/paseo.sock" }),
);
writeFileSync(
path.join(paseoHome, "config.json"),
JSON.stringify({ daemon: { listen: "127.0.0.1:7777" } }),
@@ -35,7 +38,10 @@ test("discovers the configured local socket before TCP fallback", () => {
test("discovers the running daemon TCP address from its PID record", () => {
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-node-client-pid-tcp-"));
cleanup.push(paseoHome);
writeFileSync(path.join(paseoHome, "paseo.pid"), JSON.stringify({ listen: "127.0.0.1:7789" }));
writeFileSync(
path.join(paseoHome, "paseo.pid"),
JSON.stringify({ pid: process.pid, listen: "127.0.0.1:7789" }),
);
expect(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome })).toEqual([
"127.0.0.1:7789",
@@ -46,7 +52,10 @@ test("discovers the running daemon TCP address from its PID record", () => {
test("normalizes the daemon's bare IPv6 loopback PID address", () => {
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-node-client-pid-ipv6-"));
cleanup.push(paseoHome);
writeFileSync(path.join(paseoHome, "paseo.pid"), JSON.stringify({ listen: "::1:7789" }));
writeFileSync(
path.join(paseoHome, "paseo.pid"),
JSON.stringify({ pid: process.pid, listen: "::1:7789" }),
);
expect(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome })).toEqual([
"[::1]:7789",
@@ -102,3 +111,21 @@ test("skips malformed discovered candidates and keeps the default fallback", ()
resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome, PASEO_LISTEN: "tcp://missing-port" }),
).toEqual(["localhost:6767"]);
});
test("ignores a stale PID listener before the configured daemon", () => {
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "paseo-node-client-stale-pid-"));
cleanup.push(paseoHome);
writeFileSync(
path.join(paseoHome, "paseo.pid"),
JSON.stringify({ pid: 2_147_483_647, listen: "127.0.0.1:7789" }),
);
writeFileSync(
path.join(paseoHome, "config.json"),
JSON.stringify({ daemon: { listen: "127.0.0.1:7777" } }),
);
expect(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome })).toEqual([
"127.0.0.1:7777",
"localhost:6767",
]);
});
+17
View File
@@ -111,9 +111,17 @@ function readPidListen(paseoHome: string): string | null {
if (!existsSync(pidPath)) return null;
try {
const parsed = JSON.parse(readFileSync(pidPath, "utf8")) as {
pid?: unknown;
listen?: unknown;
sockPath?: unknown;
};
if (
typeof parsed.pid !== "number" ||
!Number.isInteger(parsed.pid) ||
!isProcessRunning(parsed.pid)
) {
return null;
}
if (typeof parsed.listen === "string") return parsed.listen;
return typeof parsed.sockPath === "string" ? parsed.sockPath : null;
} catch {
@@ -121,6 +129,15 @@ function readPidListen(paseoHome: string): string | null {
}
}
function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
}
}
export function resolveDefaultDaemonHosts(env: NodeJS.ProcessEnv = process.env): string[] {
const paseoHome = resolvePaseoHome(env);
const direct = normalizeDaemonHost(env.PASEO_LISTEN ?? "");
@@ -156,6 +156,7 @@ test("preserves literal or escaped Conductor names and rewrites active variables
escaped: { command: "printf \\$CONDUCTOR_PORT" },
escapedDouble: { command: 'printf "\\$CONDUCTOR_PORT"' },
bare: { command: "printf CONDUCTOR_PORT" },
heredoc: { command: "cat <<'EOF'\n$CONDUCTOR_PORT\nEOF" },
active: { command: "printf \"'$CONDUCTOR_PORT'\"" },
},
},
@@ -170,7 +171,11 @@ test("preserves literal or escaped Conductor names and rewrites active variables
escapedDouble: { command: 'printf "\\$CONDUCTOR_PORT"' },
},
});
expect(inspected.notices).toEqual([]);
expect(inspected.notices).toContainEqual({
code: "conductor-setting-unsupported",
level: "warning",
message: "scripts.heredoc: Here-document commands are not imported.",
});
});
test("skips cwd scripts on Windows instead of emitting POSIX shell syntax", () => {
@@ -369,6 +369,10 @@ function rewriteExactCommand(
notices: MigrationNotice[],
platform: NodeJS.Platform,
): string | null {
if (containsActiveHereDocument(command)) {
notices.push(unsupportedSetting(key, "Here-document commands are not imported."));
return null;
}
const sourceVariables = collectConductorVariables(command);
if (platform === "win32" && sourceVariables.length > 0) {
notices.push(
@@ -714,6 +718,14 @@ function forEachActiveArithmetic(command: string, visit: (body: string) => void)
}
}
function containsActiveHereDocument(command: string): boolean {
const mask = activeShellMask(command);
for (const match of command.matchAll(/<<-?/g)) {
if (mask.slice(match.index, match.index + match[0].length) === match[0]) return true;
}
return false;
}
function appendArgs(command: string, args: string[]): string {
return args.length === 0 ? command : `${command} ${args.map(shellQuoteArgument).join(" ")}`;
}