From e3664dc092aa4b8af97cd6f4bc65a1303d53cd7f Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Thu, 6 Aug 2026 02:12:44 +0800 Subject: [PATCH] fix(desktop): stop one worktree in a thousand booting into a blank window (#6436) Worktree renderer ports are `5174 + cksum(path) % 1000`, a 5174-6173 window that contains exactly one port Chromium refuses to navigate to: 6000, the X11 port on its restricted list. A worktree whose path hashes to offset 826 gets a healthy Vite server on 6000 and an Electron window that fails the load with ERR_UNSAFE_PORT -- so it reads as a renderer bug, not a port one, and the only way out was setting DESKTOP_RENDERER_PORT by hand. Restricted ports in the window are now remapped into the block immediately above it (6000 -> 6174). Sending them past the end rather than shifting them by one keeps the offset -> port mapping injective, so two worktrees still cannot land on the same port and race for it. Co-authored-by: Lambda Co-authored-by: multica-agent --- CONTRIBUTING.md | 4 ++- apps/desktop/scripts/worktree-dev-env.mjs | 17 ++++++++++++- .../desktop/scripts/worktree-dev-env.test.mjs | 25 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eeee8b9db4..b5cde4bb62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -512,7 +512,9 @@ derives, from the worktree path (same `cksum % 1000` offset as the backend / frontend ports in `.env.worktree`): - `DESKTOP_RENDERER_PORT` = `5174 + offset` — its own Vite dev server (`5174` - base leaves `5173` for the primary checkout, even when `offset` is `0`) + base leaves `5173` for the primary checkout, even when `offset` is `0`). The + one offset that would land on `6000` gets `6174` instead: Chromium treats + `6000` as a restricted port and fails the load with `ERR_UNSAFE_PORT` - `DESKTOP_APP_SUFFIX` = `-` — its own single-instance lock / `userData`, and an app named `Multica Canary -` so it is distinguishable in Cmd+Tab. The offset keeps it unique across worktrees that diff --git a/apps/desktop/scripts/worktree-dev-env.mjs b/apps/desktop/scripts/worktree-dev-env.mjs index c405f0454c..5ff6ed9933 100644 --- a/apps/desktop/scripts/worktree-dev-env.mjs +++ b/apps/desktop/scripts/worktree-dev-env.mjs @@ -22,6 +22,21 @@ import { basename, join } from "node:path"; const RENDERER_PORT_BASE = 5174; const OFFSET_MODULO = 1000; +// Chromium refuses to navigate to a URL on its restricted-port list and fails +// the load with ERR_UNSAFE_PORT, so a worktree whose derived port lands on one +// gets a blank Electron window instead of the renderer -- the Vite server is +// up and healthy, which makes it read as a renderer bug rather than a port one. +// Exactly one restricted port falls inside 5174-6173: 6000 (X11). Those ports +// are remapped, in list order, into the block immediately above the range, so +// the offset -> port mapping stays injective and two worktrees still cannot +// collide. Keep this sorted and in sync with net::kRestrictedPorts. +const RESTRICTED_PORTS_IN_RANGE = [6000]; + +function avoidRestrictedPort(port) { + const index = RESTRICTED_PORTS_IN_RANGE.indexOf(port); + return index === -1 ? port : RENDERER_PORT_BASE + OFFSET_MODULO + index; +} + // POSIX cksum (CRC-32), kept byte-compatible with `cksum(1)` so the offset // matches scripts/init-worktree-env.sh — a worktree's backend (18080+offset), // frontend (13000+offset) and desktop renderer (5174+offset) ports all share @@ -60,7 +75,7 @@ export function offsetForPath(path) { } export function rendererPortForPath(path) { - return RENDERER_PORT_BASE + offsetForPath(path); + return avoidRestrictedPort(RENDERER_PORT_BASE + offsetForPath(path)); } // Worktree → a readable, unique, filesystem-safe suffix "-". diff --git a/apps/desktop/scripts/worktree-dev-env.test.mjs b/apps/desktop/scripts/worktree-dev-env.test.mjs index b8d82b54e6..f984c46b6d 100644 --- a/apps/desktop/scripts/worktree-dev-env.test.mjs +++ b/apps/desktop/scripts/worktree-dev-env.test.mjs @@ -47,6 +47,31 @@ describe("worktree-dev-env", () => { expect(rendererPortForPath("/tmp/multica-3494")).not.toBe(5173); }); + it("skips 6000, which Chromium refuses to load (ERR_UNSAFE_PORT)", () => { + // POSIX cksum("/tmp/wt-570") === 109908826, % 1000 === 826 → 5174 + 826 === 6000 + expect(offsetForPath("/tmp/wt-570")).toBe(826); + expect(rendererPortForPath("/tmp/wt-570")).toBe(6174); + }); + + it("stays collision-free across every offset while skipping restricted ports", () => { + // The remap must stay injective: two worktrees sharing a port means the + // second Electron dies on EADDRINUSE. Cover all 1000 offsets with real + // paths so this exercises rendererPortForPath rather than restating it. + const pathForOffset = new Map(); + for (let i = 0; pathForOffset.size < 1000; i++) { + const path = `/tmp/wt-${i}`; + const offset = offsetForPath(path); + if (!pathForOffset.has(offset)) pathForOffset.set(offset, path); + } + + const ports = new Set( + [...pathForOffset.values()].map((path) => rendererPortForPath(path)), + ); + expect(ports.size).toBe(1000); + expect(ports.has(6000)).toBe(false); + expect(ports.has(5173)).toBe(false); + }); + it("suffix is '-' so it stays recognizable and unique", () => { expect(appSuffixForPath("/work/MUL-3724_Desktop")).toBe( `mul-3724-desktop-${offsetForPath("/work/MUL-3724_Desktop")}`,