From 6af335c4d316aae65326fb04de8e87d542e63854 Mon Sep 17 00:00:00 2001 From: Nikolay Kushin Date: Sun, 12 Jul 2026 12:26:52 +0200 Subject: [PATCH] fix(broker): migrate legacy lock files --- extensions/relay/broker/supervisor.ts | 23 ++++++++++++++++++++++- tests/relay/broker-supervisor.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/extensions/relay/broker/supervisor.ts b/extensions/relay/broker/supervisor.ts index 29cb78d..450f850 100644 --- a/extensions/relay/broker/supervisor.ts +++ b/extensions/relay/broker/supervisor.ts @@ -1,5 +1,5 @@ import { constants } from "node:fs"; -import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { access, lstat, mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import lockfile from "proper-lockfile"; @@ -113,6 +113,23 @@ export async function ensureLocalBroker(options: EnsureLocalBrokerOptions): Prom return { status: "started", paths, pid: started.pid }; } +async function removeLegacyBrokerLockFile(lockPath: string): Promise { + try { + await unlink(lockPath); + } catch (error) { + if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return; + + // proper-lockfile owns directory locks. If unlink failed because this is already + // a directory, leave an active or stale directory lock for it to coordinate. + try { + if ((await lstat(lockPath)).isDirectory()) return; + } catch (statError) { + if (typeof statError === "object" && statError !== null && "code" in statError && statError.code === "ENOENT") return; + } + throw error; + } +} + export async function ensureScopedBroker(options: EnsureScopedBrokerOptions): Promise { const paths = brokerScopeControlPaths(options); const isAlive = options.isAlive ?? isProcessAlive; @@ -123,6 +140,10 @@ export async function ensureScopedBroker(options: EnsureScopedBrokerOptions): Pr if (pid) return { status: "existing", paths, pid }; } + // Older releases used this path as a regular lock target and left the file + // behind. proper-lockfile now uses it as an atomic directory lock. + await removeLegacyBrokerLockFile(paths.lockPath); + const lockTargetPath = paths.lockPath.endsWith(".lock") ? paths.lockPath.slice(0, -".lock".length) : `${paths.lockPath}.target`; const release = await lockfile.lock(lockTargetPath, { lockfilePath: paths.lockPath, diff --git a/tests/relay/broker-supervisor.test.ts b/tests/relay/broker-supervisor.test.ts index a53d171..69dc3e9 100644 --- a/tests/relay/broker-supervisor.test.ts +++ b/tests/relay/broker-supervisor.test.ts @@ -97,7 +97,30 @@ describe("local broker supervisor", () => { const paths = brokerScopeControlPaths({ stateDir, tokenHash: "abc123" }); expect(starts).toBe(1); expect(await readBrokerPid(paths.pidPath)).toBe(4444); - await expect(stat(`${paths.lockPath}.lock`)).rejects.toThrow(); + await expect(stat(paths.lockPath)).rejects.toThrow(); + }); + + it("replaces a legacy regular lock file with the directory lock format", async () => { + const stateDir = await mkdtemp(join(tmpdir(), "pirelay-supervisor-")); + const paths = brokerScopeControlPaths({ stateDir, tokenHash: "abc123" }); + await writeFile(paths.lockPath, "", { mode: 0o600 }); + let observedDirectoryLock = false; + + const result = await ensureScopedBroker({ + stateDir, + tokenHash: "abc123", + startBroker: async () => { + observedDirectoryLock = (await stat(paths.lockPath)).isDirectory(); + return { pid: 4567 }; + }, + probeSocket: async () => false, + waitForSocketReady: async () => undefined, + isAlive: () => false, + }); + + expect(result).toMatchObject({ status: "started", pid: 4567 }); + expect(observedDirectoryLock).toBe(true); + await expect(stat(paths.lockPath)).rejects.toThrow(); }); it("does not return pid zero when a socket is ready before its pid file", async () => {