fix: stop logging pairing offer during daemon startup

This commit is contained in:
Mohamed Boudra
2026-04-14 19:13:50 +07:00
parent 345729c588
commit 380c9927d7
5 changed files with 140 additions and 91 deletions
+50 -38
View File
@@ -1,4 +1,4 @@
import { spawn, type ChildProcess, execSync } from "node:child_process"; import { spawn, type ChildProcess, execFileSync, execSync } from "node:child_process";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
@@ -224,6 +224,51 @@ function decodeOfferFromFragmentUrl(url: string): OfferPayload {
return offer as OfferPayload; return offer as OfferPayload;
} }
function loadPairingOfferFromCli(repoRoot: string, paseoHomePath: string): OfferPayload {
const stdout = execFileSync(
process.execPath,
["--import", "tsx", "packages/cli/src/index.ts", "daemon", "pair", "--json"],
{
cwd: repoRoot,
env: {
...process.env,
PASEO_HOME: paseoHomePath,
},
encoding: "utf8",
},
);
const payload = JSON.parse(stdout) as { relayEnabled?: boolean; url?: string | null };
if (payload.relayEnabled !== true || typeof payload.url !== "string") {
throw new Error(`Unexpected daemon pair response: ${stdout}`);
}
return decodeOfferFromFragmentUrl(payload.url);
}
async function waitForPairingOfferFromCli(args: {
repoRoot: string;
paseoHome: string;
timeoutMs?: number;
}): Promise<OfferPayload> {
const timeoutMs = args.timeoutMs ?? 15000;
const start = Date.now();
let lastError: unknown = null;
while (Date.now() - start < timeoutMs) {
try {
return loadPairingOfferFromCli(args.repoRoot, args.paseoHome);
} catch (error) {
lastError = error;
await sleep(100);
}
}
throw new Error(
`Timed out waiting for \`paseo daemon pair --json\` to produce a pairing offer: ${
lastError instanceof Error ? lastError.message : String(lastError)
}`,
);
}
export default async function globalSetup() { export default async function globalSetup() {
const repoRoot = path.resolve(__dirname, "../../.."); const repoRoot = path.resolve(__dirname, "../../..");
ensureRelayBuildArtifact(repoRoot); ensureRelayBuildArtifact(repoRoot);
@@ -433,12 +478,6 @@ export default async function globalSetup() {
const serverDir = path.resolve(__dirname, "../../..", "packages/server"); const serverDir = path.resolve(__dirname, "../../..", "packages/server");
const tsxBin = execSync("which tsx").toString().trim(); const tsxBin = execSync("which tsx").toString().trim();
let offerPayload: OfferPayload | null = null;
let offerResolve: (() => void) | null = null;
const offerPromise = new Promise<void>((resolve) => {
offerResolve = resolve;
});
daemonProcess = spawn(tsxBin, ["src/server/index.ts"], { daemonProcess = spawn(tsxBin, ["src/server/index.ts"], {
cwd: serverDir, cwd: serverDir,
env: { env: {
@@ -473,26 +512,6 @@ export default async function globalSetup() {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed) continue; if (!trimmed) continue;
daemonLineBuffer.add(`[stdout] ${trimmed}`); daemonLineBuffer.add(`[stdout] ${trimmed}`);
if (!offerPayload) {
const clean = stripAnsi(trimmed);
try {
const obj = JSON.parse(clean) as { msg?: string; url?: string };
if (obj.msg === "pairing_offer" && typeof obj.url === "string") {
offerPayload = decodeOfferFromFragmentUrl(obj.url);
offerResolve?.();
}
} catch {
const match = clean.match(/https?:\/\/[^\s"]+#offer=[A-Za-z0-9_-]+/);
if (match && clean.includes("pairing_offer")) {
try {
offerPayload = decodeOfferFromFragmentUrl(match[0]);
offerResolve?.();
} catch {
// ignore parsing failures
}
}
}
}
console.log(`[daemon] ${trimmed}`); console.log(`[daemon] ${trimmed}`);
} }
}); });
@@ -523,17 +542,10 @@ export default async function globalSetup() {
}), }),
]); ]);
// Wait for daemon to emit a pairing offer (includes relay session ID). const offer = await waitForPairingOfferFromCli({
await Promise.race([ repoRoot,
offerPromise, paseoHome,
new Promise((_, reject) => });
setTimeout(() => reject(new Error("Timed out waiting for pairing_offer log")), 15000),
),
]);
if (!offerPayload) {
throw new Error("pairing_offer was not parsed from daemon logs");
}
const offer = offerPayload as OfferPayload;
process.env.E2E_DAEMON_PORT = String(port); process.env.E2E_DAEMON_PORT = String(port);
process.env.E2E_RELAY_PORT = String(relayPort); process.env.E2E_RELAY_PORT = String(relayPort);
@@ -1,11 +1,11 @@
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { Writable } from "node:stream";
import pino from "pino"; import pino from "pino";
import { afterEach, describe, expect, test, vi } from "vitest"; import { afterEach, describe, expect, test, vi } from "vitest";
import { createPaseoDaemon, parseListenString, type PaseoDaemonConfig } from "./bootstrap.js"; import { createPaseoDaemon, parseListenString, type PaseoDaemonConfig } from "./bootstrap.js";
import { generateLocalPairingOffer } from "./pairing-offer.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js"; import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { createTestAgentClients } from "./test-utils/fake-agent-client.js"; import { createTestAgentClients } from "./test-utils/fake-agent-client.js";
@@ -150,24 +150,14 @@ describe("paseo daemon bootstrap", () => {
}); });
}); });
test("emits a relay pairing offer for unix socket listeners", async () => { test("generates a relay pairing offer for unix socket listeners", async () => {
const paseoHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-socket-relay-")); const paseoHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-socket-relay-"));
const paseoHome = path.join(paseoHomeRoot, ".paseo"); const paseoHome = path.join(paseoHomeRoot, ".paseo");
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
const socketPath = path.join(paseoHomeRoot, "run", "paseo.sock"); const socketPath = path.join(paseoHomeRoot, "run", "paseo.sock");
await mkdir(path.dirname(socketPath), { recursive: true }); await mkdir(path.dirname(socketPath), { recursive: true });
await mkdir(paseoHome, { recursive: true }); await mkdir(paseoHome, { recursive: true });
const logger = pino({ level: "silent" });
const lines: string[] = [];
const logger = pino(
{ level: "info" },
new Writable({
write(chunk, _encoding, callback) {
lines.push(chunk.toString("utf8"));
callback();
},
}),
);
const config: PaseoDaemonConfig = { const config: PaseoDaemonConfig = {
listen: socketPath, listen: socketPath,
@@ -191,7 +181,16 @@ describe("paseo daemon bootstrap", () => {
try { try {
await daemon.start(); await daemon.start();
expect(lines.some((line) => line.includes('"msg":"pairing_offer"'))).toBe(true); const pairing = await generateLocalPairingOffer({
paseoHome,
relayEnabled: true,
relayEndpoint: "127.0.0.1:9",
relayPublicEndpoint: "127.0.0.1:9",
appBaseUrl: "https://app.paseo.sh",
includeQr: false,
});
expect(pairing.relayEnabled).toBe(true);
expect(pairing.url?.startsWith("https://app.paseo.sh/#offer=")).toBe(true);
} finally { } finally {
await daemon.stop().catch(() => undefined); await daemon.stop().catch(() => undefined);
await daemon.agentManager.flush().catch(() => undefined); await daemon.agentManager.flush().catch(() => undefined);
+1 -2
View File
@@ -646,8 +646,7 @@ export async function createPaseoDaemon(
relay: { endpoint: relayPublicEndpoint }, relay: { endpoint: relayPublicEndpoint },
}); });
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl }); encodeOfferToFragmentUrl({ offer, appBaseUrl });
logger.info({ url }, "pairing_offer");
relayTransport?.stop().catch(() => undefined); relayTransport?.stop().catch(() => undefined);
relayTransport = startRelayTransport({ relayTransport = startRelayTransport({
@@ -8,6 +8,7 @@ import { Writable } from "node:stream";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { generateLocalPairingOffer } from "../pairing-offer.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
function createCapturingLogger() { function createCapturingLogger() {
@@ -22,18 +23,25 @@ function createCapturingLogger() {
return { logger, lines }; return { logger, lines };
} }
function parseOfferUrlFromLogs(lines: string[]): string { async function getPairingOfferUrl(args: {
for (const line of lines) { paseoHome: string;
try { relayEnabled?: boolean;
const obj = JSON.parse(line) as { msg?: string; url?: string }; relayEndpoint?: string;
if (obj.msg === "pairing_offer" && typeof obj.url === "string") { relayPublicEndpoint?: string;
return obj.url; appBaseUrl?: string;
} }): Promise<string> {
} catch { const pairing = await generateLocalPairingOffer({
// ignore non-JSON lines paseoHome: args.paseoHome,
} relayEnabled: args.relayEnabled,
relayEndpoint: args.relayEndpoint,
relayPublicEndpoint: args.relayPublicEndpoint,
appBaseUrl: args.appBaseUrl,
includeQr: false,
});
if (!pairing.url) {
throw new Error("Expected relay pairing URL to be available");
} }
throw new Error(`pairing_offer log not found. saw ${lines.length} lines`); return pairing.url;
} }
function decodeOfferFromFragmentUrl(url: string): unknown { function decodeOfferFromFragmentUrl(url: string): unknown {
@@ -72,7 +80,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
test("emits relay-only offer URL with stable serverId", async () => { test("emits relay-only offer URL with stable serverId", async () => {
process.env.PASEO_PRIMARY_LAN_IP = "192.168.1.12"; process.env.PASEO_PRIMARY_LAN_IP = "192.168.1.12";
const { logger, lines } = createCapturingLogger(); const { logger } = createCapturingLogger();
const daemon = await createTestPaseoDaemon({ const daemon = await createTestPaseoDaemon({
listen: "0.0.0.0", listen: "0.0.0.0",
@@ -81,7 +89,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
}); });
try { try {
const offerUrl = parseOfferUrlFromLogs(lines); const offerUrl = await getPairingOfferUrl({
paseoHome: daemon.paseoHome,
relayEnabled: daemon.config.relayEnabled,
relayEndpoint: daemon.config.relayEndpoint,
relayPublicEndpoint: daemon.config.relayPublicEndpoint,
appBaseUrl: daemon.config.appBaseUrl,
});
expect(offerUrl.startsWith("https://app.paseo.sh/#offer=")).toBe(true); expect(offerUrl.startsWith("https://app.paseo.sh/#offer=")).toBe(true);
const offer = decodeOfferFromFragmentUrl(offerUrl) as { const offer = decodeOfferFromFragmentUrl(offerUrl) as {
@@ -111,7 +125,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
const tempHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-offer-home-")); const tempHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-offer-home-"));
const { logger: logger1, lines: lines1 } = createCapturingLogger(); const { logger: logger1 } = createCapturingLogger();
const daemon1 = await createTestPaseoDaemon({ const daemon1 = await createTestPaseoDaemon({
listen: "0.0.0.0", listen: "0.0.0.0",
logger: logger1, logger: logger1,
@@ -124,7 +138,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
let staticDir2: string | null = null; let staticDir2: string | null = null;
try { try {
const offerUrl1 = parseOfferUrlFromLogs(lines1); const offerUrl1 = await getPairingOfferUrl({
paseoHome: daemon1.paseoHome,
relayEnabled: daemon1.config.relayEnabled,
relayEndpoint: daemon1.config.relayEndpoint,
relayPublicEndpoint: daemon1.config.relayPublicEndpoint,
appBaseUrl: daemon1.config.appBaseUrl,
});
const offer1 = decodeOfferFromFragmentUrl(offerUrl1) as { const offer1 = decodeOfferFromFragmentUrl(offerUrl1) as {
serverId: string; serverId: string;
daemonPublicKeyB64: string; daemonPublicKeyB64: string;
@@ -133,7 +153,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
await daemon1.close(); await daemon1.close();
const { logger: logger2, lines: lines2 } = createCapturingLogger(); const { logger: logger2 } = createCapturingLogger();
const daemon2 = await createTestPaseoDaemon({ const daemon2 = await createTestPaseoDaemon({
listen: "0.0.0.0", listen: "0.0.0.0",
logger: logger2, logger: logger2,
@@ -144,7 +164,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
staticDir2 = daemon2.staticDir; staticDir2 = daemon2.staticDir;
try { try {
const offerUrl2 = parseOfferUrlFromLogs(lines2); const offerUrl2 = await getPairingOfferUrl({
paseoHome: daemon2.paseoHome,
relayEnabled: daemon2.config.relayEnabled,
relayEndpoint: daemon2.config.relayEndpoint,
relayPublicEndpoint: daemon2.config.relayPublicEndpoint,
appBaseUrl: daemon2.config.appBaseUrl,
});
const offer2 = decodeOfferFromFragmentUrl(offerUrl2) as { const offer2 = decodeOfferFromFragmentUrl(offerUrl2) as {
serverId: string; serverId: string;
daemonPublicKeyB64: string; daemonPublicKeyB64: string;
@@ -207,12 +233,6 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
stdoutLines.push(text); stdoutLines.push(text);
for (const line of text.split("\n")) { for (const line of text.split("\n")) {
if (!line.trim()) continue; if (!line.trim()) continue;
if (line.includes("pairing_offer")) {
clearTimeout(timeout);
reject(new Error("unexpected pairing_offer log when --no-relay is set"));
return;
}
try { try {
const parsed = JSON.parse(line) as { msg?: string }; const parsed = JSON.parse(line) as { msg?: string };
if (parsed.msg !== `Server listening on http://0.0.0.0:${port}`) continue; if (parsed.msg !== `Server listening on http://0.0.0.0:${port}`) continue;
@@ -7,6 +7,7 @@ import path from "node:path";
import { spawn, type ChildProcess } from "node:child_process"; import { spawn, type ChildProcess } from "node:child_process";
import { Buffer } from "node:buffer"; import { Buffer } from "node:buffer";
import { generateLocalPairingOffer } from "../pairing-offer.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee"; import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee";
import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js"; import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js";
@@ -26,19 +27,25 @@ function createCapturingLogger() {
return { logger, lines }; return { logger, lines };
} }
function parseOfferUrlFromLogs(lines: string[]): string { async function getPairingOfferUrl(args: {
for (const line of lines) { paseoHome: string;
if (!line.includes("pairing_offer")) continue; relayEnabled?: boolean;
try { relayEndpoint?: string;
const obj = JSON.parse(line) as { msg?: string; url?: string }; relayPublicEndpoint?: string;
if (obj.msg === "pairing_offer" && typeof obj.url === "string") { appBaseUrl?: string;
return obj.url; }): Promise<string> {
} const pairing = await generateLocalPairingOffer({
} catch { paseoHome: args.paseoHome,
// ignore relayEnabled: args.relayEnabled,
} relayEndpoint: args.relayEndpoint,
relayPublicEndpoint: args.relayPublicEndpoint,
appBaseUrl: args.appBaseUrl,
includeQr: false,
});
if (!pairing.url) {
throw new Error("Expected relay pairing URL to be available");
} }
throw new Error(`pairing_offer log not found. saw ${lines.length} lines`); return pairing.url;
} }
function decodeOfferFromFragmentUrl(url: string): { function decodeOfferFromFragmentUrl(url: string): {
@@ -199,7 +206,13 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
}); });
try { try {
const offerUrl = parseOfferUrlFromLogs(lines); const offerUrl = await getPairingOfferUrl({
paseoHome: daemon.paseoHome,
relayEnabled: daemon.config.relayEnabled,
relayEndpoint: daemon.config.relayEndpoint,
relayPublicEndpoint: daemon.config.relayPublicEndpoint,
appBaseUrl: daemon.config.appBaseUrl,
});
const { serverId, daemonPublicKeyB64 } = decodeOfferFromFragmentUrl(offerUrl); const { serverId, daemonPublicKeyB64 } = decodeOfferFromFragmentUrl(offerUrl);
const stableClientId = `cid_test_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`; const stableClientId = `cid_test_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
@@ -314,7 +327,13 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
}); });
try { try {
const offerUrl = parseOfferUrlFromLogs(lines); const offerUrl = await getPairingOfferUrl({
paseoHome: daemon.paseoHome,
relayEnabled: daemon.config.relayEnabled,
relayEndpoint: daemon.config.relayEndpoint,
relayPublicEndpoint: daemon.config.relayPublicEndpoint,
appBaseUrl: daemon.config.appBaseUrl,
});
const { serverId, daemonPublicKeyB64 } = decodeOfferFromFragmentUrl(offerUrl); const { serverId, daemonPublicKeyB64 } = decodeOfferFromFragmentUrl(offerUrl);
// Previously, the daemon would time out waiting for `hello` and reconnect every ~10s. // Previously, the daemon would time out waiting for `hello` and reconnect every ~10s.