From 861e902d7f460e9ed0320ff2f808d23ec53e4e30 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne Date: Wed, 13 Jul 2022 12:10:29 -0400 Subject: [PATCH] Allow directories to be used as lockfiles Some libraries, like [proper-lockfile](https://www.npmjs.com/package/proper-lockfile) use directories instead of files for locking. This PR allows the supervisor to be able to work with those types of locks when lock override is requested. Closes: #1978 Change-type: patch --- src/lib/lockfile.ts | 27 ++++++++++++++++++++++----- test/src/lib/lockfile.spec.ts | 14 ++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/lib/lockfile.ts b/src/lib/lockfile.ts index 67621631..149ce925 100644 --- a/src/lib/lockfile.ts +++ b/src/lib/lockfile.ts @@ -1,8 +1,8 @@ -import * as fs from 'fs'; +import { promises as fs, unlinkSync, rmdirSync } from 'fs'; import * as os from 'os'; import { dirname } from 'path'; -import { exec, unlinkAll } from './fs-utils'; +import { exec } from './fs-utils'; // Equivalent to `drwxrwxrwt` const STICKY_WRITE_PERMISSIONS = 0o1777; @@ -66,7 +66,7 @@ export async function lock(path: string, uid: number = os.userInfo().uid) { * `chmod` does not fail or throw if the directory already has the proper permissions. */ if (uid !== 0) { - await fs.promises.chmod(dirname(path), STICKY_WRITE_PERMISSIONS); + await fs.chmod(dirname(path), STICKY_WRITE_PERMISSIONS); } /** @@ -108,11 +108,28 @@ export async function lock(path: string, uid: number = os.userInfo().uid) { export async function unlock(path: string): Promise { // Removing the lockfile releases the lock - await unlinkAll(path); + await fs.unlink(path).catch((e) => { + // if the error is EPERM, the file is a directory + if (e.code === 'EPERM') { + return fs.rmdir(path).catch(() => { + // if the directory is not empty or something else + // happens, ignore + }); + } + // If the file does not exist or some other error + // happens, then ignore the error + }); // Remove lockfile's in-memory tracking of a file delete locksTaken[path]; } export function unlockSync(path: string) { - return fs.unlinkSync(path); + try { + return unlinkSync(path); + } catch (e) { + if (e.code === 'EPERM') { + return rmdirSync(path); + } + throw e; + } } diff --git a/test/src/lib/lockfile.spec.ts b/test/src/lib/lockfile.spec.ts index 36134586..0efa55d9 100644 --- a/test/src/lib/lockfile.spec.ts +++ b/test/src/lib/lockfile.spec.ts @@ -22,7 +22,7 @@ describe('lib/lockfile', () => { }, '7654321': { two: opts.createLock - ? { 'updates.lock': mock.file({ uid: LOCKFILE_UID }) } + ? { 'updates.lock': mock.directory({ uid: LOCKFILE_UID }) } : {}, }, }, @@ -127,9 +127,11 @@ describe('lib/lockfile', () => { await checkLockDirFiles(lockPath, { shouldExist: true }); await lockfile.unlock(lockPath); + await lockfile.unlock(lockPath2); // Verify lockfile removal await checkLockDirFiles(lockPath, { shouldExist: false }); + await checkLockDirFiles(lockPath2, { shouldExist: false }); }); it('should not error on async unlock if lockfile does not exist', async () => { @@ -149,11 +151,15 @@ describe('lib/lockfile', () => { mockDir({ createLock: true }); lockfile.unlockSync(lockPath); + lockfile.unlockSync(lockPath2); // Verify lockfile does not exist - return checkLockDirFiles(lockPath, { shouldExist: false }).catch((err) => { - expect.fail((err as Error)?.message ?? err); - }); + return Promise.all([ + checkLockDirFiles(lockPath, { shouldExist: false }).catch((err) => { + expect.fail((err as Error)?.message ?? err); + }), + checkLockDirFiles(lockPath2, { shouldExist: false }), + ]); }); it('should try to clean up existing locks on process exit', async () => {