Allow users to override HUP lock if device is stuck in invalid state

This functionality is needed when breadcrumbs aren't deleted after a HUP
rollback for whatever reason. Also rename HUP lock function.

Change-type: patch
Connects-to: #1459
Signed-off-by: Christina Wang <christina@balena.io>
This commit is contained in:
Christina Wang 2021-07-02 10:52:44 +09:00
parent 3caf608158
commit 17e740a4ba
No known key found for this signature in database
GPG Key ID: 7C3ED0230F440835
4 changed files with 23 additions and 10 deletions

View File

@ -638,7 +638,7 @@ export function reportCurrentState(
} }
export async function reboot(force?: boolean, skipLock?: boolean) { export async function reboot(force?: boolean, skipLock?: boolean) {
await updateLock.ensureNoHUPBreadcrumbsOnHost(); await updateLock.abortIfHUPInProgress({ force });
await applicationManager.stopAll({ force, skipLock }); await applicationManager.stopAll({ force, skipLock });
logger.logSystemMessage('Rebooting', {}, 'Reboot'); logger.logSystemMessage('Rebooting', {}, 'Reboot');
const $reboot = await dbus.reboot(); const $reboot = await dbus.reboot();
@ -648,7 +648,7 @@ export async function reboot(force?: boolean, skipLock?: boolean) {
} }
export async function shutdown(force?: boolean, skipLock?: boolean) { export async function shutdown(force?: boolean, skipLock?: boolean) {
await updateLock.ensureNoHUPBreadcrumbsOnHost(); await updateLock.abortIfHUPInProgress({ force });
await applicationManager.stopAll({ force, skipLock }); await applicationManager.stopAll({ force, skipLock });
logger.logSystemMessage('Shutting down', {}, 'Shutdown'); logger.logSystemMessage('Shutting down', {}, 'Shutdown');
const $shutdown = await dbus.shutdown(); const $shutdown = await dbus.shutdown();

View File

@ -42,7 +42,11 @@ function lockFilesOnHost(appId: number, serviceName: string): string[] {
* prevent reboot. If the Supervisor reboots while those services are still running, * prevent reboot. If the Supervisor reboots while those services are still running,
* the device may become stuck in an invalid state during HUP. * the device may become stuck in an invalid state during HUP.
*/ */
export function ensureNoHUPBreadcrumbsOnHost(): Promise<boolean | never> { export function abortIfHUPInProgress({
force = false,
}: {
force: boolean | undefined;
}): Promise<boolean | never> {
return Promise.all( return Promise.all(
[ [
'rollback-health-breadcrumb', 'rollback-health-breadcrumb',
@ -52,7 +56,7 @@ export function ensureNoHUPBreadcrumbsOnHost(): Promise<boolean | never> {
), ),
).then((existsArray) => { ).then((existsArray) => {
const anyExists = existsArray.some((exists) => exists); const anyExists = existsArray.some((exists) => exists);
if (anyExists) { if (anyExists && !force) {
throw new UpdatesLockedError('Waiting for Host OS update to finish'); throw new UpdatesLockedError('Waiting for Host OS update to finish');
} }
return anyExists; return anyExists;

View File

@ -359,7 +359,7 @@ describe('deviceState', () => {
it('prevents reboot or shutdown when HUP rollback breadcrumbs are present', async () => { it('prevents reboot or shutdown when HUP rollback breadcrumbs are present', async () => {
const testErrMsg = 'Waiting for Host OS updates to finish'; const testErrMsg = 'Waiting for Host OS updates to finish';
stub(updateLock, 'ensureNoHUPBreadcrumbsOnHost').throws( stub(updateLock, 'abortIfHUPInProgress').throws(
new UpdatesLockedError(testErrMsg), new UpdatesLockedError(testErrMsg),
); );
@ -370,6 +370,6 @@ describe('deviceState', () => {
.to.eventually.be.rejectedWith(testErrMsg) .to.eventually.be.rejectedWith(testErrMsg)
.and.be.an.instanceOf(UpdatesLockedError); .and.be.an.instanceOf(UpdatesLockedError);
(updateLock.ensureNoHUPBreadcrumbsOnHost as SinonStub).restore(); (updateLock.abortIfHUPInProgress as SinonStub).restore();
}); });
}); });

View File

@ -79,24 +79,33 @@ describe('lib/update-lock', () => {
}); });
}); });
describe('ensureNoHUPBreadcrumbsOnHost', () => { describe('abortIfHUPInProgress', () => {
afterEach(() => mockFs.restore()); afterEach(() => mockFs.restore());
it('should throw if any breadcrumbs exist on host', async () => { it('should throw if any breadcrumbs exist on host', async () => {
for (const bc of breadcrumbFiles) { for (const bc of breadcrumbFiles) {
mockBreadcrumbs(bc); mockBreadcrumbs(bc);
await expect(updateLock.ensureNoHUPBreadcrumbsOnHost()) await expect(updateLock.abortIfHUPInProgress({ force: false }))
.to.eventually.be.rejectedWith('Waiting for Host OS update to finish') .to.eventually.be.rejectedWith('Waiting for Host OS update to finish')
.and.be.an.instanceOf(UpdatesLockedError); .and.be.an.instanceOf(UpdatesLockedError);
} }
}); });
it('should resolve to true if no breadcrumbs on host', async () => { it('should resolve to false if no breadcrumbs on host', async () => {
mockBreadcrumbs(); mockBreadcrumbs();
await expect( await expect(
updateLock.ensureNoHUPBreadcrumbsOnHost(), updateLock.abortIfHUPInProgress({ force: false }),
).to.eventually.equal(false); ).to.eventually.equal(false);
}); });
it('should resolve to true if breadcrumbs are on host but force is passed', async () => {
for (const bc of breadcrumbFiles) {
mockBreadcrumbs(bc);
await expect(
updateLock.abortIfHUPInProgress({ force: true }),
).to.eventually.equal(true);
}
});
}); });
describe('Lock/dispose functionality', () => { describe('Lock/dispose functionality', () => {