Refactor lockfile module

Updated interfaces for clarity

Change-type: patch
This commit is contained in:
Felipe Lalanne 2024-11-15 18:25:50 -03:00
parent 0a9de69994
commit d8f54c05e7
No known key found for this signature in database
GPG Key ID: 03E696BFD472B26A
4 changed files with 83 additions and 62 deletions

View File

@ -1,5 +1,4 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import type { Stats, Dirent } from 'fs';
import os from 'os'; import os from 'os';
import { dirname } from 'path'; import { dirname } from 'path';
@ -9,38 +8,68 @@ import { isENOENT, isEISDIR, isEPERM } from './errors';
// Equivalent to `drwxrwxrwt` // Equivalent to `drwxrwxrwt`
const STICKY_WRITE_PERMISSIONS = 0o1777; const STICKY_WRITE_PERMISSIONS = 0o1777;
interface LockInfo {
/**
* The lock file path
*/
path: string;
/**
* The linux user id (uid) of the
* lock
*/
owner: number;
}
interface FindAllArgs {
root: string;
filter: (lock: LockInfo) => boolean;
recursive: boolean;
}
// Returns all current locks taken under a directory (default: /tmp) // Returns all current locks taken under a directory (default: /tmp)
// Optionally accepts filter function for only getting locks that match a condition. // Optionally accepts filter function for only getting locks that match a condition.
// A file is counted as a lock by default if it ends with `.lock`. // A file is counted as a lock by default if it ends with `.lock`.
export const getLocksTaken = async ( export async function findAll({
rootDir: string = '/tmp', root = '/tmp',
lockFilter: (path: string, stat: Stats) => boolean = (p) => filter = (l) => l.path.endsWith('.lock'),
p.endsWith('.lock'), recursive = true,
): Promise<string[]> => { }: Partial<FindAllArgs>): Promise<string[]> {
const locksTaken: string[] = []; // Queue of directories to search
let filesOrDirs: Dirent[] = []; const queue: string[] = [root];
try { const locks: string[] = [];
filesOrDirs = await fs.readdir(rootDir, { withFileTypes: true });
} catch (err) { while (queue.length > 0) {
// If lockfile directory doesn't exist, no locks are taken root = queue.shift()!;
if (isENOENT(err)) { try {
return locksTaken; const contents = await fs.readdir(root, { withFileTypes: true });
for (const file of contents) {
const path = `${root}/${file.name}`;
const stats = await fs.lstat(path);
// A lock is taken if it's a file or directory within root dir that passes filter fn.
// We also don't want to follow symlinks since we don't want to follow the lock to
// the target path if it's a symlink and only care that it exists or not.
if (filter({ path, owner: stats.uid })) {
locks.push(path);
} else if (file.isDirectory() && recursive) {
// Otherwise, if non-lock directory, seek locks recursively within directory
queue.push(path);
}
}
} catch (err) {
// if file of directory does not exist continue the search
// the file, could have been deleted after starting the call
// to findAll
if (isENOENT(err)) {
continue;
}
throw err;
} }
} }
for (const fileOrDir of filesOrDirs) {
const lockPath = `${rootDir}/${fileOrDir.name}`; return locks;
// A lock is taken if it's a file or directory within rootDir that passes filter fn. }
// We also don't want to follow symlinks since we don't want to follow the lock to
// the target path if it's a symlink and only care that it exists or not.
if (lockFilter(lockPath, await fs.lstat(lockPath))) {
locksTaken.push(lockPath);
// Otherwise, if non-lock directory, seek locks recursively within directory
} else if (fileOrDir.isDirectory()) {
locksTaken.push(...(await getLocksTaken(lockPath, lockFilter)));
}
}
return locksTaken;
};
interface ChildProcessError { interface ChildProcessError {
code: number; code: number;

View File

@ -1,6 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import type { Stats } from 'fs';
import { isRight } from 'fp-ts/lib/Either'; import { isRight } from 'fp-ts/lib/Either';
import { import {
@ -212,11 +211,10 @@ export class LocksTakenMap extends Map<number, Set<string>> {
export async function getLocksTaken( export async function getLocksTaken(
rootDir: string = pathOnRoot(BASE_LOCK_DIR), rootDir: string = pathOnRoot(BASE_LOCK_DIR),
): Promise<string[]> { ): Promise<string[]> {
return await lockfile.getLocksTaken( return await lockfile.findAll({
rootDir, root: rootDir,
(p: string, s: Stats) => filter: (l) => l.path.endsWith('updates.lock') && l.owner === LOCKFILE_UID,
p.endsWith('updates.lock') && s.uid === LOCKFILE_UID, });
);
} }
/** /**

View File

@ -165,7 +165,7 @@ describe('lib/lockfile', () => {
await Promise.all(locks.map((lock) => lockfile.lock(lock))); await Promise.all(locks.map((lock) => lockfile.lock(lock)));
// Assert all locks are listed as taken // Assert all locks are listed as taken
expect(await lockfile.getLocksTaken(lockdir)).to.have.members( expect(await lockfile.findAll({ root: lockdir })).to.have.members(
locks.concat([`${lockdir}/other.lock`]), locks.concat([`${lockdir}/other.lock`]),
); );
@ -194,30 +194,23 @@ describe('lib/lockfile', () => {
// Assert appropriate locks are listed as taken... // Assert appropriate locks are listed as taken...
// - with a specific UID // - with a specific UID
expect( expect(
await lockfile.getLocksTaken( await lockfile.findAll({
lockdir, root: lockdir,
(p, stats) => p.endsWith('.lock') && stats.uid === NOBODY_UID, filter: (lock) =>
), lock.path.endsWith('.lock') && lock.owner === NOBODY_UID,
}),
).to.have.members([ ).to.have.members([
`${lockdir}/updates.lock`, `${lockdir}/updates.lock`,
`${lockdir}/1/resin-updates.lock`, `${lockdir}/1/resin-updates.lock`,
`${lockdir}/other.lock`, `${lockdir}/other.lock`,
]); ]);
// - as a directory
expect(
await lockfile.getLocksTaken(
lockdir,
(p, stats) => p.endsWith('.lock') && stats.isDirectory(),
),
).to.have.members([
`${lockdir}/1/updates.lock`,
`${lockdir}/1/resin-updates.lock`,
]);
// - under a different root dir from default // - under a different root dir from default
expect( expect(
await lockfile.getLocksTaken(`${lockdir}/services`, (p) => await lockfile.findAll({
p.endsWith('.lock'), root: `${lockdir}/services`,
), filter: (lock) => lock.path.endsWith('.lock'),
}),
).to.have.members([ ).to.have.members([
`${lockdir}/services/main/updates.lock`, `${lockdir}/services/main/updates.lock`,
`${lockdir}/services/aux/resin-updates.lock`, `${lockdir}/services/aux/resin-updates.lock`,
@ -233,9 +226,10 @@ describe('lib/lockfile', () => {
// Create symlink lock // Create symlink lock
await fs.symlink('/nonexistent', `${lockdir}/updates.lock`); await fs.symlink('/nonexistent', `${lockdir}/updates.lock`);
expect( expect(await lockfile.findAll({ root: lockdir })).to.have.members([
await lockfile.getLocksTaken(lockdir, (_p, s) => s.isSymbolicLink()), `${lockdir}/other.lock`,
).to.have.members([`${lockdir}/updates.lock`]); `${lockdir}/updates.lock`,
]);
// Cleanup symlink lock // Cleanup symlink lock
await fs.rm(`${lockdir}/updates.lock`); await fs.rm(`${lockdir}/updates.lock`);

View File

@ -634,9 +634,9 @@ describe('lib/update-lock', () => {
// Take lock for second service of two services // Take lock for second service of two services
await lockfile.lock(`${lockdir}/1/${svcs[1]}/updates.lock`); await lockfile.lock(`${lockdir}/1/${svcs[1]}/updates.lock`);
expect(await lockfile.getLocksTaken(lockdir)).to.deep.include.members([ expect(
`${lockdir}/1/${svcs[1]}/updates.lock`, await lockfile.findAll({ root: lockdir }),
]); ).to.deep.include.members([`${lockdir}/1/${svcs[1]}/updates.lock`]);
// Watch for added files, as Supervisor-taken locks should be added // Watch for added files, as Supervisor-taken locks should be added
// then removed within updateLock.takeLock // then removed within updateLock.takeLock
@ -656,16 +656,16 @@ describe('lib/update-lock', () => {
// ..but upon error, Supervisor-taken locks should have been cleaned up // ..but upon error, Supervisor-taken locks should have been cleaned up
expect( expect(
await lockfile.getLocksTaken(lockdir), await lockfile.findAll({ root: lockdir }),
).to.not.deep.include.members([ ).to.not.deep.include.members([
`${lockdir}/1/${svcs[0]}/updates.lock`, `${lockdir}/1/${svcs[0]}/updates.lock`,
`${lockdir}/1/${svcs[0]}/resin-updates.lock`, `${lockdir}/1/${svcs[0]}/resin-updates.lock`,
]); ]);
// User lock should be left behind // User lock should be left behind
expect(await lockfile.getLocksTaken(lockdir)).to.deep.include.members([ expect(
`${lockdir}/1/${svcs[1]}/updates.lock`, await lockfile.findAll({ root: lockdir }),
]); ).to.deep.include.members([`${lockdir}/1/${svcs[1]}/updates.lock`]);
// Clean up watcher // Clean up watcher
await watcher.close(); await watcher.close();