2017-12-20 21:46:01 +00:00
|
|
|
/*
|
2020-04-30 08:45:16 +00:00
|
|
|
Copyright 2016-2020 Balena Ltd.
|
2017-12-20 21:46:01 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2020-05-04 14:57:23 +00:00
|
|
|
import type { BalenaError } from 'balena-errors';
|
2018-05-03 16:59:12 +00:00
|
|
|
import * as _ from 'lodash';
|
|
|
|
import * as os from 'os';
|
2020-03-05 01:02:46 +00:00
|
|
|
import { TypedError } from 'typed-error';
|
2020-05-04 14:57:23 +00:00
|
|
|
import { getChalk, stripIndent } from './utils/lazy';
|
2020-04-30 08:45:16 +00:00
|
|
|
import { getHelp } from './utils/messages';
|
2018-05-03 12:48:01 +00:00
|
|
|
|
2020-03-05 01:02:46 +00:00
|
|
|
export class ExpectedError extends TypedError {}
|
2019-10-31 01:40:57 +00:00
|
|
|
|
|
|
|
export class NotLoggedInError extends ExpectedError {}
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2020-03-24 08:51:19 +00:00
|
|
|
export class InsufficientPrivilegesError extends ExpectedError {}
|
|
|
|
|
2020-06-29 12:45:32 +00:00
|
|
|
export class InvalidPortMappingError extends ExpectedError {
|
|
|
|
constructor(mapping: string) {
|
|
|
|
super(`'${mapping}' is not a valid port mapping.`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class NoPortsDefinedError extends ExpectedError {
|
|
|
|
constructor() {
|
|
|
|
super('No ports have been provided.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-18 01:31:13 +00:00
|
|
|
/**
|
2020-05-01 12:38:53 +00:00
|
|
|
* instanceOf is a more reliable implementation of the plain `instanceof`
|
2020-04-18 01:31:13 +00:00
|
|
|
* typescript operator, for use with TypedError errors when the error
|
|
|
|
* classes may be defined in external packages/dependencies.
|
|
|
|
* Sample usage:
|
|
|
|
* instanceOf(err, BalenaApplicationNotFound)
|
|
|
|
*
|
|
|
|
* A plain Typescript `instanceof` test may fail if `npm install` results
|
|
|
|
* in multiple instances of a package, for example multiple versions of
|
|
|
|
* `balena-errors`:
|
|
|
|
* $ find node_modules -type d -name balena-errors
|
|
|
|
* node_modules/balena-errors
|
|
|
|
* node_modules/balena-sdk/node_modules/balena-errors
|
|
|
|
*
|
|
|
|
* In these cases, `instanceof` produces a false negative when comparing objects
|
|
|
|
* and classes of the different package versions, but the `err.name` test still
|
|
|
|
* succeeds.
|
|
|
|
*
|
|
|
|
* @param err Error object, for example in a `catch(err)` block
|
|
|
|
* @param klass TypedError subclass, e.g. BalenaApplicationNotFound. The type
|
|
|
|
* is annotated as 'any' for the same reason of multiple package installations
|
|
|
|
* mentioned above.
|
|
|
|
*/
|
|
|
|
export function instanceOf(err: any, klass: any): boolean {
|
|
|
|
if (err instanceof klass) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const name: string | undefined = err.name || err.constructor?.name;
|
|
|
|
return name != null && name === klass.name;
|
|
|
|
}
|
|
|
|
|
2018-05-03 12:48:01 +00:00
|
|
|
function hasCode(error: any): error is Error & { code: string } {
|
|
|
|
return error.code != null;
|
|
|
|
}
|
|
|
|
|
2018-05-03 13:15:22 +00:00
|
|
|
function treatFailedBindingAsMissingModule(error: any): void {
|
|
|
|
if (error.message.startsWith('Could not locate the bindings file.')) {
|
|
|
|
error.code = 'MODULE_NOT_FOUND';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-31 01:40:57 +00:00
|
|
|
function interpret(error: Error): string {
|
2018-05-03 13:15:22 +00:00
|
|
|
treatFailedBindingAsMissingModule(error);
|
|
|
|
|
|
|
|
if (hasCode(error)) {
|
2018-05-03 12:48:01 +00:00
|
|
|
const errorCodeHandler = messages[error.code];
|
|
|
|
const message = errorCodeHandler && errorCodeHandler(error);
|
|
|
|
|
|
|
|
if (message) {
|
|
|
|
return message;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!_.isEmpty(error.message)) {
|
|
|
|
return `${error.code}: ${error.message}`;
|
|
|
|
}
|
|
|
|
}
|
2019-10-31 01:40:57 +00:00
|
|
|
|
|
|
|
return error.message;
|
2018-05-03 12:48:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const messages: {
|
2018-05-03 16:59:12 +00:00
|
|
|
[key: string]: (error: Error & { path?: string }) => string;
|
2018-05-03 12:48:01 +00:00
|
|
|
} = {
|
2020-06-15 22:53:07 +00:00
|
|
|
EISDIR: (error) => `File is a directory: ${error.path}`,
|
2018-05-03 12:48:01 +00:00
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
ENOENT: (error) => `No such file or directory: ${error.path}`,
|
2018-05-03 12:48:01 +00:00
|
|
|
|
|
|
|
ENOGIT: () => stripIndent`
|
|
|
|
Git is not installed on this system.
|
|
|
|
Head over to http://git-scm.com to install it and run this command again.`,
|
|
|
|
|
|
|
|
EPERM: () => stripIndent`
|
2019-07-17 19:12:51 +00:00
|
|
|
You don't have sufficient privileges to run this operation.
|
2018-05-03 16:59:12 +00:00
|
|
|
${
|
|
|
|
os.platform() === 'win32'
|
|
|
|
? 'Run a new Command Prompt as administrator and try running this command again.'
|
|
|
|
: 'Try running this command again prefixing it with `sudo`.'
|
2018-05-03 12:48:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`,
|
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
EACCES: (e) => messages.EPERM(e),
|
2018-05-03 12:48:01 +00:00
|
|
|
|
2018-05-03 16:59:12 +00:00
|
|
|
ETIMEDOUT: () =>
|
|
|
|
'Oops something went wrong, please check your connection and try again.',
|
2018-05-03 12:48:01 +00:00
|
|
|
|
2018-05-03 13:15:22 +00:00
|
|
|
MODULE_NOT_FOUND: () => stripIndent`
|
|
|
|
Part of the CLI could not be loaded. This typically means your CLI install is in a broken state.
|
2018-05-03 16:59:12 +00:00
|
|
|
${
|
|
|
|
os.arch() === 'x64'
|
|
|
|
? 'You can normally fix this by uninstalling and reinstalling the CLI.'
|
|
|
|
: stripIndent`
|
2018-05-03 13:15:22 +00:00
|
|
|
You're using an unsupported architecture (${os.arch()}), so this is typically caused by missing native modules.
|
|
|
|
Reinstalling may help, but pay attention to errors in native module build steps en route.
|
|
|
|
`
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
|
2018-10-19 14:38:50 +00:00
|
|
|
BalenaExpiredToken: () => stripIndent`
|
2020-06-15 22:53:01 +00:00
|
|
|
Looks like the session token has expired.
|
|
|
|
Try logging in again with the "balena login" command.`,
|
2018-05-03 16:59:12 +00:00
|
|
|
};
|
2018-05-03 12:48:01 +00:00
|
|
|
|
2020-05-01 12:38:53 +00:00
|
|
|
const EXPECTED_ERROR_REGEXES = [
|
2020-06-09 15:40:06 +00:00
|
|
|
/^BalenaAmbiguousApplication/, // balena-sdk
|
|
|
|
/^BalenaApplicationNotFound/, // balena-sdk
|
|
|
|
/^BalenaDeviceNotFound/, // balena-sdk
|
|
|
|
/^BalenaExpiredToken/, // balena-sdk
|
2020-07-15 14:26:31 +00:00
|
|
|
/^BalenaInvalidDeviceType/, // balena-sdk
|
2020-06-23 10:09:12 +00:00
|
|
|
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
|
|
|
|
/Missing required flag/, // oclif parser: RequiredFlagError
|
2020-05-15 16:23:36 +00:00
|
|
|
/^Unexpected argument/, // oclif parser: UnexpectedArgsError
|
2020-05-01 12:38:53 +00:00
|
|
|
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
|
2020-06-23 10:09:12 +00:00
|
|
|
/must also be provided when using/, // oclif parser (depends-on)
|
2020-06-24 13:24:54 +00:00
|
|
|
/^Expected an integer/, // oclif parser (flags.integer)
|
2020-09-25 09:00:18 +00:00
|
|
|
/^Flag .* expects a value/, // oclif parser
|
2020-05-01 12:38:53 +00:00
|
|
|
];
|
|
|
|
|
2020-05-15 16:23:36 +00:00
|
|
|
// Support unit testing of handleError
|
2020-06-22 16:05:10 +00:00
|
|
|
export const getSentry = async function () {
|
2020-05-15 16:23:36 +00:00
|
|
|
return await import('@sentry/node');
|
2020-06-22 16:05:10 +00:00
|
|
|
};
|
2020-05-15 16:23:36 +00:00
|
|
|
|
2020-06-10 10:37:09 +00:00
|
|
|
export async function handleError(error: Error) {
|
2020-05-01 12:38:53 +00:00
|
|
|
// Set appropriate exitCode
|
2019-10-31 01:40:57 +00:00
|
|
|
process.exitCode =
|
2020-06-10 10:37:09 +00:00
|
|
|
(error as BalenaError).exitCode === 0
|
2019-10-31 01:40:57 +00:00
|
|
|
? 0
|
2020-06-10 10:37:09 +00:00
|
|
|
: Math.trunc((error as BalenaError).exitCode) || process.exitCode || 1;
|
2017-12-20 21:46:01 +00:00
|
|
|
|
2020-05-01 12:38:53 +00:00
|
|
|
// Prepare message
|
2019-10-31 01:40:57 +00:00
|
|
|
const message = [interpret(error)];
|
|
|
|
|
2020-05-15 16:23:36 +00:00
|
|
|
if (error.stack && process.env.DEBUG) {
|
|
|
|
message.push('\n' + error.stack);
|
2017-12-20 21:46:01 +00:00
|
|
|
}
|
2020-03-05 01:02:46 +00:00
|
|
|
|
2020-05-01 12:38:53 +00:00
|
|
|
// Expected?
|
|
|
|
const isExpectedError =
|
2020-03-05 01:02:46 +00:00
|
|
|
error instanceof ExpectedError ||
|
2020-06-15 22:53:07 +00:00
|
|
|
EXPECTED_ERROR_REGEXES.some((re) => re.test(message[0])) ||
|
|
|
|
EXPECTED_ERROR_REGEXES.some((re) => re.test((error as BalenaError).code));
|
2020-05-01 12:38:53 +00:00
|
|
|
|
|
|
|
// Output/report error
|
|
|
|
if (isExpectedError) {
|
2020-06-22 16:05:10 +00:00
|
|
|
printExpectedErrorMessage(message.join('\n'));
|
2020-05-01 12:38:53 +00:00
|
|
|
} else {
|
2020-06-22 16:05:10 +00:00
|
|
|
printErrorMessage(message.join('\n'));
|
2020-05-01 12:38:53 +00:00
|
|
|
|
|
|
|
// Report "unexpected" errors via Sentry.io
|
2020-06-22 16:05:10 +00:00
|
|
|
const Sentry = await getSentry();
|
2020-05-01 12:38:53 +00:00
|
|
|
Sentry.captureException(error);
|
2020-05-15 16:23:36 +00:00
|
|
|
try {
|
|
|
|
await Sentry.close(1000);
|
|
|
|
} catch (e) {
|
|
|
|
if (process.env.DEBUG) {
|
|
|
|
console.error('Timeout reporting error to sentry.io');
|
|
|
|
}
|
|
|
|
}
|
2020-05-01 12:38:53 +00:00
|
|
|
// Unhandled/unexpected error: ensure that the process terminates.
|
|
|
|
// The exit error code was set above through `process.exitCode`.
|
|
|
|
process.exit();
|
2019-10-31 01:40:57 +00:00
|
|
|
}
|
2019-04-02 11:26:21 +00:00
|
|
|
}
|
2020-04-30 08:45:16 +00:00
|
|
|
|
2020-06-22 16:05:10 +00:00
|
|
|
export const printErrorMessage = function (message: string) {
|
2020-04-30 08:45:16 +00:00
|
|
|
const chalk = getChalk();
|
2020-05-01 12:38:53 +00:00
|
|
|
|
|
|
|
// Only first line should be red
|
|
|
|
const messageLines = message.split('\n');
|
|
|
|
console.error(chalk.red(messageLines.shift()));
|
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
messageLines.forEach((line) => {
|
2020-05-01 12:38:53 +00:00
|
|
|
console.error(line);
|
|
|
|
});
|
|
|
|
|
|
|
|
console.error(`\n${getHelp}\n`);
|
2020-06-22 16:05:10 +00:00
|
|
|
};
|
2020-05-01 12:38:53 +00:00
|
|
|
|
2020-06-22 16:05:10 +00:00
|
|
|
export const printExpectedErrorMessage = function (message: string) {
|
2020-05-01 12:38:53 +00:00
|
|
|
console.error(`${message}\n`);
|
2020-06-22 16:05:10 +00:00
|
|
|
};
|
2020-04-30 08:45:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Print a friendly error message and exit the CLI with an error code, BYPASSING
|
|
|
|
* error reporting through Sentry.io's platform (raven.Raven.captureException).
|
|
|
|
* Note that lib/errors.ts provides top-level error handling code to catch any
|
|
|
|
* otherwise uncaught errors, AND to report them through Sentry.io. But many
|
|
|
|
* "expected" errors (say, a JSON parsing error in a file provided by the user)
|
|
|
|
* don't warrant reporting through Sentry.io. For such mundane errors, catch
|
|
|
|
* them and call this function.
|
|
|
|
*
|
|
|
|
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
|
|
|
|
*/
|
|
|
|
export function exitWithExpectedError(message: string | Error): never {
|
|
|
|
if (message instanceof Error) {
|
|
|
|
({ message } = message);
|
|
|
|
}
|
|
|
|
|
|
|
|
printErrorMessage(message);
|
|
|
|
process.exit(1);
|
|
|
|
}
|