Merge pull request #1984 from balena-os/lockdir

Allow directories to be used as lockfiles
This commit is contained in:
bulldozer-balena[bot] 2022-07-13 20:01:36 +00:00 committed by GitHub
commit 175d14258b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 * as os from 'os';
import { dirname } from 'path'; import { dirname } from 'path';
import { exec, unlinkAll } from './fs-utils'; import { exec } from './fs-utils';
// Equivalent to `drwxrwxrwt` // Equivalent to `drwxrwxrwt`
const STICKY_WRITE_PERMISSIONS = 0o1777; 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. * `chmod` does not fail or throw if the directory already has the proper permissions.
*/ */
if (uid !== 0) { 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> { export async function unlock(path: string): Promise<void> {
// Removing the lockfile releases the lock // 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 // Remove lockfile's in-memory tracking of a file
delete locksTaken[path]; delete locksTaken[path];
} }
export function unlockSync(path: string) { 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': { '7654321': {
two: opts.createLock 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 checkLockDirFiles(lockPath, { shouldExist: true });
await lockfile.unlock(lockPath); await lockfile.unlock(lockPath);
await lockfile.unlock(lockPath2);
// Verify lockfile removal // Verify lockfile removal
await checkLockDirFiles(lockPath, { shouldExist: false }); await checkLockDirFiles(lockPath, { shouldExist: false });
await checkLockDirFiles(lockPath2, { shouldExist: false });
}); });
it('should not error on async unlock if lockfile does not exist', async () => { it('should not error on async unlock if lockfile does not exist', async () => {
@ -149,11 +151,15 @@ describe('lib/lockfile', () => {
mockDir({ createLock: true }); mockDir({ createLock: true });
lockfile.unlockSync(lockPath); lockfile.unlockSync(lockPath);
lockfile.unlockSync(lockPath2);
// Verify lockfile does not exist // Verify lockfile does not exist
return checkLockDirFiles(lockPath, { shouldExist: false }).catch((err) => { return Promise.all([
checkLockDirFiles(lockPath, { shouldExist: false }).catch((err) => {
expect.fail((err as Error)?.message ?? err); expect.fail((err as Error)?.message ?? err);
}); }),
checkLockDirFiles(lockPath2, { shouldExist: false }),
]);
}); });
it('should try to clean up existing locks on process exit', async () => { it('should try to clean up existing locks on process exit', async () => {