2022-03-07 22:10:51 +00:00
|
|
|
import { expect } from 'chai';
|
|
|
|
import { dirname, basename } from 'path';
|
|
|
|
import { stub, SinonStub } from 'sinon';
|
|
|
|
import { promises as fs } from 'fs';
|
|
|
|
import mock = require('mock-fs');
|
|
|
|
|
2022-08-17 23:35:08 +00:00
|
|
|
import * as lockfile from '~/lib/lockfile';
|
|
|
|
import * as fsUtils from '~/lib/fs-utils';
|
2022-08-18 22:50:13 +00:00
|
|
|
const BASE_LOCK_DIR = '/tmp/balena-supervisor/services';
|
|
|
|
const LOCKFILE_UID = 65534;
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
describe('lib/lockfile', () => {
|
2022-04-11 21:07:36 +00:00
|
|
|
const lockPath = `${BASE_LOCK_DIR}/1234567/one/updates.lock`;
|
|
|
|
const lockPath2 = `${BASE_LOCK_DIR}/7654321/two/updates.lock`;
|
2022-03-07 22:10:51 +00:00
|
|
|
|
2022-04-07 04:49:01 +00:00
|
|
|
const mockDir = (opts: { createLock: boolean } = { createLock: false }) => {
|
2022-03-07 22:10:51 +00:00
|
|
|
mock({
|
2022-04-11 21:07:36 +00:00
|
|
|
[BASE_LOCK_DIR]: {
|
2022-04-07 04:49:01 +00:00
|
|
|
'1234567': {
|
|
|
|
one: opts.createLock
|
2022-04-11 21:07:36 +00:00
|
|
|
? { 'updates.lock': mock.file({ uid: LOCKFILE_UID }) }
|
2022-04-07 04:49:01 +00:00
|
|
|
: {},
|
|
|
|
},
|
|
|
|
'7654321': {
|
|
|
|
two: opts.createLock
|
2022-07-13 16:10:29 +00:00
|
|
|
? { 'updates.lock': mock.directory({ uid: LOCKFILE_UID }) }
|
2022-04-07 04:49:01 +00:00
|
|
|
: {},
|
|
|
|
},
|
|
|
|
},
|
2022-03-07 22:10:51 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const checkLockDirFiles = async (
|
|
|
|
path: string,
|
|
|
|
opts: { shouldExist: boolean } = { shouldExist: true },
|
|
|
|
) => {
|
|
|
|
const files = await fs.readdir(dirname(path));
|
|
|
|
if (opts.shouldExist) {
|
|
|
|
expect(files).to.include(basename(path));
|
|
|
|
} else {
|
|
|
|
expect(files).to.have.length(0);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let execStub: SinonStub;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
// @ts-ignore
|
|
|
|
execStub = stub(fsUtils, 'exec').callsFake(async (command, opts) => {
|
|
|
|
// Sanity check for the command call
|
|
|
|
expect(command.trim().startsWith('lockfile')).to.be.true;
|
|
|
|
|
|
|
|
// Remove any `lockfile` command options to leave just the command and the target filepath
|
|
|
|
const [, targetPath] = command
|
|
|
|
.replace(/-v|-nnn|-r\s+\d+|-l\s+\d+|-s\s+\d+|-!|-ml|-mu/g, '')
|
|
|
|
.split(/\s+/);
|
|
|
|
|
|
|
|
// Emulate the lockfile binary exec call
|
|
|
|
await fsUtils.touch(targetPath);
|
|
|
|
await fs.chown(targetPath, opts!.uid!, 0);
|
|
|
|
});
|
2022-04-07 04:49:01 +00:00
|
|
|
|
2022-04-11 21:07:36 +00:00
|
|
|
mock({ [BASE_LOCK_DIR]: {} });
|
2022-03-07 22:10:51 +00:00
|
|
|
});
|
|
|
|
|
2022-03-08 00:56:11 +00:00
|
|
|
afterEach(async () => {
|
2022-03-07 22:10:51 +00:00
|
|
|
execStub.restore();
|
2022-03-08 00:56:11 +00:00
|
|
|
|
|
|
|
// Even though mock-fs is restored, this is needed to delete any in-memory storage of locks
|
|
|
|
for (const lock of lockfile.getLocksTaken()) {
|
|
|
|
await lockfile.unlock(lock);
|
|
|
|
}
|
|
|
|
|
2022-03-07 22:10:51 +00:00
|
|
|
mock.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should create a lockfile as the `nobody` user at target path', async () => {
|
|
|
|
mockDir();
|
|
|
|
|
2022-04-11 21:07:36 +00:00
|
|
|
await lockfile.lock(lockPath, LOCKFILE_UID);
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
// Verify lockfile exists
|
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: true });
|
|
|
|
|
|
|
|
// Verify lockfile UID
|
2022-04-11 21:07:36 +00:00
|
|
|
expect((await fs.stat(lockPath)).uid).to.equal(LOCKFILE_UID);
|
2022-03-07 22:10:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should create a lockfile with the provided `uid` if specified', async () => {
|
|
|
|
mockDir();
|
|
|
|
|
|
|
|
await lockfile.lock(lockPath, 2);
|
|
|
|
|
|
|
|
// Verify lockfile exists
|
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: true });
|
|
|
|
|
|
|
|
// Verify lockfile UID
|
|
|
|
expect((await fs.stat(lockPath)).uid).to.equal(2);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not create a lockfile if `lock` throws', async () => {
|
|
|
|
mockDir();
|
|
|
|
|
|
|
|
// Override default exec stub declaration, as it normally emulates a lockfile call with
|
|
|
|
// no errors, but we want it to throw an error just for this unit test
|
|
|
|
execStub.restore();
|
|
|
|
|
2022-03-08 00:56:11 +00:00
|
|
|
const childProcessError = new lockfile.LockfileExistsError(
|
|
|
|
'/tmp/test/path',
|
|
|
|
);
|
2022-03-07 22:10:51 +00:00
|
|
|
execStub = stub(fsUtils, 'exec').throws(childProcessError);
|
|
|
|
|
|
|
|
try {
|
2022-04-11 21:07:36 +00:00
|
|
|
await lockfile.lock(lockPath, LOCKFILE_UID);
|
2022-03-08 00:56:11 +00:00
|
|
|
expect.fail('lockfile.lock should throw an error');
|
2022-03-07 22:10:51 +00:00
|
|
|
} catch (err) {
|
|
|
|
expect(err).to.exist;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify lockfile does not exist
|
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: false });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should asynchronously unlock a lockfile', async () => {
|
2022-04-07 04:49:01 +00:00
|
|
|
mockDir({ createLock: true });
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
// Verify lockfile exists
|
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: true });
|
|
|
|
|
|
|
|
await lockfile.unlock(lockPath);
|
2022-07-13 16:10:29 +00:00
|
|
|
await lockfile.unlock(lockPath2);
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
// Verify lockfile removal
|
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: false });
|
2022-07-13 16:10:29 +00:00
|
|
|
await checkLockDirFiles(lockPath2, { shouldExist: false });
|
2022-03-07 22:10:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should not error on async unlock if lockfile does not exist', async () => {
|
2022-04-07 04:49:01 +00:00
|
|
|
mockDir({ createLock: false });
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
// Verify lockfile does not exist
|
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: false });
|
|
|
|
|
|
|
|
try {
|
|
|
|
await lockfile.unlock(lockPath);
|
|
|
|
} catch (err) {
|
|
|
|
expect.fail((err as Error)?.message ?? err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should synchronously unlock a lockfile', () => {
|
2022-04-07 04:49:01 +00:00
|
|
|
mockDir({ createLock: true });
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
lockfile.unlockSync(lockPath);
|
2022-07-13 16:10:29 +00:00
|
|
|
lockfile.unlockSync(lockPath2);
|
2022-03-07 22:10:51 +00:00
|
|
|
|
|
|
|
// Verify lockfile does not exist
|
2022-07-13 16:10:29 +00:00
|
|
|
return Promise.all([
|
|
|
|
checkLockDirFiles(lockPath, { shouldExist: false }).catch((err) => {
|
|
|
|
expect.fail((err as Error)?.message ?? err);
|
|
|
|
}),
|
|
|
|
checkLockDirFiles(lockPath2, { shouldExist: false }),
|
|
|
|
]);
|
2022-03-07 22:10:51 +00:00
|
|
|
});
|
2022-03-08 00:56:11 +00:00
|
|
|
|
|
|
|
it('should try to clean up existing locks on process exit', async () => {
|
2022-04-07 04:49:01 +00:00
|
|
|
mockDir({ createLock: false });
|
2022-03-08 00:56:11 +00:00
|
|
|
|
2022-04-07 04:49:01 +00:00
|
|
|
// Create lockfiles for multiple appId / uuids
|
2022-04-11 21:07:36 +00:00
|
|
|
await lockfile.lock(lockPath, LOCKFILE_UID);
|
|
|
|
await lockfile.lock(lockPath2, LOCKFILE_UID);
|
2022-03-08 00:56:11 +00:00
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
process.emit('exit');
|
|
|
|
|
2022-04-07 04:49:01 +00:00
|
|
|
// Verify lockfile removal regardless of appId / appUuid
|
2022-03-08 00:56:11 +00:00
|
|
|
await checkLockDirFiles(lockPath, { shouldExist: false });
|
2022-04-07 04:49:01 +00:00
|
|
|
await checkLockDirFiles(lockPath2, { shouldExist: false });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should list locks taken according to a filter function', async () => {
|
|
|
|
mockDir({ createLock: false });
|
|
|
|
|
|
|
|
// Create lockfiles for multiple appId / uuids
|
2022-04-11 21:07:36 +00:00
|
|
|
await lockfile.lock(lockPath, LOCKFILE_UID);
|
|
|
|
await lockfile.lock(lockPath2, LOCKFILE_UID);
|
2022-04-07 04:49:01 +00:00
|
|
|
|
|
|
|
expect(
|
|
|
|
lockfile.getLocksTaken((path) => path.includes('1234567')),
|
|
|
|
).to.have.members([lockPath]);
|
|
|
|
expect(
|
|
|
|
lockfile.getLocksTaken((path) => path.includes('7654321')),
|
|
|
|
).to.have.members([lockPath2]);
|
|
|
|
expect(lockfile.getLocksTaken()).to.have.members([lockPath, lockPath2]);
|
2022-03-08 00:56:11 +00:00
|
|
|
});
|
2022-03-07 22:10:51 +00:00
|
|
|
});
|