(opts: AcquireLockOptions)
| 191 | * recorded pid is no longer running. |
| 192 | */ |
| 193 | export function acquireLock(opts: AcquireLockOptions): AcquireLockResult { |
| 194 | const lockPath = opts.lockPath ?? DEFAULT_LOCK_PATH; |
| 195 | const pid = opts.pid ?? process.pid; |
| 196 | const startedAt = opts.nowIso ?? new Date().toISOString(); |
| 197 | const contents: LockContents = { |
| 198 | pid, |
| 199 | started_at: startedAt, |
| 200 | host: opts.host, |
| 201 | port: opts.port, |
| 202 | ...(opts.hostVersion !== undefined ? { host_version: opts.hostVersion } : {}), |
| 203 | ...(opts.entry !== undefined ? { entry: opts.entry } : {}), |
| 204 | }; |
| 205 | |
| 206 | mkdirSync(dirname(lockPath), { recursive: true }); |
| 207 | |
| 208 | // First try: clean acquire. |
| 209 | if (tryExclusiveCreate(lockPath, contents)) { |
| 210 | return makeReleaseHandle(lockPath, pid); |
| 211 | } |
| 212 | |
| 213 | // Lock exists — inspect. |
| 214 | const existing = readLockContents(lockPath); |
| 215 | if (existing && pidAlive(existing.pid)) { |
| 216 | // Live owner — refuse to take over. Note that "same pid as ours" still |
| 217 | // counts as live: callers that genuinely want to swap should release the |
| 218 | // existing handle first, not stomp via acquireLock. |
| 219 | throw new ServerLockedError( |
| 220 | `server already running (pid=${existing.pid}, port=${existing.port}, started=${existing.started_at})`, |
| 221 | existing, |
| 222 | ); |
| 223 | } |
| 224 | |
| 225 | // Stale (dead pid) or unparseable — take over. |
| 226 | try { |
| 227 | unlinkSync(lockPath); |
| 228 | } catch (err) { |
| 229 | // EBUSY/ENOENT both acceptable — race with another concurrent acquirer. |
| 230 | if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { |
| 231 | throw err; |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | if (!tryExclusiveCreate(lockPath, contents)) { |
| 236 | // Someone slipped in. Re-read for diagnostic. |
| 237 | const winner = readLockContents(lockPath); |
| 238 | throw new ServerLockedError( |
| 239 | winner |
| 240 | ? `server already running (pid=${winner.pid}, port=${winner.port}, started=${winner.started_at})` |
| 241 | : 'lock file present but unreadable', |
| 242 | winner ?? { pid: -1, started_at: '', port: opts.port }, |
| 243 | ); |
| 244 | } |
| 245 | return makeReleaseHandle(lockPath, pid); |
| 246 | } |
| 247 | |
| 248 | function makeReleaseHandle(lockPath: string, ownerPid: number): AcquireLockResult { |
| 249 | let released = false; |
no test coverage detected