mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-07 11:14:28 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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>".
|
||||
|
||||
@@ -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")}`,
|
||||
|
||||
Reference in New Issue
Block a user