Merge pull request #1791 from balena-io/errors-refactor

Errors refactor
This commit is contained in:
srlowe 2020-05-01 15:04:30 +02:00 committed by GitHub
commit 3b53b75626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 58 additions and 74 deletions

View File

@ -87,6 +87,7 @@ Examples:
const balena = getBalenaSdk();
const patterns = await import('../utils/patterns');
const messages = await import('../utils/messages');
const { exitWithExpectedError } = await import('../errors');
const doLogin = async (loginOptions: Options): Promise<void> => {
if (loginOptions.token != null) {
@ -103,7 +104,7 @@ Examples:
}
await balena.auth.loginWithToken(token);
if (!(await balena.auth.whoami())) {
patterns.exitWithExpectedError('Token authentication failed');
exitWithExpectedError('Token authentication failed');
}
return;
} else if (loginOptions.credentials) {
@ -120,7 +121,7 @@ Examples:
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
const open = await import('open');
open(signupUrl, { wait: false });
return patterns.exitWithExpectedError(`Please sign up at ${signupUrl}`);
return exitWithExpectedError(`Please sign up at ${signupUrl}`);
}
loginOptions[loginType] = true;

View File

@ -322,7 +322,7 @@ Examples:
generateApplicationConfig,
} = require('../utils/config');
const helpers = require('../utils/helpers');
const { exitWithExpectedError } = require('../utils/patterns');
const { exitWithExpectedError } = require('../errors');
if (options.device == null && options.application == null) {
exitWithExpectedError(`\

View File

@ -20,7 +20,7 @@ import * as capitano from 'capitano';
import * as columnify from 'columnify';
import * as messages from '../utils/messages';
import { getManualSortCompareFunction } from '../utils/helpers';
import { exitWithExpectedError } from '../utils/patterns';
import { exitWithExpectedError } from '../errors';
import { getOclifHelpLinePairs } from './help_ts';
const parse = object =>

View File

@ -1,7 +1,7 @@
import * as Promise from 'bluebird';
import * as _ from 'lodash';
import * as dockerUtils from '../../utils/docker';
import { exitWithExpectedError } from '../../utils/patterns';
import { exitWithExpectedError } from '../../errors';
import { getChalk } from '../../utils/lazy';
export const dockerPort = 2375;

View File

@ -91,9 +91,8 @@ export const logs: CommandDefinition<
'../utils/device/logs'
);
const { validateIPAddress } = await import('../utils/validation');
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
'../utils/patterns'
);
const { checkLoggedIn } = await import('../utils/patterns');
const { exitWithExpectedError } = await import('../errors');
const Logger = await import('../utils/logger');
const logger = Logger.getLogger();
@ -152,7 +151,7 @@ export const logs: CommandDefinition<
servicesToDisplay,
);
} else {
await exitIfNotLoggedIn();
await checkLoggedIn();
if (options.tail) {
return balena.logs
.subscribe(params.uuidOrDevice, { count: 100 })

View File

@ -87,7 +87,7 @@ const getApplicationsWithSuccessfulBuilds = function(deviceType) {
const selectApplication = function(deviceType) {
const visuals = getVisuals();
const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../utils/patterns');
const { exitWithExpectedError } = require('../errors');
const applicationInfoSpinner = new visuals.Spinner(
'Downloading list of applications and releases.',
@ -116,7 +116,7 @@ const selectApplication = function(deviceType) {
const selectApplicationCommit = function(releases) {
const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../utils/patterns');
const { exitWithExpectedError } = require('../errors');
if (releases.length === 0) {
exitWithExpectedError('This application has no successful releases.');
@ -263,7 +263,7 @@ Examples:
const balenaPreload = require('balena-preload');
const visuals = getVisuals();
const nodeCleanup = require('node-cleanup');
const { exitWithExpectedError } = require('../utils/patterns');
const { exitWithExpectedError } = require('../errors');
const progressBars = {};

View File

@ -60,7 +60,7 @@ export const list: CommandDefinition<
const _ = await import('lodash');
const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../utils/patterns');
const { exitWithExpectedError } = await import('../errors');
return Bluebird.try<ApplicationTag[] | DeviceTag[] | ReleaseTag[]>(
async () => {
@ -161,7 +161,7 @@ export const set: CommandDefinition<
const _ = await import('lodash');
const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../utils/patterns');
const { exitWithExpectedError } = await import('../errors');
if (_.isEmpty(params.tagKey)) {
return exitWithExpectedError('No tag key was provided');
@ -250,7 +250,7 @@ export const remove: CommandDefinition<
async action(params, options) {
const _ = await import('lodash');
const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../utils/patterns');
const { exitWithExpectedError } = await import('../errors');
if (_.isEmpty(params.tagKey)) {
return exitWithExpectedError('No tag key was provided');

View File

@ -22,7 +22,7 @@ import * as events from './events';
capitano.permission('user', done =>
require('./utils/patterns')
.exitIfNotLoggedIn()
.checkLoggedIn()
.then(done, done),
);

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2020 Balena
Copyright 2016-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.
@ -18,6 +18,8 @@ import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import * as os from 'os';
import { TypedError } from 'typed-error';
import { getChalk } from './utils/lazy';
import { getHelp } from './utils/messages';
export class ExpectedError extends TypedError {}
@ -130,8 +132,6 @@ const messages: {
};
export async function handleError(error: any) {
const { printErrorMessage } = await import('./utils/patterns');
process.exitCode =
error.exitCode === 0
? 0
@ -145,7 +145,7 @@ export async function handleError(error: any) {
const message = [interpret(error)];
if (process.env.DEBUG && error.stack) {
message.push(error.stack);
message.push('\n' + error.stack);
}
printErrorMessage(message.join('\n'));
@ -171,3 +171,29 @@ export async function handleError(error: any) {
// The exit error code was set above through `process.exitCode`.
process.exit();
}
export function printErrorMessage(message: string) {
const chalk = getChalk();
console.error(chalk.red(message));
console.error(chalk.red(`\n${getHelp}\n`));
}
/**
* 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);
}

View File

@ -16,7 +16,7 @@
*/
import { stripIndent } from 'common-tags';
import { exitWithExpectedError } from './utils/patterns';
import { exitWithExpectedError } from './errors';
export interface AppOptions {
// Prevent the default behavior of flushing stdout after running a command

View File

@ -74,7 +74,7 @@ async function environmentFromInput(
serviceNames: string[],
logger: Logger,
): Promise<ParsedEnvironment> {
const { exitWithExpectedError } = await import('../patterns');
const { exitWithExpectedError } = await import('../../errors');
// A normal environment variable regex, with an added part
// to find a colon followed servicename at the start
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
@ -121,7 +121,7 @@ async function environmentFromInput(
export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
const { tarDirectory } = await import('../compose');
const { exitWithExpectedError } = await import('../patterns');
const { exitWithExpectedError } = await import('../../errors');
const { displayDeviceLogs } = await import('./logs');
const api = new DeviceAPI(globalLogger, opts.deviceHost);
@ -574,7 +574,7 @@ export function generateTargetState(
}
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
const { exitWithExpectedError } = await import('../patterns');
const { exitWithExpectedError } = await import('../../errors');
const failures: LocalPushErrors.BuildFailure[] = [];

View File

@ -270,7 +270,7 @@ export const createClient = function(opts) {
};
var ensureDockerSeemsAccessible = function(docker) {
const { exitWithExpectedError } = require('./patterns');
const { exitWithExpectedError } = require('../errors');
return docker
.ping()
.catch(() =>

View File

@ -20,9 +20,8 @@ import { stripIndent } from 'common-tags';
import _ = require('lodash');
import _form = require('resin-cli-form');
import { instanceOf, NotLoggedInError } from '../errors';
import { getBalenaSdk, getChalk, getVisuals } from './lazy';
import messages = require('./messages');
import { exitWithExpectedError, instanceOf, NotLoggedInError } from '../errors';
import { getBalenaSdk, getVisuals } from './lazy';
import validation = require('./validation');
const getForm = _.once((): typeof _form => require('resin-cli-form'));
@ -88,22 +87,6 @@ export async function checkLoggedIn(): Promise<void> {
}
}
/**
* Check if logged in, and call `exitWithExpectedError()` if not.
* DEPRECATED: Use checkLoggedIn() instead.
*/
export async function exitIfNotLoggedIn(): Promise<void> {
try {
await checkLoggedIn();
} catch (error) {
if (error instanceof NotLoggedInError) {
exitWithExpectedError(error);
} else {
throw error;
}
}
}
export function askLoginType() {
return getForm().ask<'web' | 'credentials' | 'token' | 'register'>({
message: 'How would you like to login?',
@ -435,29 +418,3 @@ export function selectFromList<T>(
})),
});
}
export function printErrorMessage(message: string) {
const chalk = getChalk();
console.error(chalk.red(message));
console.error(chalk.red(`\n${messages.getHelp}\n`));
}
/**
* 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);
}

View File

@ -17,7 +17,7 @@
import * as BalenaSdk from 'balena-sdk';
import { stripIndent } from 'common-tags';
import { ExpectedError } from '../errors';
import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals } from './lazy';
import Logger = require('./logger');
import { exec, execBuffered, getDeviceOsRelease } from './ssh';
@ -325,7 +325,6 @@ async function createApplication(
): Promise<BalenaSdk.Application> {
const form = await import('resin-cli-form');
const validation = await import('./validation');
const patterns = await import('./patterns');
let username = await sdk.auth.whoami();
if (!username) {
@ -352,7 +351,9 @@ async function createApplication(
],
},
});
patterns.printErrorMessage(
// TODO: This is the only example in the codebase where `printErrorMessage()`
// is called directly. Consider refactoring.
printErrorMessage(
'You already have an application with that name; please choose another.',
);
continue;

View File

@ -24,7 +24,7 @@ import streamToPromise = require('stream-to-promise');
import { Pack } from 'tar-stream';
import { ExpectedError } from '../errors';
import { exitWithExpectedError } from '../utils/patterns';
import { exitWithExpectedError } from '../errors';
import { tarDirectory } from './compose';
import { getVisuals } from './lazy';
import Logger = require('./logger');