| 88 | } |
| 89 | |
| 90 | export function acquireProcessLock(opts: { |
| 91 | lockName: LockName; |
| 92 | lockDir: string; |
| 93 | metadata: { |
| 94 | port: number; |
| 95 | worktreeRoot: string; |
| 96 | kind?: LockKind; |
| 97 | parentPid?: number; |
| 98 | capabilities?: string[]; |
| 99 | protocolVersion?: number; |
| 100 | runtimeVersion?: string; |
| 101 | }; |
| 102 | }): ProcessLockHandle { |
| 103 | const { lockName, lockDir, metadata: init } = opts; |
| 104 | const logPrefix = `[${lockName}-lock]`; |
| 105 | |
| 106 | mkdirSync(lockDir, { recursive: true }); |
| 107 | const lockPath = lockFilePath(lockDir, lockName); |
| 108 | |
| 109 | const record: ProcessLockMetadata = { |
| 110 | pid: process.pid, |
| 111 | hostname: hostname(), |
| 112 | port: init.port, |
| 113 | startedAt: new Date().toISOString(), |
| 114 | worktreeRoot: init.worktreeRoot, |
| 115 | ...(init.kind !== undefined && { kind: init.kind }), |
| 116 | ...(init.parentPid !== undefined && { parentPid: init.parentPid }), |
| 117 | ...(init.capabilities !== undefined && { capabilities: init.capabilities }), |
| 118 | protocolVersion: init.protocolVersion ?? PROTOCOL_VERSION, |
| 119 | runtimeVersion: init.runtimeVersion ?? RUNTIME_VERSION, |
| 120 | }; |
| 121 | const payload = JSON.stringify(record, null, 2); |
| 122 | |
| 123 | const MAX_ATTEMPTS = 3; |
| 124 | for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { |
| 125 | if (!existsSync(lockPath)) { |
| 126 | try { |
| 127 | const fd = openSync(lockPath, 'wx', 0o600); |
| 128 | try { |
| 129 | writeSync(fd, payload); |
| 130 | } finally { |
| 131 | closeSync(fd); |
| 132 | } |
| 133 | bumpActiveLockRef(lockPath); |
| 134 | return buildHandle({ lockName, lockDir, lockPath }); |
| 135 | } catch (err) { |
| 136 | if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err; |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | const existing = parseLock(lockPath, logPrefix); |
| 141 | if (existing) { |
| 142 | const sameHost = existing.hostname === hostname(); |
| 143 | if (sameHost && existing.pid === process.pid) { |
| 144 | writeFileSync(lockPath, payload, { encoding: 'utf-8', mode: 0o600 }); |
| 145 | bumpActiveLockRef(lockPath); |
| 146 | return buildHandle({ lockName, lockDir, lockPath }); |
| 147 | } |