fix: handle ENOTEMPTY race in CLI prepareDashboardRuntime (#1609)

This commit is contained in:
Konsti Wohlwend 2026-06-17 11:33:44 -07:00 committed by GitHub
parent 2cf2552803
commit ba467f8876
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,6 +1,6 @@
import { execFileSync, spawn, type ChildProcess } from "child_process";
import { Command } from "commander";
import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync, writeSync } from "fs";
import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { DEFAULT_API_URL, DEFAULT_PUBLISHABLE_CLIENT_KEY, resolveLoginConfig } from "../lib/auth.js";
@ -250,6 +250,10 @@ function replaceDashboardRuntimeSentinels(root: string, env: NodeJS.ProcessEnv):
}
}
function dashboardRuntimeLockPath(port: number): string {
return `${dashboardRuntimeRoot(port)}.lock`;
}
function prepareDashboardRuntime(env: NodeJS.ProcessEnv, port: number): string {
assertBundledDashboardExists();
const runtimeRoot = dashboardRuntimeRoot(port);
@ -479,19 +483,54 @@ async function startDashboardIfNeeded(options: { apiBaseUrl: string, secret: str
const logFd = openSync(logPath, "a", 0o600);
chmodSync(logPath, 0o600);
writeSync(logFd, `\n[${new Date().toISOString()}] Starting Hexclave development-environment dashboard on ${url}\n`);
const child = (() => {
try {
return startDashboardProcess({ dashboardEnv, logFd, port: options.port });
} finally {
closeSync(logFd);
// Acquire a filesystem lock so parallel `hexclave dev` invocations don't
// race on the runtime directory. openSync with 'wx' is an atomic
// exclusive-create; EEXIST means another process holds the lock.
let lockAcquired = false;
const lockPath = dashboardRuntimeLockPath(options.port);
// Remove stale lock left behind if a previous process was killed mid-prepare
// (normal hold time is <1 s, so 5 s is certainly stale).
try {
const lockStat = statSync(lockPath);
if (Date.now() - lockStat.mtimeMs > 5000) {
unlinkSync(lockPath);
}
} catch {
// lock doesn't exist or was already removed — fine
}
try {
closeSync(openSync(lockPath, "wx"));
lockAcquired = true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
}
if (!lockAcquired) {
closeSync(logFd);
logDev("Another process is starting the dashboard; waiting for it...");
} else {
try {
const child = (() => {
try {
return startDashboardProcess({ dashboardEnv, logFd, port: options.port });
} finally {
closeSync(logFd);
}
})();
if (child.pid == null) {
throw new CliError(`Failed to start the development environment dashboard process. Dashboard logs: ${logPath}`);
}
recordLocalDashboardProcess(options.port, options.secret, child.pid, logPath, cliVersion());
logDev(`Dashboard logs: ${logPath}`);
child.unref();
} finally {
try {
unlinkSync(lockPath);
} catch {
// best-effort cleanup
}
}
})();
if (child.pid == null) {
throw new CliError(`Failed to start the development environment dashboard process. Dashboard logs: ${logPath}`);
}
recordLocalDashboardProcess(options.port, options.secret, child.pid, logPath, cliVersion());
logDev(`Dashboard logs: ${logPath}`);
child.unref();
const startedAt = performance.now();
while (performance.now() - startedAt < DASHBOARD_START_TIMEOUT_MS) {