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 <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Jiayuan Zhang
2026-08-06 02:12:44 +08:00
committed by GitHub
parent 2cbd72e800
commit e3664dc092
3 changed files with 44 additions and 2 deletions

View File

@@ -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` = `<folder>-<offset>` — its own single-instance lock /
`userData`, and an app named `Multica Canary <folder>-<offset>` so it is
distinguishable in Cmd+Tab. The offset keeps it unique across worktrees that

View File

@@ -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 "<folder>-<offset>".

View File

@@ -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 '<folder>-<offset>' so it stays recognizable and unique", () => {
expect(appSuffixForPath("/work/MUL-3724_Desktop")).toBe(
`mul-3724-desktop-${offsetForPath("/work/MUL-3724_Desktop")}`,