diff options
| author | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-05-16 13:00:48 -0400 |
|---|---|---|
| committer | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-05-16 13:00:48 -0400 |
| commit | fcae5b188baa1bdfed48208d7cfc2b711af4aae3 (patch) | |
| tree | 040c6cde8b8d23d1909c7235641e6694ed170495 /extensions | |
| parent | 43c62c2f338654394f82c86aa12b22be6a358d94 (diff) | |
0.17.2: fix /next lastSeenAt + strict daemon version match
Bugs surfaced in real-world 0.17.0/0.17.1 testing:
1. Daemon /next only recorded lastSeenAt AFTER the auth/pair checks. Pre-pair
the daemon returns needsPairing early without recording the poll, so
connected stayed false even though the extension was polling. /chrome
onboard's wait-for-poll loop spun forever on a healthy extension.
Fix: record lastSeenAt at the top of /next for any chrome-extension
origin, before auth checks. Onboard now correctly detects 'extension
alive but unpaired' which is the precise state during pair step.
2. Daemon compatibility was checked at major.minor match. Result: a patch
release fixing a daemon bug (like #1) couldn't replace the running
daemon — new Pi client joins old buggy daemon as client.
Fix: exact version match. Patch releases auto-kill+respawn the daemon.
Diffstat (limited to 'extensions')
| -rw-r--r-- | extensions/chrome-profile-bridge/browser-extension/manifest.json | 2 | ||||
| -rw-r--r-- | extensions/chrome-profile-bridge/daemon.cjs | 10 | ||||
| -rw-r--r-- | extensions/chrome-profile-bridge/index.ts | 16 |
3 files changed, 18 insertions, 10 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json index 98f47a5..d3db49e 100644 --- a/extensions/chrome-profile-bridge/browser-extension/manifest.json +++ b/extensions/chrome-profile-bridge/browser-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Pi Chrome Connector", - "version": "0.17.1", + "version": "0.17.2", "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.", "permissions": [ "tabs", diff --git a/extensions/chrome-profile-bridge/daemon.cjs b/extensions/chrome-profile-bridge/daemon.cjs index 676344f..bcd2b68 100644 --- a/extensions/chrome-profile-bridge/daemon.cjs +++ b/extensions/chrome-profile-bridge/daemon.cjs @@ -569,6 +569,14 @@ async function handle(req, res) { if (!isBrowserOriginAllowed(req)) { sendJson(res, 403, { ok: false, error: "browser origin not allowed" }); return; } const originExt = extensionIdFromOrigin(req); const currentVersion = ARGS.version; + // Any /next from a chrome-extension origin counts as 'extension is alive'. We record + // lastSeenAt here — before the auth/pair checks below — so that /chrome onboard's + // connect-wait loop can detect a polling-but-unpaired extension (which is precisely + // the state during the pair step). + if (originExt) { + lastSeenAt = Date.now(); + clientName = url.searchParams.get("name") ?? undefined; + } if (!auth.paired) { sendJson(res, 200, { type: "none", needsPairing: true, expectedExtensionVersion: currentVersion }, { ...corsHeaders, "x-pi-chrome-version": currentVersion }); @@ -590,8 +598,6 @@ async function handle(req, res) { return; } if (waiters.length >= MAX_WAITERS) { sendJson(res, 429, { ok: false, error: "too many pollers" }, corsHeaders); return; } - lastSeenAt = Date.now(); - clientName = url.searchParams.get("name") ?? undefined; let aborted = false; let active; req.once("close", () => { aborted = true; if (active) waiters = waiters.filter((e) => e !== active); }); diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts index db41ce3..820c7d7 100644 --- a/extensions/chrome-profile-bridge/index.ts +++ b/extensions/chrome-profile-bridge/index.ts @@ -114,10 +114,12 @@ function probeBridgeStatus(host: string, port: number): { bridgeVersion?: string } } -function sameMajorMinor(a: string, b: string): boolean { - const [aMaj, aMin] = a.split(".").map((n) => Number(n)); - const [bMaj, bMin] = b.split(".").map((n) => Number(n)); - return aMaj === bMaj && aMin === bMin; +// Daemon compat is checked at exact version match — not major.minor — so that even patch +// releases trigger an automatic kill+respawn. Patch releases routinely fix bugs in the daemon +// itself (e.g. /next lastSeenAt handling); without exact matching, a new Pi client joins an +// old buggy daemon and the fix never lands. +function daemonVersionCompatible(daemonVersion: string, clientVersion: string): boolean { + return daemonVersion === clientVersion; } // Ensure the bundled daemon.cjs is copied to a versioned cache dir so each pi-chrome @@ -178,7 +180,7 @@ function waitForDaemonReady(host: string, port: number, timeoutMs = 4_000): Reco while (Date.now() < deadline) { const probed = probeBridgeStatus(host, port); if (probed && typeof (probed as { bridgeVersion?: string }).bridgeVersion === "string" - && sameMajorMinor((probed as { bridgeVersion: string }).bridgeVersion, PI_CHROME_VERSION)) { + && daemonVersionCompatible((probed as { bridgeVersion: string }).bridgeVersion, PI_CHROME_VERSION)) { return probed; } spawnSync("sleep", ["0.1"]); @@ -844,7 +846,7 @@ class ChromeProfileBridge { const ownerVersion = typeof (probed as { bridgeVersion?: string })?.bridgeVersion === "string" ? (probed as { bridgeVersion: string }).bridgeVersion : undefined; - const compatible = ownerVersion ? sameMajorMinor(ownerVersion, PI_CHROME_VERSION) : false; + const compatible = ownerVersion ? daemonVersionCompatible(ownerVersion, PI_CHROME_VERSION) : false; if (probed && compatible) { this.mode = "client"; return; @@ -868,7 +870,7 @@ class ChromeProfileBridge { private async handleEaddrinuse(): Promise<void> { const probed = probeBridgeStatus(this.host, this.port); const ownerVersion = typeof probed?.bridgeVersion === "string" ? probed.bridgeVersion : undefined; - const compatible = ownerVersion ? sameMajorMinor(ownerVersion, PI_CHROME_VERSION) : false; + const compatible = ownerVersion ? daemonVersionCompatible(ownerVersion, PI_CHROME_VERSION) : false; if (probed && compatible) { this.mode = "client"; return; |
