diff --git a/lib/commands/config/inject.ts b/lib/commands/config/inject.ts index dce9a3bd..aa62a80a 100644 --- a/lib/commands/config/inject.ts +++ b/lib/commands/config/inject.ts @@ -68,7 +68,7 @@ export default class ConfigInjectCmd extends Command { ConfigInjectCmd, ); - const { safeUmount } = await import('../../utils/helpers'); + const { safeUmount } = await import('../../utils/umount'); const drive = options.drive || (await getVisuals().drive('Select the device/OS drive')); diff --git a/lib/commands/config/read.ts b/lib/commands/config/read.ts index c6dd1377..79cf0670 100644 --- a/lib/commands/config/read.ts +++ b/lib/commands/config/read.ts @@ -54,7 +54,7 @@ export default class ConfigReadCmd extends Command { public async run() { const { flags: options } = this.parse(ConfigReadCmd); - const { safeUmount } = await import('../../utils/helpers'); + const { safeUmount } = await import('../../utils/umount'); const drive = options.drive || (await getVisuals().drive('Select the device drive')); diff --git a/lib/commands/config/reconfigure.ts b/lib/commands/config/reconfigure.ts index e09adcbf..8b9edcc9 100644 --- a/lib/commands/config/reconfigure.ts +++ b/lib/commands/config/reconfigure.ts @@ -58,7 +58,7 @@ export default class ConfigReconfigureCmd extends Command { public async run() { const { flags: options } = this.parse(ConfigReconfigureCmd); - const { safeUmount } = await import('../../utils/helpers'); + const { safeUmount } = await import('../../utils/umount'); const drive = options.drive || (await getVisuals().drive('Select the device drive')); diff --git a/lib/commands/config/write.ts b/lib/commands/config/write.ts index 2de46682..8261bafd 100644 --- a/lib/commands/config/write.ts +++ b/lib/commands/config/write.ts @@ -75,7 +75,7 @@ export default class ConfigWriteCmd extends Command { ConfigWriteCmd, ); - const { safeUmount } = await import('../../utils/helpers'); + const { safeUmount } = await import('../../utils/umount'); const drive = options.drive || (await getVisuals().drive('Select the device drive')); diff --git a/lib/commands/local/configure.ts b/lib/commands/local/configure.ts index 02659d12..78a72b1b 100644 --- a/lib/commands/local/configure.ts +++ b/lib/commands/local/configure.ts @@ -63,7 +63,7 @@ export default class LocalConfigureCmd extends Command { const path = await import('path'); const reconfix = await import('reconfix'); const denymount = promisify(await import('denymount')); - const { safeUmount } = await import('../../utils/helpers'); + const { safeUmount } = await import('../../utils/umount'); const Logger = await import('../../utils/logger'); const logger = Logger.getLogger(); diff --git a/lib/commands/os/initialize.ts b/lib/commands/os/initialize.ts index 5deca04a..718da94b 100644 --- a/lib/commands/os/initialize.ts +++ b/lib/commands/os/initialize.ts @@ -74,9 +74,7 @@ export default class OsInitializeCmd extends Command { OsInitializeCmd, ); - const { getManifest, safeUmount, sudo } = await import( - '../../utils/helpers' - ); + const { getManifest, sudo } = await import('../../utils/helpers'); console.info(`Initializing device ${INIT_WARNING_MESSAGE}`); @@ -96,6 +94,7 @@ export default class OsInitializeCmd extends Command { `Going to erase ${answers.drive}.`, true, ); + const { safeUmount } = await import('../../utils/umount'); await safeUmount(answers.drive); } @@ -108,6 +107,7 @@ export default class OsInitializeCmd extends Command { ]); if (answers.drive != null) { + const { safeUmount } = await import('../../utils/umount'); await safeUmount(answers.drive); console.info(`You can safely remove ${answers.drive} now`); } diff --git a/lib/commands/ssh.ts b/lib/commands/ssh.ts index 0ebcddf7..23dcd11a 100644 --- a/lib/commands/ssh.ts +++ b/lib/commands/ssh.ts @@ -136,7 +136,7 @@ export default class SshCmd extends Command { } // Remote connection - const { getProxyConfig, which } = await import('../utils/helpers'); + const { getProxyConfig } = await import('../utils/helpers'); const { getOnlineTargetDeviceUuid } = await import('../utils/patterns'); const sdk = getBalenaSdk(); @@ -156,6 +156,7 @@ export default class SshCmd extends Command { const deviceId = device.id; const supervisorVersion = device.supervisor_version; + const { which } = await import('../utils/which'); const [whichProxytunnel, username, proxyUrl] = await Promise.all([ useProxy ? which('proxytunnel', false) : undefined, @@ -301,7 +302,7 @@ export default class SshCmd extends Command { // container const childProcess = await import('child_process'); const { escapeRegExp } = await import('lodash'); - const { which } = await import('../utils/helpers'); + const { which } = await import('../utils/which'); const { deviceContainerEngineBinary } = await import( '../utils/device/ssh' ); diff --git a/lib/utils/compose_ts.ts b/lib/utils/compose_ts.ts index 7d223ea7..eba51143 100644 --- a/lib/utils/compose_ts.ts +++ b/lib/utils/compose_ts.ts @@ -42,6 +42,7 @@ import { import type { DeviceInfo } from './device/api'; import { getBalenaSdk, getChalk, stripIndent } from './lazy'; import Logger = require('./logger'); +import { exists } from './which'; /** * Given an array representing the raw `--release-tag` flag of the deploy and @@ -98,15 +99,6 @@ export async function applyReleaseTagKeysAndValues( ); } -const exists = async (filename: string) => { - try { - await fs.access(filename); - return true; - } catch { - return false; - } -}; - const LOG_LENGTH_MAX = 512 * 1024; // 512KB const compositionFileNames = ['docker-compose.yml', 'docker-compose.yaml']; const hr = diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 8772dcc3..9ca11d96 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -405,90 +405,6 @@ function windowsCmdExeEscapeArg(arg: string): string { return `"${arg.replace(/["]/g, '""')}"`; } -/** - * Error handling wrapper around the npm `which` package: - * "Like the unix which utility. Finds the first instance of a specified - * executable in the PATH environment variable. Does not cache the results, - * so hash -r is not needed when the PATH changes." - * - * @param program Basename of a program, for example 'ssh' - * @param rejectOnMissing If the program cannot be found, reject the promise - * with an ExpectedError instead of fulfilling it with an empty string. - * @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE' - */ -export async function which( - program: string, - rejectOnMissing = true, -): Promise { - const whichMod = await import('which'); - let programPath: string; - try { - programPath = await whichMod(program); - } catch (err) { - if (err.code === 'ENOENT') { - if (rejectOnMissing) { - const { ExpectedError } = await import('../errors'); - throw new ExpectedError( - `'${program}' program not found. Is it installed?`, - ); - } else { - return ''; - } - } - throw err; - } - return programPath; -} - -/** - * Call which(programName) and spawn() with the given arguments. - * - * If returnExitCodeOrSignal is true, the returned promise will resolve to - * an array [code, signal] with the child process exit code number or exit - * signal string respectively (as provided by the spawn close event). - * - * If returnExitCodeOrSignal is false, the returned promise will reject with - * a custom error if the child process returns a non-zero exit code or a - * non-empty signal string (as reported by the spawn close event). - * - * In either case and if spawn itself emits an error event or fails synchronously, - * the returned promise will reject with a custom error that includes the error - * message of spawn's error. - */ -export async function whichSpawn( - programName: string, - args: string[], - 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(', ')}]`); - } - let error: Error | undefined; - let exitCode: number | undefined; - let exitSignal: string | undefined; - try { - [exitCode, exitSignal] = await new Promise((resolve, reject) => { - spawn(program, args, options) - .on('error', reject) - .on('close', (code, signal) => resolve([code, signal])); - }); - } catch (err) { - error = err; - } - if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) { - const msg = [ - `${programName} failed with exit code=${exitCode} signal=${exitSignal}:`, - `[${program}, ${args.join(', ')}]`, - ...(error ? [`${error}`] : []), - ]; - throw new Error(msg.join('\n')); - } - return [exitCode, exitSignal]; -} - export interface ProxyConfig { host: string; port: string; @@ -614,16 +530,3 @@ 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); - } -} diff --git a/lib/utils/ssh.ts b/lib/utils/ssh.ts index 6ca5b9fc..ac5e28cd 100644 --- a/lib/utils/ssh.ts +++ b/lib/utils/ssh.ts @@ -36,7 +36,7 @@ export async function exec( cmd: string, stdout?: NodeJS.WritableStream, ): Promise { - const { which } = await import('./helpers'); + const { which } = await import('./which'); const program = await which('ssh'); const args = [ '-n', @@ -132,7 +132,7 @@ export async function spawnSshAndThrowOnError( args: string[], options?: import('child_process').SpawnOptions, ) { - const { whichSpawn } = await import('./helpers'); + const { whichSpawn } = await import('./which'); const [exitCode, exitSignal] = await whichSpawn( 'ssh', args, diff --git a/lib/utils/umount.ts b/lib/utils/umount.ts new file mode 100644 index 00000000..02f6657e --- /dev/null +++ b/lib/utils/umount.ts @@ -0,0 +1,122 @@ +/** + * @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 { promisify } from 'util'; +import * as child_process from 'child_process'; + +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 { + 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 { + 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); + } +} diff --git a/lib/utils/which.ts b/lib/utils/which.ts new file mode 100644 index 00000000..7d20dabf --- /dev/null +++ b/lib/utils/which.ts @@ -0,0 +1,146 @@ +/** + * @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. + */ + +import { promises as fs, constants } from 'fs'; +import * as path from 'path'; + +export const { F_OK, R_OK, W_OK, X_OK } = constants; + +export async function exists(filename: string, mode = F_OK) { + try { + await fs.access(filename, mode); + return true; + } catch { + return false; + } +} + +/** + * Replace sequences of untowardly characters like /[<>:"/\\|?*\u0000-\u001F]/g + * and '.' or '..' with an underscore, plus other rules enforced by the filenamify + * package. See https://github.com/sindresorhus/filenamify/ + */ +export function sanitizePath(filepath: string) { + const filenamify = require('filenamify') as typeof import('filenamify'); + // normalize also converts forward slash to backslash on Windows + return path + .normalize(filepath) + .split(path.sep) + .map((f) => filenamify(f, { replacement: '_' })) + .join(path.sep); +} + +/** + * Given a program name like 'mount', search for it in a pre-defined set of + * folders ('/usr/bin', '/bin', '/usr/sbin', '/sbin') and return the full path if found. + * + * For executables, in some scenarios, this can be more secure than allowing + * any folder in the PATH. Only relevant on Linux or macOS. + */ +export async function whichBin(programName: string): Promise { + for (const dir of ['/usr/bin', '/bin', '/usr/sbin', '/sbin']) { + const candidate = path.join(dir, programName); + if (await exists(candidate, X_OK)) { + return candidate; + } + } + return ''; +} + +/** + * Error handling wrapper around the npm `which` package: + * "Like the unix which utility. Finds the first instance of a specified + * executable in the PATH environment variable. Does not cache the results, + * so hash -r is not needed when the PATH changes." + * + * @param program Basename of a program, for example 'ssh' + * @param rejectOnMissing If the program cannot be found, reject the promise + * with an ExpectedError instead of fulfilling it with an empty string. + * @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE' + */ +export async function which( + program: string, + rejectOnMissing = true, +): Promise { + const whichMod = await import('which'); + let programPath: string; + try { + programPath = await whichMod(program); + } catch (err) { + if (err.code === 'ENOENT') { + if (rejectOnMissing) { + const { ExpectedError } = await import('../errors'); + throw new ExpectedError( + `'${program}' program not found. Is it installed?`, + ); + } else { + return ''; + } + } + throw err; + } + return programPath; +} + +/** + * Call which(programName) and spawn() with the given arguments. + * + * If returnExitCodeOrSignal is true, the returned promise will resolve to + * an array [code, signal] with the child process exit code number or exit + * signal string respectively (as provided by the spawn close event). + * + * If returnExitCodeOrSignal is false, the returned promise will reject with + * a custom error if the child process returns a non-zero exit code or a + * non-empty signal string (as reported by the spawn close event). + * + * In either case and if spawn itself emits an error event or fails synchronously, + * the returned promise will reject with a custom error that includes the error + * message of spawn's error. + */ +export async function whichSpawn( + programName: string, + args: string[], + 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(', ')}]`); + } + let error: Error | undefined; + let exitCode: number | undefined; + let exitSignal: string | undefined; + try { + [exitCode, exitSignal] = await new Promise((resolve, reject) => { + spawn(program, args, options) + .on('error', reject) + .on('close', (code, signal) => resolve([code, signal])); + }); + } catch (err) { + error = err; + } + if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) { + const msg = [ + `${programName} failed with exit code=${exitCode} signal=${exitSignal}:`, + `[${program}, ${args.join(', ')}]`, + ...(error ? [`${error}`] : []), + ]; + throw new Error(msg.join('\n')); + } + return [exitCode, exitSignal]; +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 7e63e890..37148fbf 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1269,20 +1269,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.16.tgz", "integrity": "sha512-6CLxw83vQf6DKqXxMPwl8qpF8I7THFZuIwLt4TnNsumxkp1VsRZWT8txQxncT/Rl2UojTsFzWgDG4FRMwafrlA==", "dev": true - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } } } }, @@ -7823,6 +7809,21 @@ "minimatch": "^3.0.4" } }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=" + }, + "filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -8491,9 +8492,9 @@ } }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11972,6 +11973,20 @@ "dev": true, "optional": true }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -17031,6 +17046,14 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "struct-fu": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/struct-fu/-/struct-fu-1.2.1.tgz", @@ -17599,6 +17622,14 @@ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "ts-node": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.0.0.tgz", @@ -17847,14 +17878,6 @@ "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", "dev": true }, - "umount": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/umount/-/umount-1.1.6.tgz", - "integrity": "sha1-p0kPu9pIunalAKL0vgDV5mAnzQA=", - "requires": { - "lodash": "~4.17.4" - } - }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/package.json b/package.json index ea910796..342e438c 100644 --- a/package.json +++ b/package.json @@ -231,7 +231,9 @@ "fast-boot2": "^1.1.0", "fast-levenshtein": "^3.0.0", "file-disk": "^8.0.1", + "filenamify": "^4.3.0", "get-stdin": "^8.0.0", + "glob": "^7.1.7", "global-agent": "^2.1.12", "global-tunnel-ng": "^2.1.1", "humanize": "0.0.9", @@ -276,7 +278,6 @@ "through2": "^2.0.3", "tmp": "^0.2.1", "typed-error": "^3.2.1", - "umount": "^1.1.6", "update-notifier": "^4.1.0", "which": "^2.0.2", "window-size": "^1.1.0" diff --git a/tests/commands/ssh.spec.ts b/tests/commands/ssh.spec.ts index 46ccdcfd..efc227d5 100644 --- a/tests/commands/ssh.spec.ts +++ b/tests/commands/ssh.spec.ts @@ -37,7 +37,7 @@ describe('balena ssh', function () { if (hasSshExecutable) { [sshServer, sshServerPort] = await startMockSshServer(); } - const modPath = '../../build/utils/helpers'; + const modPath = '../../build/utils/which'; const mod = await import(modPath); mock(modPath, { ...mod, @@ -130,7 +130,7 @@ describe('balena ssh', function () { /** Check whether the 'ssh' tool (executable) exists in the PATH */ async function checkSsh(): Promise { - const { which } = await import('../../build/utils/helpers'); + const { which } = await import('../../build/utils/which'); const sshPath = await which('ssh', false); if ((sshPath || '').includes('\\Windows\\System32\\OpenSSH\\ssh')) { // don't use Windows' built-in ssh tool for these test cases diff --git a/typings/umount/index.d.ts b/typings/umount/index.d.ts deleted file mode 100644 index 245efc27..00000000 --- a/typings/umount/index.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright 2020 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. - */ - -declare module 'umount' { - export const umount: ( - device: string, - callback: (err?: Error, stdout?: any, stderr?: any) => void, - ) => void; - export const isMounted: ( - device: string, - callback: (err: Error | null, isMounted?: boolean) => void, - ) => void; -}