fix: stop logging pairing offer during daemon startup

This commit is contained in:
Mohamed Boudra
2026-04-14 19:12:13 +07:00
parent 345729c588
commit c67691e1d0
5 changed files with 140 additions and 91 deletions

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 { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
@@ -224,6 +224,51 @@ function decodeOfferFromFragmentUrl(url: string): 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() {
const repoRoot = path.resolve(__dirname, "../../..");
ensureRelayBuildArtifact(repoRoot);
@@ -433,12 +478,6 @@ export default async function globalSetup() {
const serverDir = path.resolve(__dirname, "../../..", "packages/server");
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"], {
cwd: serverDir,
env: {
@@ -473,26 +512,6 @@ export default async function globalSetup() {
const trimmed = line.trim();
if (!trimmed) continue;
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}`);
}
});
@@ -523,17 +542,10 @@ export default async function globalSetup() {
}),
]);
// Wait for daemon to emit a pairing offer (includes relay session ID).
await Promise.race([
offerPromise,
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;
const offer = await waitForPairingOfferFromCli({
repoRoot,
paseoHome,
});
process.env.E2E_DAEMON_PORT = String(port);
process.env.E2E_RELAY_PORT = String(relayPort);

View File

@@ -1,11 +1,11 @@
import os from "node:os";
import path from "node:path";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { Writable } from "node:stream";
import pino from "pino";
import { afterEach, describe, expect, test, vi } from "vitest";
import { createPaseoDaemon, parseListenString, type PaseoDaemonConfig } from "./bootstrap.js";
import { generateLocalPairingOffer } from "./pairing-offer.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.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 paseoHome = path.join(paseoHomeRoot, ".paseo");
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
const socketPath = path.join(paseoHomeRoot, "run", "paseo.sock");
await mkdir(path.dirname(socketPath), { recursive: true });
await mkdir(paseoHome, { recursive: true });
const lines: string[] = [];
const logger = pino(
{ level: "info" },
new Writable({
write(chunk, _encoding, callback) {
lines.push(chunk.toString("utf8"));
callback();
},
}),
);
const logger = pino({ level: "silent" });
const config: PaseoDaemonConfig = {
listen: socketPath,
@@ -191,7 +181,16 @@ describe("paseo daemon bootstrap", () => {
try {
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 {
await daemon.stop().catch(() => undefined);
await daemon.agentManager.flush().catch(() => undefined);

View File

@@ -646,8 +646,7 @@ export async function createPaseoDaemon(
relay: { endpoint: relayPublicEndpoint },
});
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
logger.info({ url }, "pairing_offer");
encodeOfferToFragmentUrl({ offer, appBaseUrl });
relayTransport?.stop().catch(() => undefined);
relayTransport = startRelayTransport({

View File

@@ -8,6 +8,7 @@ import { Writable } from "node:stream";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { generateLocalPairingOffer } from "../pairing-offer.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
function createCapturingLogger() {
@@ -22,18 +23,25 @@ function createCapturingLogger() {
return { logger, lines };
}
function parseOfferUrlFromLogs(lines: string[]): string {
for (const line of lines) {
try {
const obj = JSON.parse(line) as { msg?: string; url?: string };
if (obj.msg === "pairing_offer" && typeof obj.url === "string") {
return obj.url;
}
} catch {
// ignore non-JSON lines
}
async function getPairingOfferUrl(args: {
paseoHome: string;
relayEnabled?: boolean;
relayEndpoint?: string;
relayPublicEndpoint?: string;
appBaseUrl?: string;
}): Promise<string> {
const pairing = await generateLocalPairingOffer({
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 {
@@ -72,7 +80,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
test("emits relay-only offer URL with stable serverId", async () => {
process.env.PASEO_PRIMARY_LAN_IP = "192.168.1.12";
const { logger, lines } = createCapturingLogger();
const { logger } = createCapturingLogger();
const daemon = await createTestPaseoDaemon({
listen: "0.0.0.0",
@@ -81,7 +89,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
});
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);
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 { logger: logger1, lines: lines1 } = createCapturingLogger();
const { logger: logger1 } = createCapturingLogger();
const daemon1 = await createTestPaseoDaemon({
listen: "0.0.0.0",
logger: logger1,
@@ -124,7 +138,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
let staticDir2: string | null = null;
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 {
serverId: string;
daemonPublicKeyB64: string;
@@ -133,7 +153,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
await daemon1.close();
const { logger: logger2, lines: lines2 } = createCapturingLogger();
const { logger: logger2 } = createCapturingLogger();
const daemon2 = await createTestPaseoDaemon({
listen: "0.0.0.0",
logger: logger2,
@@ -144,7 +164,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
staticDir2 = daemon2.staticDir;
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 {
serverId: string;
daemonPublicKeyB64: string;
@@ -207,12 +233,6 @@ describe("ConnectionOfferV2 (daemon E2E)", () => {
stdoutLines.push(text);
for (const line of text.split("\n")) {
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 {
const parsed = JSON.parse(line) as { msg?: string };
if (parsed.msg !== `Server listening on http://0.0.0.0:${port}`) continue;

View File

@@ -7,6 +7,7 @@ import path from "node:path";
import { spawn, type ChildProcess } from "node:child_process";
import { Buffer } from "node:buffer";
import { generateLocalPairingOffer } from "../pairing-offer.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee";
import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js";
@@ -26,19 +27,25 @@ function createCapturingLogger() {
return { logger, lines };
}
function parseOfferUrlFromLogs(lines: string[]): string {
for (const line of lines) {
if (!line.includes("pairing_offer")) continue;
try {
const obj = JSON.parse(line) as { msg?: string; url?: string };
if (obj.msg === "pairing_offer" && typeof obj.url === "string") {
return obj.url;
}
} catch {
// ignore
}
async function getPairingOfferUrl(args: {
paseoHome: string;
relayEnabled?: boolean;
relayEndpoint?: string;
relayPublicEndpoint?: string;
appBaseUrl?: string;
}): Promise<string> {
const pairing = await generateLocalPairingOffer({
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): {
@@ -199,7 +206,13 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis
});
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 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 {
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);
// Previously, the daemon would time out waiting for `hello` and reconnect every ~10s.