balena-cli/lib/utils/umount.ts
Paulo Castro e624726e44 config write: Fix EBUSY error on macOS
Change-type: patch
2021-07-21 23:56:57 +01:00

154 lines
4.3 KiB
TypeScript

/**
* @license
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This module was inspired by the npm `umount` package:
* https://www.npmjs.com/package/umount
* With some important changes:
* - Fix "Command Injection" security advisory 1512
* https://www.npmjs.com/advisories/1512
* - Port from CoffeeScript to TypeScript
* - Convert callbacks to async/await
*/
import * as child_process from 'child_process';
import * as path from 'path';
import { promisify } from 'util';
const execFile = promisify(child_process.execFile);
/**
* Unmount a device on Linux or macOS. No-op on Windows.
* @param device Device path, e.g. '/dev/disk2'
*/
export async function umount(device: string): Promise<void> {
if (process.platform === 'win32') {
return;
}
const { sanitizePath, whichBin } = await import('./which');
// sanitize user's input (regular expression attacks ?)
device = sanitizePath(device);
const cmd: string[] = [];
if (process.platform === 'darwin') {
cmd.push('/usr/sbin/diskutil', 'unmountDisk', 'force', device);
} else {
// Linux
const glob = promisify(await import('glob'));
// '?*' expands a base device path like '/dev/sdb' to an array of paths
// like '/dev/sdb1', '/dev/sdb2', ..., '/dev/sdb11', ... (partitions)
// that exist for balenaOS images and are needed as arguments to 'umount'
// on Linux (otherwise, umount produces an error "/dev/sdb: not mounted")
const devices = await glob(`${device}?*`, { nodir: true, nonull: true });
cmd.push(await whichBin('umount'), ...devices);
}
if (cmd.length > 1) {
let stderr = '';
try {
const proc = await execFile(cmd[0], cmd.slice(1));
stderr = proc.stderr;
} catch (err) {
const msg = [
'',
`Error executing "${cmd.join(' ')}"`,
stderr || '',
err.message || '',
];
if (process.platform === 'linux') {
// ignore errors like: "umount: /dev/sdb4: not mounted."
if (process.env.DEBUG) {
console.error(msg.join('\n[debug] '));
}
return;
}
const { ExpectedError } = await import('../errors');
throw new ExpectedError(msg.join('\n'));
}
}
}
/**
* Check if a device is mounted on Linux or macOS. Always true on Windows.
* @param device Device path, e.g. '/dev/disk2'
*/
export async function isMounted(device: string): Promise<boolean> {
if (process.platform === 'win32') {
return true;
}
if (!device) {
return false;
}
const { whichBin } = await import('./which');
const mountCmd = await whichBin('mount');
let stdout = '';
let stderr = '';
try {
const proc = await execFile(mountCmd);
stdout = proc.stdout;
stderr = proc.stderr;
} catch (err) {
const { ExpectedError } = await import('../errors');
throw new ExpectedError(
`Error executing "${mountCmd}":\n${stderr}\n${err.message}`,
);
}
const result = (stdout || '')
.split('\n')
.some((line) => line.startsWith(device));
return result;
}
/** Check if `drive` is mounted and, if so, umount it. No-op on Windows. */
export async function safeUmount(drive: string) {
if (!drive) {
return;
}
if (await isMounted(drive)) {
await umount(drive);
}
}
/**
* Wrapper around the `denymount` package. See:
* https://github.com/balena-io-modules/denymount
*/
export async function denyMount(
target: string,
handler: () => any,
opts: { autoMountOnSuccess?: boolean; executablePath?: string } = {},
) {
const denymount = promisify(await import('denymount'));
if (process.pkg) {
// when running in a standalone pkg install, the 'denymount'
// executable is placed on the same folder as process.execPath
opts.executablePath ||= path.join(
path.dirname(process.execPath),
'denymount',
);
}
const dmHandler = async (cb: (err?: Error) => void) => {
let err: Error | undefined;
try {
await handler();
} catch (e) {
err = e;
}
cb(err);
};
await denymount(target, dmHandler, opts);
}