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
This commit is contained in:
Felipe Lalanne 2022-07-13 12:10:29 -04:00
parent d11d4fba91
commit 861e902d7f
2 changed files with 32 additions and 9 deletions

View File

@ -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<void> {
// 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;
}
}

View File

@ -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 () => {