fix(desktop): allow concurrent worktree dev instances

Desktop terminals can inherit another instance's profile path and debugging flags. Scope the profile to the current dev root and select an available debugging port so instances do not share global runtime state.
This commit is contained in:
Mohamed Boudra
2026-07-29 16:40:50 +02:00
parent aed3eb490e
commit 6b13587943
5 changed files with 182 additions and 19 deletions
+11 -4
View File
@@ -99,10 +99,17 @@ The iOS simulator shares the Mac's loopback, so `localhost:<port>` reaches the h
### Desktop renderer profiling
`npm run dev:desktop` starts Electron with Chromium remote debugging enabled on
`http://127.0.0.1:9223` so renderer CPU profiles can be captured through CDP.
It launches its own Electron-flavored Expo server and passes that URL to Electron.
Override the CDP port with `PASEO_ELECTRON_REMOTE_DEBUGGING_PORT` when `9223` is busy.
`npm run dev:desktop` starts Electron with Chromium remote debugging enabled so
renderer CPU profiles can be captured through CDP. It prefers
`http://127.0.0.1:9223` and automatically selects a free port when that port is
already occupied. The startup banner prints the selected URL. It launches its own
Electron-flavored Expo server and passes that URL to Electron. Set
`PASEO_ELECTRON_REMOTE_DEBUGGING_PORT` when a QA workflow requires a fixed port.
Desktop dev also scopes Electron `userData` to the current dev root. This prevents
desktop-only environment inherited by terminals opened inside Paseo from coupling
a new worktree instance to the parent desktop instance's profile or single-instance
lock.
With desktop dev running, verify the real BrowserWindow, titlebar clearance, fullscreen
transition, and 751-pixel settings split with:
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
import net from "node:net";
import path from "node:path";
import { fileURLToPath } from "node:url";
const DEFAULT_REMOTE_DEBUGGING_PORT = 9223;
export function resolveDevUserDataDir({ devRoot, inheritedUserDataDir, fallbackRoot }) {
if (devRoot) {
return path.join(devRoot, ".dev", "user-data");
}
if (inheritedUserDataDir) {
return inheritedUserDataDir;
}
if (!fallbackRoot) {
throw new Error("A dev root or fallback root is required");
}
return path.join(fallbackRoot, ".dev", "user-data");
}
export function buildElectronFlags(inheritedFlags, remoteDebuggingPort) {
const flags = (inheritedFlags ?? "")
.split(/\s+/)
.filter(Boolean)
.filter((flag) => !flag.startsWith("--remote-debugging-port="));
flags.push(`--remote-debugging-port=${remoteDebuggingPort}`);
return flags.join(" ");
}
function parsePort(value, name) {
const port = Number(value);
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error(`${name} must be an integer between 1 and 65535`);
}
return port;
}
export function findAvailablePort(preferredPort, host = "127.0.0.1") {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.once("error", (error) => {
if (error.code === "EADDRINUSE" || error.code === "EACCES") {
resolve(findAvailablePort(0, host));
return;
}
reject(error);
});
server.listen(preferredPort, host, () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Could not resolve an available CDP port"));
return;
}
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve(address.port);
});
});
});
}
export async function selectRemoteDebuggingPort({
configuredPort,
preferredPort = DEFAULT_REMOTE_DEBUGGING_PORT,
findAvailablePort: findPort = findAvailablePort,
} = {}) {
if (configuredPort) {
return parsePort(configuredPort, "PASEO_ELECTRON_REMOTE_DEBUGGING_PORT");
}
return findPort(preferredPort);
}
export async function resolveDevRuntime(env = process.env) {
const remoteDebuggingPort = await selectRemoteDebuggingPort({
configuredPort: env.PASEO_ELECTRON_REMOTE_DEBUGGING_PORT,
});
return {
electronFlags: buildElectronFlags(env.PASEO_ELECTRON_FLAGS, remoteDebuggingPort),
remoteDebuggingPort,
userDataDir: resolveDevUserDataDir({
devRoot: env.PASEO_DEV_ROOT,
inheritedUserDataDir: env.PASEO_ELECTRON_USER_DATA_DIR,
fallbackRoot: env.PASEO_DEV_RUNTIME_FALLBACK_ROOT,
}),
};
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
try {
process.stdout.write(JSON.stringify(await resolveDevRuntime()));
} catch (error) {
console.error(`[dev-runtime] ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
}
}
@@ -0,0 +1,56 @@
import path from "node:path";
import { describe, expect, test } from "vitest";
import {
buildElectronFlags,
resolveDevUserDataDir,
selectRemoteDebuggingPort,
} from "./dev-runtime.mjs";
describe("desktop dev runtime isolation", () => {
test("scopes inherited user data to the explicitly selected dev root", () => {
expect(
resolveDevUserDataDir({
devRoot: "/worktrees/feature-a",
inheritedUserDataDir: "/checkouts/paseo/.dev/user-data",
}),
).toBe(path.join("/worktrees/feature-a", ".dev", "user-data"));
});
test("keeps an explicit user data override outside a managed dev root", () => {
expect(
resolveDevUserDataDir({
inheritedUserDataDir: "/tmp/paseo-user-data",
fallbackRoot: "/checkouts/paseo",
}),
).toBe("/tmp/paseo-user-data");
});
test("replaces an inherited CDP flag with this instance's selected port", () => {
expect(buildElectronFlags("--disable-gpu --remote-debugging-port=9223", 43127)).toBe(
"--disable-gpu --remote-debugging-port=43127",
);
});
test("selects another CDP port when the default is occupied", async () => {
const selectedPort = await selectRemoteDebuggingPort({
preferredPort: 9223,
findAvailablePort: async (preferredPort) => {
expect(preferredPort).toBe(9223);
return 43127;
},
});
expect(selectedPort).toBe(43127);
});
test("honors an explicit CDP port without silently changing it", async () => {
const selectedPort = await selectRemoteDebuggingPort({
configuredPort: "9333",
findAvailablePort: async () => {
throw new Error("must not probe an explicit port");
},
});
expect(selectedPort).toBe(9333);
});
});
+7 -12
View File
@@ -33,17 +33,13 @@ try {
# Set EXPO_DEV_URL in the environment so Electron inherits it
$env:EXPO_DEV_URL = "http://localhost:$($env:EXPO_PORT)"
$RemoteDebuggingPort = if ($env:PASEO_ELECTRON_REMOTE_DEBUGGING_PORT) {
$env:PASEO_ELECTRON_REMOTE_DEBUGGING_PORT
} else {
"9223"
}
$ExistingElectronFlags = if ($env:PASEO_ELECTRON_FLAGS) {
"$($env:PASEO_ELECTRON_FLAGS) "
} else {
""
}
$env:PASEO_ELECTRON_FLAGS = "$($ExistingElectronFlags)--remote-debugging-port=$RemoteDebuggingPort"
$env:PASEO_DEV_ROOT = $RootDir
$env:PASEO_DEV_RUNTIME_FALLBACK_ROOT = $RootDir
$DevRuntime = node "$ScriptDir\dev-runtime.mjs" | ConvertFrom-Json
$RemoteDebuggingPort = $DevRuntime.remoteDebuggingPort
$env:PASEO_ELECTRON_FLAGS = $DevRuntime.electronFlags
$env:PASEO_ELECTRON_USER_DATA_DIR = $DevRuntime.userDataDir
Remove-Item Env:\PASEO_DEV_RUNTIME_FALLBACK_ROOT -ErrorAction SilentlyContinue
# Allow any origin in dev so Electron on random ports works.
# SECURITY: wildcard CORS is unsafe in production — only acceptable here because
@@ -64,7 +60,6 @@ if (-not $env:PASEO_HOME) {
} else {
$PaseoHomeManaged = $false
}
if (-not $env:PASEO_ELECTRON_USER_DATA_DIR) { $env:PASEO_ELECTRON_USER_DATA_DIR = "$DevStateDir\user-data" }
New-Item -ItemType Directory -Force -Path $env:PASEO_HOME, $env:PASEO_ELECTRON_USER_DATA_DIR | Out-Null
$DevDaemonPort = if ($env:PASEO_DEV_DAEMON_PORT) { $env:PASEO_DEV_DAEMON_PORT } else { "6788" }
+7 -3
View File
@@ -12,7 +12,13 @@ export PASEO_LISTEN="${PASEO_LISTEN:-127.0.0.1:6768}"
configure_dev_paseo_home
DEV_ROOT="${PASEO_DEV_ROOT:-$(default_dev_paseo_root)}"
export PASEO_ELECTRON_USER_DATA_DIR="${PASEO_ELECTRON_USER_DATA_DIR:-$DEV_ROOT/.dev/user-data}"
export PASEO_DEV_ROOT="$DEV_ROOT"
export PASEO_DEV_RUNTIME_FALLBACK_ROOT="$DEV_ROOT"
DEV_RUNTIME="$(node "$SCRIPT_DIR/dev-runtime.mjs")"
REMOTE_DEBUGGING_PORT="$(node -e 'process.stdout.write(String(JSON.parse(process.argv[1]).remoteDebuggingPort))' "$DEV_RUNTIME")"
export PASEO_ELECTRON_FLAGS="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).electronFlags)' "$DEV_RUNTIME")"
export PASEO_ELECTRON_USER_DATA_DIR="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).userDataDir)' "$DEV_RUNTIME")"
unset PASEO_DEV_RUNTIME_FALLBACK_ROOT
mkdir -p "$PASEO_ELECTRON_USER_DATA_DIR"
if [ -z "${EXPO_PORT:-}" ]; then
@@ -24,8 +30,6 @@ export EXPO_DEV_URL="http://localhost:${EXPO_PORT}"
DAEMON_ENDPOINT="$(resolve_dev_daemon_endpoint)"
export PASEO_DAEMON_ENDPOINT="$DAEMON_ENDPOINT"
REMOTE_DEBUGGING_PORT="${PASEO_ELECTRON_REMOTE_DEBUGGING_PORT:-9223}"
export PASEO_ELECTRON_FLAGS="${PASEO_ELECTRON_FLAGS:+$PASEO_ELECTRON_FLAGS }--remote-debugging-port=$REMOTE_DEBUGGING_PORT"
export PASEO_CORS_ORIGINS="${PASEO_CORS_ORIGINS:-*}"
npm run build:main