diff --git a/src/main/services/team/fileLock.ts b/src/main/services/team/fileLock.ts index 56a1c7de..46bb3ada 100644 --- a/src/main/services/team/fileLock.ts +++ b/src/main/services/team/fileLock.ts @@ -71,8 +71,23 @@ function removeLockPath(lockPath: string): void { function writeLockFile(lockPath: string): void { const fd = fs.openSync(lockPath, 'wx'); - fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`); - fs.closeSync(fd); + let closeError: unknown = null; + try { + fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`); + } finally { + try { + fs.closeSync(fd); + } catch (err) { + closeError = err; + } + } + if (closeError) { + throw closeError; + } +} + +function isExistingLockError(code: string | undefined): boolean { + return code === 'EEXIST' || code === 'EISDIR'; } function tryAcquire(lockPath: string, options: Required): boolean { @@ -85,19 +100,27 @@ function tryAcquire(lockPath: string, options: Required): boole } catch (err) { const code = (err as NodeJS.ErrnoException).code; if (code === 'ENOENT') { - // Lock directory missing — create it lazily and acquire in the same call, so + // Lock directory missing - create it lazily and acquire in the same call, so // first-acquire latency in a fresh dir is unchanged. try { fs.mkdirSync(path.dirname(lockPath), { recursive: true }); writeLockFile(lockPath); return true; - } catch { - // Lost a race (another process created the dir/lock) or still failing — - // fall through to a normal retry on the next loop iteration. - return false; + } catch (retryError) { + const retryCode = (retryError as NodeJS.ErrnoException).code; + if (retryCode === 'ENOENT') { + return false; + } + if (isExistingLockError(retryCode)) { + if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) { + removeLockPath(lockPath); + } + return false; + } + throw retryError; } } - if (code === 'EEXIST' || code === 'EISDIR') { + if (isExistingLockError(code)) { if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) { removeLockPath(lockPath); } diff --git a/test/main/services/team/fileLock.test.ts b/test/main/services/team/fileLock.test.ts index fdb99841..c99825ec 100644 --- a/test/main/services/team/fileLock.test.ts +++ b/test/main/services/team/fileLock.test.ts @@ -1,9 +1,10 @@ +import { withFileLock } from '@main/services/team/fileLock'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { withFileLock } from '@main/services/team/fileLock'; +const canAssertPosixPermissions = process.platform !== 'win32' && process.getuid?.() !== 0; describe('withFileLock', () => { let tmpDir: string; @@ -111,4 +112,24 @@ describe('withFileLock', () => { expect(result).toBe('created'); expect(fs.existsSync(`${nested}.lock`)).toBe(false); }); + + it.skipIf(!canAssertPosixPermissions)( + 'rethrows fatal errors while creating missing lock directory', + async () => { + const readonlyDir = path.join(tmpDir, 'readonly'); + fs.mkdirSync(readonlyDir, 0o555); + const nested = path.join(readonlyDir, 'missing', 'test.json'); + + try { + await expect( + withFileLock(nested, async () => 'ok', { + acquireTimeoutMs: 25, + retryIntervalMs: 1, + }) + ).rejects.toMatchObject({ code: 'EACCES' }); + } finally { + fs.chmodSync(readonlyDir, 0o755); + } + } + ); });