Skip to content

Commit b47ff01

Browse files
Ankur ChakrabortyAnkur Chakraborty
authored andcommitted
feat: add PID lockfile to prevent multiple dreamcode instances
Uses O_EXCL (exclusive create) via fs.openSync('wx') for race-free lock acquisition at /tmp/dreamcode.pid. - On startup: read existing PID, check if alive via process.kill(pid, 0) - If alive: print error with PID hint and exit - If stale (PID no longer running): remove lockfile and retry - Bounded retry (max 3) prevents infinite recursion in tight races - Cleanup: process.on('exit') removes lockfile only if we still own it - All failures silently proceed (advisory lock, not mandatory)
1 parent 25eb8fe commit b47ff01

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

‎packages/opencode/src/index.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from "node:fs"
12
import yargs from "yargs"
23
import { hideBin } from "yargs/helpers"
34
import { RunCommand } from "./cli/cmd/run"
@@ -30,6 +31,72 @@ import { errorMessage } from "./util/error"
3031
import { PluginCommand } from "./cli/cmd/plug"
3132
import { Heap } from "./cli/heap"
3233

34+
const LOCKFILE = "/tmp/dreamcode.pid"
35+
36+
function acquireLockfile(): void {
37+
// Use O_EXCL (exclusive create) via fs.openSync("wx") to atomically claim
38+
// the lockfile. This is race-free: if two instances start simultaneously,
39+
// only one open() succeeds; the other gets EEXIST.
40+
const tryLock = (retries = 0): boolean => {
41+
if (retries > 3) return false
42+
try {
43+
const fd = fs.openSync(LOCKFILE, "wx", 0o644)
44+
fs.writeSync(fd, String(process.pid))
45+
fs.closeSync(fd)
46+
return true
47+
} catch (err: unknown) {
48+
const code = typeof err === "object" && err !== null && "code" in err ? (err as { code?: string }).code : undefined
49+
if (code !== "EEXIST") return false
50+
// Lockfile exists — check if the PID inside is still alive
51+
try {
52+
const pidStr = fs.readFileSync(LOCKFILE, "utf-8").trim()
53+
const pid = parseInt(pidStr, 10)
54+
if (!isNaN(pid) && pid > 0) {
55+
try {
56+
process.kill(pid, 0)
57+
// Process is alive — another instance is running
58+
console.error(`Error: Another dreamcode instance is already running (PID ${pid})`)
59+
console.error(`If this is incorrect, delete ${LOCKFILE} and try again.`)
60+
process.exit(1)
61+
} catch {
62+
// Stale lockfile — remove and retry
63+
fs.unlinkSync(LOCKFILE)
64+
return tryLock(retries + 1)
65+
}
66+
}
67+
// Invalid PID — remove stale lockfile and retry
68+
fs.unlinkSync(LOCKFILE)
69+
return tryLock(retries + 1)
70+
} catch {
71+
// Can't read or remove lockfile — just proceed
72+
return false
73+
}
74+
}
75+
}
76+
77+
try {
78+
if (!tryLock()) return // Could not acquire lock — proceed anyway
79+
80+
// Clean up lockfile on exit — only if we still own it
81+
const cleanup = () => {
82+
try {
83+
if (fs.existsSync(LOCKFILE)) {
84+
const current = fs.readFileSync(LOCKFILE, "utf-8").trim()
85+
if (current === String(process.pid)) {
86+
fs.unlinkSync(LOCKFILE)
87+
}
88+
}
89+
} catch { /* ignore cleanup errors */ }
90+
}
91+
92+
process.on("exit", cleanup)
93+
} catch {
94+
// If lockfile operations fail (permissions, etc.), just proceed
95+
}
96+
}
97+
98+
acquireLockfile()
99+
33100
const args = hideBin(process.argv)
34101

35102
function show(out: string) {

0 commit comments

Comments
 (0)