|
| 1 | +import fs from "node:fs" |
1 | 2 | import yargs from "yargs" |
2 | 3 | import { hideBin } from "yargs/helpers" |
3 | 4 | import { RunCommand } from "./cli/cmd/run" |
@@ -30,6 +31,72 @@ import { errorMessage } from "./util/error" |
30 | 31 | import { PluginCommand } from "./cli/cmd/plug" |
31 | 32 | import { Heap } from "./cli/heap" |
32 | 33 |
|
| 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 | + |
33 | 100 | const args = hideBin(process.argv) |
34 | 101 |
|
35 | 102 | function show(out: string) { |
|
0 commit comments