Merge pull request #2251 from balena-io/1003-config-inject-umount

config inject/read/write: Fix umount errors with OS image files
This commit is contained in:
bulldozer-balena[bot] 2021-04-14 01:03:53 +00:00 committed by GitHub
commit f6e6d9ce8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 80 additions and 110 deletions

View File

@ -2330,7 +2330,7 @@ device type (Check available types with `balena devices supported`)
#### -d, --drive DRIVE
device filesystem or OS image location
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
## config read
@ -2350,7 +2350,7 @@ device type (Check available types with `balena devices supported`)
#### -d, --drive DRIVE
device filesystem or OS image location
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
## config reconfigure
@ -2370,7 +2370,7 @@ device type (Check available types with `balena devices supported`)
#### -d, --drive DRIVE
device filesystem or OS image location
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
#### -v, --advanced
@ -2405,7 +2405,7 @@ device type (Check available types with `balena devices supported`)
#### -d, --drive DRIVE
device filesystem or OS image location
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
# Preload

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -54,16 +54,8 @@ export default class ConfigInjectCmd extends Command {
public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
help: cf.help,
};
@ -76,12 +68,11 @@ export default class ConfigInjectCmd extends Command {
ConfigInjectCmd,
);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device/OS drive'));
await umountAsync(drive);
await safeUmount(drive);
const fs = await import('fs');
const configJSON = JSON.parse(

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -42,16 +42,8 @@ export default class ConfigReadCmd extends Command {
public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
help: cf.help,
};
@ -62,12 +54,11 @@ export default class ConfigReadCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
await umountAsync(drive);
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive, options.type);

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -42,16 +42,8 @@ export default class ConfigReconfigureCmd extends Command {
public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
advanced: flags.boolean({
description: 'show advanced commands',
char: 'v',
@ -66,16 +58,15 @@ export default class ConfigReconfigureCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
await umountAsync(drive);
await safeUmount(drive);
const config = await import('balena-config-json');
const { uuid } = await config.read(drive, options.type);
await umountAsync(drive);
await safeUmount(drive);
const configureCommand = ['os', 'configure', drive, '--device', uuid];
if (options.advanced) {

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -61,16 +61,8 @@ export default class ConfigWriteCmd extends Command {
public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
drive: flags.string({
description: 'device filesystem or OS image location',
char: 'd',
}),
type: cf.deviceType,
drive: cf.driveOrImg,
help: cf.help,
};
@ -83,12 +75,11 @@ export default class ConfigWriteCmd extends Command {
ConfigWriteCmd,
);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { safeUmount } = await import('../../utils/helpers');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
await umountAsync(drive);
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive, options.type);
@ -97,7 +88,7 @@ export default class ConfigWriteCmd extends Command {
const _ = await import('lodash');
_.set(configJSON, params.key, params.value);
await umountAsync(drive);
await safeUmount(drive);
await config.write(drive, options.type, configJSON);

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -61,20 +61,16 @@ export default class LocalConfigureCmd extends Command {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
const path = await import('path');
const umount = await import('umount');
const umountAsync = promisify(umount.umount);
const isMountedAsync = promisify(umount.isMounted);
const reconfix = await import('reconfix');
const denymount = promisify(await import('denymount'));
const { safeUmount } = await import('../../utils/helpers');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const configurationSchema = await this.prepareConnectionFile(params.target);
if (await isMountedAsync(params.target)) {
await umountAsync(params.target);
}
await safeUmount(params.target);
const dmOpts: any = {};
if (process.pkg) {

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -61,12 +61,7 @@ export default class OsInitializeCmd extends Command {
public static usage = 'os initialize <image>';
public static flags: flags.Input<FlagsDef> = {
type: flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
}),
type: cf.deviceType,
drive: cf.drive,
yes: cf.yes,
help: cf.help,
@ -79,9 +74,9 @@ export default class OsInitializeCmd extends Command {
OsInitializeCmd,
);
const { promisify } = await import('util');
const umountAsync = promisify((await import('umount')).umount);
const { getManifest, sudo } = await import('../../utils/helpers');
const { getManifest, safeUmount, sudo } = await import(
'../../utils/helpers'
);
console.info(`Initializing device ${INIT_WARNING_MESSAGE}`);
@ -101,7 +96,7 @@ export default class OsInitializeCmd extends Command {
`Going to erase ${answers.drive}.`,
true,
);
await umountAsync(answers.drive);
await safeUmount(answers.drive);
}
await sudo([
@ -113,22 +108,7 @@ export default class OsInitializeCmd extends Command {
]);
if (answers.drive != null) {
// TODO: balena local makes use of ejectAsync, see below
// DO we need this / should we do that here?
// getDrive = (drive) ->
// driveListAsync().then (drives) ->
// selectedDrive = _.find(drives, device: drive)
// if not selectedDrive?
// throw new Error("Drive not found: #{drive}")
// return selectedDrive
// if (os.platform() is 'win32') and selectedDrive.mountpoint?
// ejectAsync = Promise.promisify(require('removedrive').eject)
// return ejectAsync(selectedDrive.mountpoint)
await umountAsync(answers.drive);
await safeUmount(answers.drive);
console.info(`You can safely remove ${answers.drive} now`);
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019 Balena Ltd.
* Copyright 2019-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -82,6 +82,19 @@ export const drive = flags.string({
`,
});
export const driveOrImg = flags.string({
char: 'd',
description:
'path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)',
});
export const deviceType = flags.string({
description:
'device type (Check available types with `balena devices supported`)',
char: 't',
required: true,
});
export const json: IBooleanFlag<boolean> = flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2020 Balena
Copyright 2016-2021 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,16 +16,11 @@ limitations under the License.
import type { InitializeEmitter, OperationState } from 'balena-device-init';
import type * as BalenaSdk from 'balena-sdk';
import { spawn, SpawnOptions } from 'child_process';
import * as _ from 'lodash';
import * as os from 'os';
import type * as ShellEscape from 'shell-escape';
import type { Device, PineOptions } from 'balena-sdk';
import { ExpectedError, SIGINTError } from '../errors';
import { getBalenaSdk, getChalk, getVisuals } from './lazy';
import * as _ from 'lodash';
import { promisify } from 'util';
import { isSubcommand } from '../preparser';
import { getBalenaSdk, getChalk, getVisuals } from './lazy';
export function getGroupDefaults(group: {
options: Array<{ name: string; default: string | number }>;
@ -84,7 +79,7 @@ export async function sudo(
) {
const { executeWithPrivileges } = await import('./sudo');
if (os.platform() !== 'win32') {
if (process.platform !== 'win32') {
console.log(
msg ||
'Admin privileges required: you may be asked for your computer password to continue.',
@ -95,6 +90,9 @@ export async function sudo(
}
export function runCommand<T>(commandArgs: string[]): Promise<T> {
const {
isSubcommand,
} = require('../preparser') as typeof import('../preparser');
if (isSubcommand(commandArgs)) {
commandArgs = [
commandArgs[0] + ':' + commandArgs[1],
@ -238,6 +236,7 @@ export async function retry<T>({
backoffScaler?: number;
maxSingleDelayMs?: number;
}): Promise<T> {
const { SIGINTError } = await import('../errors');
let delayMs = initialDelayMs;
for (let count = 0; count < maxAttempts - 1; count++) {
const lastAttemptMs = Date.now();
@ -348,7 +347,7 @@ export function shellEscape(args: string[], detectShell = false): string[] {
if (isCmdExe) {
return args.map((v) => windowsCmdExeEscapeArg(v));
} else {
const shellEscapeFunc: typeof ShellEscape = require('shell-escape');
const shellEscapeFunc: typeof import('shell-escape') = require('shell-escape');
return args.map((v) => shellEscapeFunc([v]));
}
}
@ -392,6 +391,7 @@ export async function which(
} catch (err) {
if (err.code === 'ENOENT') {
if (rejectOnMissing) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`'${program}' program not found. Is it installed?`,
);
@ -422,9 +422,10 @@ export async function which(
export async function whichSpawn(
programName: string,
args: string[],
options: SpawnOptions = { stdio: 'inherit' },
options: import('child_process').SpawnOptions = { stdio: 'inherit' },
returnExitCodeOrSignal = false,
): Promise<[number | undefined, string | undefined]> {
const { spawn } = await import('child_process');
const program = await which(programName);
if (process.env.DEBUG) {
console.error(`[debug] [${program}, ${args.join(', ')}]`);
@ -510,7 +511,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
}
}
export const expandForAppName: PineOptions<Device> = {
export const expandForAppName: BalenaSdk.PineOptions<BalenaSdk.Device> = {
$expand: {
belongs_to__application: { $select: 'app_name' },
is_of__device_type: { $select: 'slug' },
@ -565,6 +566,9 @@ export async function awaitInterruptibleTask<
let sigintHandler: () => void = () => undefined;
const sigintPromise = new Promise<T>((_resolve, reject) => {
sigintHandler = () => {
const {
SIGINTError,
} = require('../errors') as typeof import('../errors');
reject(new SIGINTError('Task aborted on SIGINT signal'));
};
addSIGINTHandler(sigintHandler);
@ -575,3 +579,16 @@ export async function awaitInterruptibleTask<
process.removeListener('SIGINT', sigintHandler);
}
}
/** Check if `drive` is mounted, and if so umount it. No-op on Windows. */
export async function safeUmount(drive: string) {
if (!drive) {
return;
}
const { isMounted, umount } = await import('umount');
const isMountedAsync = promisify(isMounted);
if (await isMountedAsync(drive)) {
const umountAsync = promisify(umount);
await umountAsync(drive);
}
}