mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-01 15:20:51 +00:00
Refactor lockfile module
Updated interfaces for clarity Change-type: patch
This commit is contained in:
parent
0a9de69994
commit
d8f54c05e7
@ -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;
|
||||||
|
@ -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,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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`);
|
||||||
|
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user