Merge pull request #2296 from balena-io/npm-audit

chore: Update dependencies (balena-lint, oclif, "npm audit fix")
This commit is contained in:
bulldozer-balena[bot] 2021-07-22 01:29:20 +00:00 committed by GitHub
commit fa4e8e7b55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1669 additions and 1092 deletions

View File

@ -86,7 +86,7 @@ function importOclifCommands(jsFilename: string): OclifCommand[] {
const command: OclifCommand =
jsFilename === 'help'
? ((new FakeHelpCommand() as unknown) as OclifCommand)
? (new FakeHelpCommand() as unknown as OclifCommand)
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
return [command];

View File

@ -62,9 +62,12 @@ export async function release() {
/** Return a cached Octokit instance, creating a new one as needed. */
const getOctokit = _.once(function () {
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
(require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling'))
.throttling,
const Octokit = (
require('@octokit/rest') as typeof import('@octokit/rest')
).Octokit.plugin(
(
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
).throttling,
);
return new Octokit({
auth: GITHUB_TOKEN,
@ -110,7 +113,8 @@ function getPageNumbers(
if (!response.headers.link) {
return res;
}
const parse = require('parse-link-header') as typeof import('parse-link-header');
const parse =
require('parse-link-header') as typeof import('parse-link-header');
const parsed = parse(response.headers.link);
if (parsed == null) {
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
@ -158,12 +162,14 @@ async function updateGitHubReleaseDescriptions(
per_page: perPage,
});
let errCount = 0;
type Release = import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
type Release =
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
for await (const response of octokit.paginate.iterator<Release>(options)) {
const { page: thisPage, pages: totalPages, ordinal } = getPageNumbers(
response,
perPage,
);
const {
page: thisPage,
pages: totalPages,
ordinal,
} = getPageNumbers(response, perPage);
let i = 0;
for (const cliRelease of response.data) {
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;

View File

@ -111,8 +111,6 @@ ${dockerignoreHelp}
await Command.checkLoggedInIf(!!options.application);
// compositions with many services trigger misleading warnings
// @ts-ignore editing property that isn't typed but does exist
(await import('events')).defaultMaxListeners = 1000;
const sdk = getBalenaSdk();

View File

@ -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'));

View File

@ -54,7 +54,7 @@ export default class ConfigReadCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
const { safeUmount } = await import('../../utils/helpers');
const { safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));

View File

@ -58,7 +58,7 @@ export default class ConfigReconfigureCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
const { safeUmount } = await import('../../utils/helpers');
const { safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));

View File

@ -75,7 +75,7 @@ export default class ConfigWriteCmd extends Command {
ConfigWriteCmd,
);
const { safeUmount } = await import('../../utils/helpers');
const { denyMount, safeUmount } = await import('../../utils/umount');
const drive =
options.drive || (await getVisuals().drive('Select the device drive'));
@ -87,9 +87,10 @@ export default class ConfigWriteCmd extends Command {
console.info(`Setting ${params.key} to ${params.value}`);
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
await safeUmount(drive);
await config.write(drive, options.type, configJSON);
await denyMount(drive, async () => {
await safeUmount(drive);
await config.write(drive, options.type, configJSON);
});
console.info('Done');
}

View File

@ -155,8 +155,6 @@ ${dockerignoreHelp}
DeployCmd,
);
// compositions with many services trigger misleading warnings
// @ts-ignore editing property that isn't typed but does exist
(await import('events')).defaultMaxListeners = 1000;
const logger = await Command.getLogger();
@ -190,15 +188,13 @@ ${dockerignoreHelp}
options['registry-secrets'],
);
} else {
const {
dockerfilePath,
registrySecrets,
} = await validateProjectDirectory(sdk, {
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'],
});
const { dockerfilePath, registrySecrets } =
await validateProjectDirectory(sdk, {
dockerfilePath: options.dockerfile,
noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'],
});
options.dockerfile = dockerfilePath;
options['registry-secrets'] = registrySecrets;
}

View File

@ -111,7 +111,8 @@ export default class DeviceCmd extends Command {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';

View File

@ -103,7 +103,9 @@ export default class DeviceInitCmd extends Command {
const application = (await getApplication(
balena,
options['application'] ||
(await (await import('../../utils/patterns')).selectApplication()).id,
(
await (await import('../../utils/patterns')).selectApplication()
).id,
{
$expand: {
is_for__device_type: {

View File

@ -98,15 +98,17 @@ export default class DeviceMoveCmd extends Command {
const devices = await Promise.all(
deviceIds.map(
(uuid) =>
balena.models.device.get(uuid, expandForAppName) as Promise<
ExtendedDevice
>,
balena.models.device.get(
uuid,
expandForAppName,
) as Promise<ExtendedDevice>,
),
);
// Map application name for each device
for (const device of devices) {
const belongsToApplication = device.belongs_to__application as Application[];
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
@ -168,7 +170,6 @@ export default class DeviceMoveCmd extends Command {
compatibleDeviceTypes.some(
(dt) => dt.slug === app.is_for__device_type[0].slug,
) &&
// @ts-ignore using the extended device object prop
devices.some((device) => device.application_name !== app.app_name),
true,
);

View File

@ -79,19 +79,15 @@ export default class DeviceOsUpdateCmd extends Command {
const sdk = getBalenaSdk();
// Get device info
const {
uuid,
is_of__device_type,
os_version,
os_variant,
} = (await sdk.models.device.get(params.uuid, {
$select: ['uuid', 'os_version', 'os_variant'],
$expand: {
is_of__device_type: {
$select: 'slug',
const { uuid, is_of__device_type, os_version, os_variant } =
(await sdk.models.device.get(params.uuid, {
$select: ['uuid', 'os_version', 'os_variant'],
$expand: {
is_of__device_type: {
$select: 'slug',
},
},
},
})) as DeviceWithDeviceType;
})) as DeviceWithDeviceType;
// Get current device OS version
const currentOsVersion = sdk.models.device.getOsVersion({

View File

@ -96,7 +96,8 @@ export default class DevicesCmd extends Command {
devices = devices.map(function (device) {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
const belongsToApplication =
device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]?.app_name || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);

View File

@ -344,8 +344,10 @@ function fillInInfoFields(
envVar.serviceName = (envVar.service as SDK.Service[])[0]?.service_name;
} else if ('service_install' in envVar) {
// envVar is of type DeviceServiceEnvironmentVariableInfo
envVar.serviceName = ((envVar.service_install as SDK.ServiceInstall[])[0]
?.installs__service as SDK.Service[])[0]?.service_name;
envVar.serviceName = (
(envVar.service_install as SDK.ServiceInstall[])[0]
?.installs__service as SDK.Service[]
)[0]?.service_name;
}
envVar.appName = appNameOrSlug;
envVar.serviceName = envVar.serviceName || '*';

View File

@ -60,50 +60,36 @@ export default class LocalConfigureCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
const path = await import('path');
const reconfix = await import('reconfix');
const denymount = promisify(await import('denymount'));
const { safeUmount } = await import('../../utils/helpers');
const { denyMount, safeUmount } = await import('../../utils/umount');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const configurationSchema = await this.prepareConnectionFile(params.target);
await safeUmount(params.target);
const dmOpts: any = {};
if (process.pkg) {
// when running in a standalone pkg install, the 'denymount'
// executable is placed on the same folder as process.execPath
dmOpts.executablePath = path.join(
path.dirname(process.execPath),
'denymount',
await denyMount(params.target, async () => {
// TODO: safeUmount umounts drives like '/dev/sdc', but does not
// umount image files like 'balena.img'
await safeUmount(params.target);
const config = await reconfix.readConfiguration(
configurationSchema,
params.target,
);
}
const dmHandler = (cb: () => void) =>
reconfix
.readConfiguration(configurationSchema, params.target)
.then(async (config: any) => {
logger.logDebug('Current config:');
logger.logDebug(JSON.stringify(config));
const answers = await this.getConfiguration(config);
logger.logDebug('New config:');
logger.logDebug(JSON.stringify(answers));
if (!answers.hostname) {
await this.removeHostname(configurationSchema);
}
return await reconfix.writeConfiguration(
configurationSchema,
answers,
params.target,
);
})
.asCallback(cb);
await denymount(params.target, dmHandler, dmOpts);
logger.logDebug('Current config:');
logger.logDebug(JSON.stringify(config));
const answers = await this.getConfiguration(config);
logger.logDebug('New config:');
logger.logDebug(JSON.stringify(answers));
if (!answers.hostname) {
await this.removeHostname(configurationSchema);
}
await reconfix.writeConfiguration(
configurationSchema,
answers,
params.target,
);
});
console.log('Done!');
}

View File

@ -83,8 +83,9 @@ export default class LocalFlashCmd extends Command {
try {
const info = await execAsync('cat /proc/version');
distroVersion = info.stdout.toLowerCase();
// tslint:disable-next-line: no-empty
} catch {}
} catch {
// pass
}
if (distroVersion.includes('microsoft')) {
throw new ExpectedError(stripIndent`
This command is known not to work on WSL. Please use a CLI release

View File

@ -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`);
}

View File

@ -55,10 +55,8 @@ export default class OsVersionsCmd extends Command {
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(OsVersionsCmd);
const {
versions: vs,
recommended,
} = await getBalenaSdk().models.os.getSupportedVersions(params.type);
const { versions: vs, recommended } =
await getBalenaSdk().models.os.getSupportedVersions(params.type);
vs.forEach((v) => {
console.log(`v${v}` + (v === recommended ? ' (recommended)' : ''));

View File

@ -87,9 +87,8 @@ export default class ScanCmd extends Command {
const ux = getCliUx();
ux.action.start('Scanning for local balenaOS devices');
const localDevices: LocalBalenaOsDevice[] = await discover.discoverLocalBalenaOsDevices(
discoverTimeout,
);
const localDevices: LocalBalenaOsDevice[] =
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
const engineReachableDevices: boolean[] = await Promise.all(
localDevices.map(async ({ address }: { address: string }) => {
const docker = await dockerUtils.createClient({

View File

@ -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'
);

View File

@ -112,9 +112,8 @@ export default class BalenaHelp extends Help {
typeof additionalCommands[0]?.usage === 'string'
) {
primaryCommands[0].usage = primaryCommands[0].usage.padEnd(usageLength);
additionalCommands[0].usage = additionalCommands[0].usage.padEnd(
usageLength,
);
additionalCommands[0].usage =
additionalCommands[0].usage.padEnd(usageLength);
}
// Output help
@ -139,9 +138,8 @@ export default class BalenaHelp extends Help {
console.log(' --help, -h');
console.log(' --debug\n');
const {
reachingOut,
} = require('./utils/messages') as typeof import('./utils/messages');
const { reachingOut } =
require('./utils/messages') as typeof import('./utils/messages');
console.log(reachingOut);
}

View File

@ -25,7 +25,8 @@
export class CliSettings {
public readonly settings: any;
constructor() {
this.settings = require('balena-settings-client') as typeof import('balena-settings-client');
this.settings =
require('balena-settings-client') as typeof import('balena-settings-client');
}
public get<T>(name: string): T {

View File

@ -188,10 +188,8 @@ async function resolveOSVersion(deviceType: string, version: string) {
return version;
}
const {
versions: vs,
recommended,
} = await getBalenaSdk().models.os.getSupportedVersions(deviceType);
const { versions: vs, recommended } =
await getBalenaSdk().models.os.getSupportedVersions(deviceType);
const choices = vs.map((v) => ({
value: v,

View File

@ -42,15 +42,17 @@ 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
* push commands, parse it into separate arrays of release tag keys and values.
* The returned keys and values arrays are guaranteed to be of the same length.
*/
export function parseReleaseTagKeysAndValues(
releaseTags: string[],
): { releaseTagKeys: string[]; releaseTagValues: string[] } {
export function parseReleaseTagKeysAndValues(releaseTags: string[]): {
releaseTagKeys: string[];
releaseTagValues: string[];
} {
if (releaseTags.length === 0) {
return { releaseTagKeys: [], releaseTagValues: [] };
}
@ -97,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 =
@ -285,16 +278,15 @@ export async function buildProject(opts: {
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<
TransposeOptions | undefined
> = await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
@ -498,9 +490,8 @@ async function setTaskProgressHooks({
let rawStream;
stream = createLogStream(stream);
if (transposeOptions) {
const buildThroughStream = transpose.getBuildThroughStream(
transposeOptions,
);
const buildThroughStream =
transpose.getBuildThroughStream(transposeOptions);
rawStream = stream.pipe(buildThroughStream);
} else {
rawStream = stream;
@ -766,10 +757,8 @@ async function newTarDirectory(
const tar = await import('tar-stream');
const pack = tar.pack();
const serviceDirs = await getServiceDirsFromComposition(dir, composition);
const {
filteredFileList,
dockerignoreFiles,
} = await filterFilesWithDockerignore(dir, multiDockerignore, serviceDirs);
const { filteredFileList, dockerignoreFiles } =
await filterFilesWithDockerignore(dir, multiDockerignore, serviceDirs);
printDockerignoreWarn(dockerignoreFiles, serviceDirs, multiDockerignore);
for (const fileStats of filteredFileList) {
pack.entry(
@ -975,9 +964,10 @@ async function parseRegistrySecrets(
}
const raw = (await fs.readFile(secretsFilename)).toString();
const multiBuild = await import('resin-multibuild');
const registrySecrets = new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
);
const registrySecrets =
new multiBuild.RegistrySecretValidator().validateRegistrySecrets(
isYaml ? require('js-yaml').load(raw) : JSON.parse(raw),
);
multiBuild.addCanonicalDockerHubEntry(registrySecrets);
return registrySecrets;
} catch (error) {
@ -1279,17 +1269,20 @@ async function pushServiceImages(
const { pushAndUpdateServiceImages } = await import('./compose');
const releaseMod = await import('balena-release');
logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(docker, token, taggedImages, async function (
serviceImage,
) {
logger.logDebug(
`Saving image ${serviceImage.is_stored_at__image_location}`,
);
if (skipLogUpload) {
delete serviceImage.build_log;
}
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
});
await pushAndUpdateServiceImages(
docker,
token,
taggedImages,
async function (serviceImage) {
logger.logDebug(
`Saving image ${serviceImage.is_stored_at__image_location}`,
);
if (skipLogUpload) {
delete serviceImage.build_log;
}
await releaseMod.updateImage(pineClient, serviceImage.id, serviceImage);
},
);
}
export async function deployProject(

View File

@ -216,7 +216,7 @@ export class DeviceAPI {
const NetKeepalive = await import('net-keepalive');
// Certain versions of typescript won't convert
// this automatically
const sock = (res.socket as any) as NodeJSSocketWithFileDescriptor;
const sock = res.socket as any as NodeJSSocketWithFileDescriptor;
// We send a tcp keepalive probe once every 5 seconds
NetKeepalive.setKeepAliveInterval(sock, 5000);
// After 5 failed probes, the connection is marked as
@ -235,7 +235,7 @@ export class DeviceAPI {
// A helper method for promisifying general (non-streaming) requests. Streaming
// requests should use a seperate setup
private static async promisifiedRequest<
T extends Parameters<typeof request>[0]
T extends Parameters<typeof request>[0],
>(opts: T, logger?: Logger): Promise<any> {
interface ObjectWithUrl {
url?: string;

View File

@ -151,9 +151,8 @@ export class LivepushManager {
// only happens when the dockerfile path is
// specified differently - this should be patched
// in resin-bundle-resolve
this.dockerfilePaths[
buildTask.serviceName
] = this.getDockerfilePathFromTask(buildTask);
this.dockerfilePaths[buildTask.serviceName] =
this.getDockerfilePathFromTask(buildTask);
} else {
this.dockerfilePaths[buildTask.serviceName] = [
buildTask.dockerfilePath,

View File

@ -90,9 +90,8 @@ export async function sudo(
}
export function runCommand<T>(commandArgs: string[]): Promise<T> {
const {
isSubcommand,
} = require('../preparser') as typeof import('../preparser');
const { isSubcommand } =
require('../preparser') as typeof import('../preparser');
if (isSubcommand(commandArgs)) {
commandArgs = [
commandArgs[0] + ':' + commandArgs[1],
@ -406,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<string> {
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;
@ -598,14 +513,13 @@ export function addSIGINTHandler(sigintHandler: () => void, once = true) {
* @param theArgs Arguments to be passed to the task function
*/
export async function awaitInterruptibleTask<
T extends (...args: any[]) => Promise<any>
T extends (...args: any[]) => Promise<any>,
>(task: T, ...theArgs: Parameters<T>): Promise<ReturnType<T>> {
let sigintHandler: () => void = () => undefined;
const sigintPromise = new Promise<T>((_resolve, reject) => {
sigintHandler = () => {
const {
SIGINTError,
} = require('../errors') as typeof import('../errors');
const { SIGINTError } =
require('../errors') as typeof import('../errors');
reject(new SIGINTError('Task aborted on SIGINT signal'));
};
addSIGINTHandler(sigintHandler);
@ -616,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);
}
}

View File

@ -61,5 +61,6 @@ export const getCliForm = once(
export const getCliUx = once(() => require('cli-ux').ux as typeof ux);
// Directly export stripIndent as we always use it immediately, but importing just `stripIndent` reduces startup time
// tslint:disable-next-line:no-var-requires
export const stripIndent = require('common-tags/lib/stripIndent') as typeof StripIndent;
export const stripIndent =
// tslint:disable-next-line:no-var-requires
require('common-tags/lib/stripIndent') as typeof StripIndent;

View File

@ -268,13 +268,11 @@ export async function awaitDeviceOsUpdate(
progressBar.update({ percentage: 0 });
const poll = async (): Promise<void> => {
const [
osUpdateStatus,
{ overall_progress: osUpdateProgress },
] = await Promise.all([
balena.models.device.getOsUpdateStatus(uuid),
balena.models.device.get(uuid, { $select: 'overall_progress' }),
]);
const [osUpdateStatus, { overall_progress: osUpdateProgress }] =
await Promise.all([
balena.models.device.getOsUpdateStatus(uuid),
balena.models.device.get(uuid, { $select: 'overall_progress' }),
]);
if (osUpdateStatus.status === 'done') {
console.info(
`The device ${deviceName} has been updated to v${targetOsVersion} and will restart shortly!`,

View File

@ -36,7 +36,7 @@ export async function exec(
cmd: string,
stdout?: NodeJS.WritableStream,
): Promise<void> {
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,

153
lib/utils/umount.ts Normal file
View File

@ -0,0 +1,153 @@
/**
* @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);
}

146
lib/utils/which.ts Normal file
View File

@ -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<string> {
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<string> {
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];
}

1935
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -69,7 +69,7 @@
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
"ci": "npm run test && npm run catch-uncommitted",
"watch": "gulp watch",
"lint": "balena-lint -e ts -e js --typescript --fix automation/ completion/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js",
"lint": "balena-lint -e ts -e js --fix automation/ completion/ lib/ typings/ tests/ bin/balena bin/balena-dev gulpfile.js .mocharc.js .mocharc-standalone.js",
"update": "ts-node --transpile-only ./automation/update-module.ts",
"prepare": "echo {} > bin/.fast-boot.json",
"prepublishOnly": "npm run build"
@ -111,7 +111,7 @@
]
},
"devDependencies": {
"@balena/lint": "^5.2.0",
"@balena/lint": "^6.1.1",
"@oclif/config": "^1.17.0",
"@oclif/dev-cli": "^1.26.0",
"@oclif/parser": "^3.8.5",
@ -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",
@ -242,7 +244,7 @@
"js-yaml": "^4.0.0",
"klaw": "^3.0.0",
"livepush": "^3.5.0",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"minimatch": "^3.0.4",
"mixpanel": "^0.10.3",
"moment": "^2.27.0",
@ -250,7 +252,7 @@
"ndjson": "^2.0.0",
"node-cleanup": "^2.1.2",
"node-unzip-2": "^0.2.8",
"oclif": "^1.16.1",
"oclif": "^1.18.1",
"open": "^7.1.0",
"partitioninfo": "^6.0.2",
"patch-package": "^6.4.7",
@ -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"

View File

@ -223,7 +223,7 @@ describe('balena deploy', function () {
});
} finally {
await switchSentry(sentryStatus);
// @ts-ignore
// @ts-expect-error
process.exit.restore();
}
});

View File

@ -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<boolean> {
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

View File

@ -100,9 +100,8 @@ export async function inspectTarStream(
try {
expect($expected).to.deep.equal(found);
} catch (e) {
const {
diff,
} = require('deep-object-diff') as typeof import('deep-object-diff');
const { diff } =
require('deep-object-diff') as typeof import('deep-object-diff');
const diffStr = JSON.stringify(
diff($expected, found),
(_k, v) => (v === undefined ? 'undefined' : v),
@ -216,7 +215,7 @@ export async function testDockerBuildStream(o: {
}
if (o.expectedExitCode != null) {
if (process.env.BALENA_CLI_TEST_TYPE !== 'standalone') {
// @ts-ignore
// @ts-expect-error
sinon.assert.calledWith(process.exit);
}
expect(o.expectedExitCode).to.equal(exitCode);

View File

@ -48,7 +48,7 @@ describe('handleError() function', () => {
'printExpectedErrorMessage',
);
captureException = sinon.stub();
// @ts-ignore
// @ts-expect-error
sandbox.stub(ErrorsModule, 'getSentry').resolves({ captureException });
processExit = sandbox.stub(process, 'exit');

View File

@ -39,9 +39,8 @@ interface TestOutput {
* @param testOutput
*/
function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
const {
matchesNodeEngineVersionWarn,
} = require('../automation/utils') as typeof import('../automation/utils');
const { matchesNodeEngineVersionWarn } =
require('../automation/utils') as typeof import('../automation/utils');
return {
exitCode: testOutput.exitCode,
err: testOutput.err.filter(

View File

@ -130,9 +130,8 @@ export class NockMock {
}
protected handleUnexpectedRequest(req: any) {
const {
interceptorServerPort,
} = require('./proxy-server') as typeof import('./proxy-server');
const { interceptorServerPort } =
require('./proxy-server') as typeof import('./proxy-server');
const o = req.options || {};
const u = o.uri || {};
const method = req.method;

View File

@ -86,16 +86,19 @@ async function createProxyServer(): Promise<[number, number]> {
const interceptorPort = await createInterceptorServer();
const proxy = httpProxy.createProxyServer();
proxy.on('error', function (
err: Error,
_req: http.IncomingMessage,
res: http.ServerResponse,
) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
const msg = `Proxy server error: ${err}`;
console.error(msg);
res.end(msg);
});
proxy.on(
'error',
function (
err: Error,
_req: http.IncomingMessage,
res: http.ServerResponse,
) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
const msg = `Proxy server error: ${err}`;
console.error(msg);
res.end(msg);
},
);
const server = http.createServer(function (
req: http.IncomingMessage,
@ -150,10 +153,9 @@ async function createInterceptorServer(): Promise<number> {
if (process.env.DEBUG) {
console.error(`[debug] Interceptor forwarding for ${proxiedFor}`);
}
// tslint:disable-next-line:prefer-const
let { protocol, hostname, port, path: urlPath, hash } = url.parse(
proxiedFor,
);
const parsed = url.parse(proxiedFor);
const { hash, hostname, path: urlPath } = parsed;
let { port, protocol } = parsed;
protocol = (protocol || 'http:').toLowerCase();
port = port || (protocol === 'https:' ? '443' : '80');
const reqOpts = {

View File

@ -46,7 +46,8 @@ class MockLivepushManager extends LivepushManager {
api: {} as import('../../../lib/utils/device/api').DeviceAPI,
logger: {} as import('../../../lib/utils/logger'),
buildLogs: {},
deployOpts: {} as import('../../../lib/utils/device/deploy').DeviceDeployOptions,
deployOpts:
{} as import('../../../lib/utils/device/deploy').DeviceDeployOptions,
});
}

View File

@ -72,7 +72,6 @@ describe('detectEncoding() function', function () {
'node_modules/.bin/mocha',
'node_modules/.bin/rimraf',
'node_modules/.bin/gulp',
'node_modules/.bin/coffeelint',
'node_modules/.bin/tsc',
'node_modules/.bin/balena-lint',
'node_modules/.bin/balena-preload',

View File

@ -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;
}