* Attempts to acquire a lock for auto-updater * @returns true if lock was acquired, false if another process holds the lock
()
| 174 | * @returns true if lock was acquired, false if another process holds the lock |
| 175 | */ |
| 176 | async function acquireLock(): Promise<boolean> { |
| 177 | const fs = getFsImplementation() |
| 178 | const lockPath = getLockFilePath() |
| 179 | |
| 180 | // Check for existing lock: 1 stat() on the happy path (fresh lock or ENOENT), |
| 181 | // 2 on stale-lock recovery (re-verify staleness immediately before unlink). |
| 182 | try { |
| 183 | const stats = await fs.stat(lockPath) |
| 184 | const age = Date.now() - stats.mtimeMs |
| 185 | if (age < LOCK_TIMEOUT_MS) { |
| 186 | return false |
| 187 | } |
| 188 | // Lock is stale, remove it before taking over. Re-verify staleness |
| 189 | // immediately before unlinking to close a TOCTOU race: if two processes |
| 190 | // both observe the stale lock, A unlinks + writes a fresh lock, then B |
| 191 | // would unlink A's fresh lock and both believe they hold it. A fresh |
| 192 | // lock has a recent mtime, so re-checking staleness makes B back off. |
| 193 | try { |
| 194 | const recheck = await fs.stat(lockPath) |
| 195 | if (Date.now() - recheck.mtimeMs < LOCK_TIMEOUT_MS) { |
| 196 | return false |
| 197 | } |
| 198 | await fs.unlink(lockPath) |
| 199 | } catch (err) { |
| 200 | if (!isENOENT(err)) { |
| 201 | logError(err as Error) |
| 202 | return false |
| 203 | } |
| 204 | } |
| 205 | } catch (err) { |
| 206 | if (!isENOENT(err)) { |
| 207 | logError(err as Error) |
| 208 | return false |
| 209 | } |
| 210 | // ENOENT: no lock file, proceed to create one |
| 211 | } |
| 212 | |
| 213 | // Create lock file atomically with O_EXCL (flag: 'wx'). If another process |
| 214 | // wins the race and creates it first, we get EEXIST and back off. |
| 215 | // Lazy-mkdir the config dir on ENOENT. |
| 216 | try { |
| 217 | await writeFile(lockPath, `${process.pid}`, { |
| 218 | encoding: 'utf8', |
| 219 | flag: 'wx', |
| 220 | }) |
| 221 | return true |
| 222 | } catch (err) { |
| 223 | const code = getErrnoCode(err) |
| 224 | if (code === 'EEXIST') { |
| 225 | return false |
| 226 | } |
| 227 | if (code === 'ENOENT') { |
| 228 | try { |
| 229 | // fs.mkdir from getFsImplementation() is always recursive:true and |
| 230 | // swallows EEXIST internally, so a dir-creation race cannot reach the |
| 231 | // catch below — only writeFile's EEXIST (true lock contention) can. |
| 232 | await fs.mkdir(getClaudeConfigHomeDir()) |
| 233 | await writeFile(lockPath, `${process.pid}`, { |
no test coverage detected