( options: FileLockOptions )
| 117 | } |
| 118 | |
| 119 | export async function acquireFileLock( |
| 120 | options: FileLockOptions |
| 121 | ): Promise<nodeFs.promises.FileHandle> { |
| 122 | const { lockPath, errorFor } = options; |
| 123 | const lockDir = path.dirname(lockPath); |
| 124 | await FileSystemUtils.createDirectory(lockDir); |
| 125 | if (!(await FileSystemUtils.canWriteFile(lockDir))) { |
| 126 | throw errorFor('create-failed', { lockPath, cause: 'EACCES' }); |
| 127 | } |
| 128 | const deadline = Date.now() + LOCK_DEADLINE_MS; |
| 129 | |
| 130 | while (true) { |
| 131 | try { |
| 132 | return await fs.open(lockPath, 'wx'); |
| 133 | } catch (error) { |
| 134 | if (!isNodeErrorCode(error, 'EEXIST')) { |
| 135 | // A permission or filesystem problem, not contention - say so. |
| 136 | throw errorFor('create-failed', { lockPath, cause: error }); |
| 137 | } |
| 138 | |
| 139 | // A crashed process leaves the lock behind forever; state-file |
| 140 | // writes are sub-second, so an old lock is an orphan - steal it. |
| 141 | let staleStolen = false; |
| 142 | try { |
| 143 | const lockStat = await fs.stat(lockPath); |
| 144 | if (Date.now() - lockStat.mtimeMs > STALE_LOCK_THRESHOLD_MS) { |
| 145 | await fs.rm(lockPath, { force: true }); |
| 146 | staleStolen = true; |
| 147 | } |
| 148 | } catch { |
| 149 | // The holder released between open and stat - retry, but stay |
| 150 | // bounded: a persistently failing stat (EPERM, delete-pending) |
| 151 | // must hit the deadline instead of spinning forever. |
| 152 | } |
| 153 | |
| 154 | if (!staleStolen) { |
| 155 | if (Date.now() >= deadline) { |
| 156 | throw errorFor('timeout', { lockPath }); |
| 157 | } |
| 158 | await sleep(LOCK_POLL_MS); |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | export async function releaseFileLock( |
| 165 | lock: nodeFs.promises.FileHandle, |
no test coverage detected