mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-18 21:27:51 +00:00
Refactor initialization code (delete app-oclif.ts and app-common.ts)
Change-type: patch
This commit is contained in:
parent
cf376316bc
commit
d0e4fa0e59
@ -1,61 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 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 { Main } from '@oclif/command';
|
||||
|
||||
import { trackPromise } from './hooks/prerun/track';
|
||||
|
||||
class CustomMain extends Main {
|
||||
protected _helpOverride(): boolean {
|
||||
// Disable oclif's default handler for the 'version' command
|
||||
if (['-v', '--version', 'version'].includes(this.argv[0])) {
|
||||
return false;
|
||||
} else {
|
||||
return super._helpOverride();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import type { AppOptions } from './preparser';
|
||||
|
||||
/**
|
||||
* oclif CLI entrypoint
|
||||
*/
|
||||
export async function run(command: string[], options: AppOptions) {
|
||||
const runPromise = CustomMain.run(command).then(
|
||||
() => {
|
||||
if (!options.noFlush) {
|
||||
return require('@oclif/command/flush');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
try {
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
}
|
||||
}
|
185
lib/app.ts
185
lib/app.ts
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
* Copyright 2019-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.
|
||||
@ -15,73 +15,138 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
|
||||
/**
|
||||
* CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
|
||||
* call this function.
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/error-reporting/quickstart/?platform=node
|
||||
*/
|
||||
export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtras({
|
||||
is_pkg: !!(process as any).pkg,
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
});
|
||||
});
|
||||
return Sentry.getCurrentHub();
|
||||
});
|
||||
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Setup balena-sdk options that are shared with imported packages */
|
||||
function setupBalenaSdkSharedOptions(settings: CliSettings) {
|
||||
const BalenaSdk = require('balena-sdk') as typeof import('balena-sdk');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get<string>('apiUrl'),
|
||||
dataDirectory: settings.get<string>('dataDirectory'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Addresses the console warning:
|
||||
* (node:49500) MaxListenersExceededWarning: Possible EventEmitter memory
|
||||
* leak detected. 11 error listeners added. Use emitter.setMaxListeners() to
|
||||
* increase limit
|
||||
*/
|
||||
export function setMaxListeners(maxListeners: number) {
|
||||
require('events').EventEmitter.defaultMaxListeners = maxListeners;
|
||||
}
|
||||
|
||||
/** Selected CLI initialization steps */
|
||||
async function init() {
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
// Proxy setup should be done early on, before loading balena-sdk
|
||||
await (await import('./utils/proxy')).setupGlobalHttpProxy(settings);
|
||||
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(
|
||||
command: string[],
|
||||
options: import('./preparser').AppOptions,
|
||||
) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
const runPromise = CustomMain.run(command).then(
|
||||
() => {
|
||||
if (!options.noFlush) {
|
||||
return require('@oclif/command/flush');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
(await import('./utils/bootstrap')).normalizeEnvVar('DEBUG');
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
// for use of the standalone zip package. See pkgExec function.
|
||||
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
|
||||
return pkgExec(cliArgs[3], cliArgs.slice(4));
|
||||
}
|
||||
|
||||
const { globalInit } = await import('./app-common');
|
||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
||||
|
||||
// globalInit() must be called very early on (before other imports) because
|
||||
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
|
||||
// shared options, and performs node version requirement checks.
|
||||
await globalInit();
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
const args = await preparseArgs(cliArgs);
|
||||
await (await import('./app-oclif')).run(args, options);
|
||||
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the 'pkgExec' command, used as a way to provide a Node.js
|
||||
* interpreter for child_process.spawn()-like operations when the CLI is
|
||||
* executing as a standalone zip package (built-in Node interpreter) and
|
||||
* the system may not have a separate Node.js installation. A present use
|
||||
* case is a patched version of the 'windosu' package that requires a
|
||||
* Node.js interpreter to spawn a privileged child process.
|
||||
*
|
||||
* @param modFunc Path to a JS module that will be executed via require().
|
||||
* The modFunc argument may optionally contain a function name separated
|
||||
* by '::', for example '::main' in:
|
||||
* 'C:\\snapshot\\balena-cli\\node_modules\\windosu\\lib\\pipe.js::main'
|
||||
* in which case that function is executed in the require'd module.
|
||||
* @param args Optional arguments to passed through process.argv and as
|
||||
* arguments to the function specified via modFunc.
|
||||
*/
|
||||
async function pkgExec(modFunc: string, args: string[]) {
|
||||
const [modPath, funcName] = modFunc.split('::');
|
||||
let replacedModPath = modPath;
|
||||
const match = modPath
|
||||
.replace(/\\/g, '/')
|
||||
.match(/\/snapshot\/balena-cli\/(.+)/);
|
||||
if (match) {
|
||||
replacedModPath = `../${match[1]}`;
|
||||
}
|
||||
process.argv = [process.argv[0], process.argv[1], ...args];
|
||||
try {
|
||||
const mod: any = await import(replacedModPath);
|
||||
if (funcName) {
|
||||
await mod[funcName](...args);
|
||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||
normalizeEnvVars();
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
// for use of the standalone zip package. See pkgExec function.
|
||||
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
|
||||
return pkgExec(cliArgs[3], cliArgs.slice(4));
|
||||
}
|
||||
|
||||
await init();
|
||||
|
||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
const args = await preparseArgs(cliArgs);
|
||||
await oclifRun(args, options);
|
||||
} catch (err) {
|
||||
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
|
||||
console.error(err);
|
||||
await (await import('./errors')).handleError(err);
|
||||
} finally {
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
* Copyright 2019-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.
|
||||
@ -22,6 +22,31 @@
|
||||
* like Sentry error reporting, preparser, oclif parser and the like.
|
||||
*/
|
||||
|
||||
export class CliSettings {
|
||||
public readonly settings: any;
|
||||
constructor() {
|
||||
this.settings = require('balena-settings-client') as typeof import('balena-settings-client');
|
||||
}
|
||||
|
||||
public get<T>(name: string): T {
|
||||
return this.settings.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like settings.get(), but return `undefined` instead of throwing an
|
||||
* error if the setting is not found / not defined.
|
||||
*/
|
||||
public getCatch<T>(name: string): T | undefined {
|
||||
try {
|
||||
return this.settings.get(name);
|
||||
} catch (err) {
|
||||
if (!/Setting not found/i.test(err.message)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBoolEnvVar(varName: string): boolean {
|
||||
return !['0', 'no', 'false', '', undefined].includes(
|
||||
process.env[varName]?.toLowerCase(),
|
||||
@ -31,3 +56,48 @@ export function parseBoolEnvVar(varName: string): boolean {
|
||||
export function normalizeEnvVar(varName: string) {
|
||||
process.env[varName] = parseBoolEnvVar(varName) ? '1' : '';
|
||||
}
|
||||
|
||||
const bootstrapVars = ['DEBUG', 'BALENARC_NO_SENTRY'];
|
||||
|
||||
export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
|
||||
for (const varName of varNames) {
|
||||
normalizeEnvVar(varName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the 'pkgExec' command, used as a way to provide a Node.js
|
||||
* interpreter for child_process.spawn()-like operations when the CLI is
|
||||
* executing as a standalone zip package (built-in Node interpreter) and
|
||||
* the system may not have a separate Node.js installation. A present use
|
||||
* case is a patched version of the 'windosu' package that requires a
|
||||
* Node.js interpreter to spawn a privileged child process.
|
||||
*
|
||||
* @param modFunc Path to a JS module that will be executed via require().
|
||||
* The modFunc argument may optionally contain a function name separated
|
||||
* by '::', for example '::main' in:
|
||||
* 'C:\\snapshot\\balena-cli\\node_modules\\windosu\\lib\\pipe.js::main'
|
||||
* in which case that function is executed in the require'd module.
|
||||
* @param args Optional arguments to passed through process.argv and as
|
||||
* arguments to the function specified via modFunc.
|
||||
*/
|
||||
export async function pkgExec(modFunc: string, args: string[]) {
|
||||
const [modPath, funcName] = modFunc.split('::');
|
||||
let replacedModPath = modPath;
|
||||
const match = modPath
|
||||
.replace(/\\/g, '/')
|
||||
.match(/\/snapshot\/balena-cli\/(.+)/);
|
||||
if (match) {
|
||||
replacedModPath = `../${match[1]}`;
|
||||
}
|
||||
process.argv = [process.argv[0], process.argv[1], ...args];
|
||||
try {
|
||||
const mod: any = await import(replacedModPath);
|
||||
if (funcName) {
|
||||
await mod[funcName](...args);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Main } from '@oclif/command';
|
||||
import type * as Config from '@oclif/config';
|
||||
|
||||
/**
|
||||
@ -48,6 +49,17 @@ export class CommandHelp {
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomMain extends Main {
|
||||
protected _helpOverride(): boolean {
|
||||
// Disable oclif's default handler for the 'version' command
|
||||
if (['-v', '--version', 'version'].includes(this.argv[0])) {
|
||||
return false;
|
||||
} else {
|
||||
return super._helpOverride();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert e.g. 'env add NAME [VALUE]' to 'env add <name> [value]' */
|
||||
export function capitanoizeOclifUsage(
|
||||
oclifUsage: string | string[] | undefined,
|
||||
|
@ -15,71 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
|
||||
class CliSettings {
|
||||
public readonly settings: any;
|
||||
constructor() {
|
||||
this.settings = require('balena-settings-client') as typeof import('balena-settings-client');
|
||||
}
|
||||
|
||||
public get<T>(name: string): T {
|
||||
return this.settings.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like settings.get(), but return `undefined` instead of throwing an
|
||||
* error if the setting is not found / not defined.
|
||||
*/
|
||||
public getCatch<T>(name: string): T | undefined {
|
||||
try {
|
||||
return this.settings.get(name);
|
||||
} catch (err) {
|
||||
if (!/Setting not found/i.test(err.message)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/error-reporting/quickstart/?platform=node
|
||||
*/
|
||||
export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtras({
|
||||
is_pkg: !!(process as any).pkg,
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
});
|
||||
});
|
||||
return Sentry.getCurrentHub();
|
||||
});
|
||||
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
import type { Options as GlobalTunnelNgConfig } from 'global-tunnel-ng';
|
||||
export type { GlobalTunnelNgConfig };
|
||||
|
||||
import { CliSettings } from './bootstrap';
|
||||
|
||||
type ProxyConfig = string | GlobalTunnelNgConfig;
|
||||
|
||||
/**
|
||||
@ -104,7 +44,7 @@ type ProxyConfig = string | GlobalTunnelNgConfig;
|
||||
* 'localhost' and '127.0.0.1' are always excluded. If NO_PROXY is not defined,
|
||||
* default exclusion patterns are added for all private IPv4 address ranges.
|
||||
*/
|
||||
async function setupGlobalHttpProxy(settings: CliSettings) {
|
||||
export async function setupGlobalHttpProxy(settings: CliSettings) {
|
||||
// `global-tunnel-ng` accepts lowercase variables with higher precedence
|
||||
// than uppercase variables, but `global-agent` does not accept lowercase.
|
||||
// Set uppercase versions for backwards compatibility.
|
||||
@ -207,42 +147,3 @@ export function makeUrlFromTunnelNgConfig(cfg: GlobalTunnelNgConfig): string {
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function setupBalenaSdkSharedOptions(settings: CliSettings) {
|
||||
// We don't yet use balena-sdk directly everywhere, but we set up shared
|
||||
// options correctly so we can do safely in submodules
|
||||
const BalenaSdk = require('balena-sdk') as typeof import('balena-sdk');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get<string>('apiUrl'),
|
||||
dataDirectory: settings.get<string>('dataDirectory'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Addresses the console warning:
|
||||
* (node:49500) MaxListenersExceededWarning: Possible EventEmitter memory
|
||||
* leak detected. 11 error listeners added. Use emitter.setMaxListeners() to
|
||||
* increase limit
|
||||
*/
|
||||
export function setMaxListeners(maxListeners: number) {
|
||||
require('events').EventEmitter.defaultMaxListeners = maxListeners;
|
||||
}
|
||||
|
||||
export async function globalInit() {
|
||||
(await import('./utils/bootstrap')).normalizeEnvVar('BALENARC_NO_SENTRY');
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
// Proxy setup should be done early on, before loading balena-sdk
|
||||
await setupGlobalHttpProxy(settings);
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
@ -23,7 +23,6 @@ import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as balenaCLI from '../build/app';
|
||||
import { setupSentry } from '../build/app-common';
|
||||
|
||||
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
|
||||
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
|
||||
@ -287,7 +286,7 @@ export function fillTemplateArray(
|
||||
export async function switchSentry(
|
||||
enabled: boolean | undefined,
|
||||
): Promise<boolean | undefined> {
|
||||
const sentryOpts = (await setupSentry()).getClient()?.getOptions();
|
||||
const sentryOpts = (await balenaCLI.setupSentry()).getClient()?.getOptions();
|
||||
if (sentryOpts) {
|
||||
const sentryStatus = sentryOpts.enabled;
|
||||
sentryOpts.enabled = enabled;
|
||||
|
@ -20,7 +20,7 @@ import { expect } from 'chai';
|
||||
import {
|
||||
GlobalTunnelNgConfig,
|
||||
makeUrlFromTunnelNgConfig,
|
||||
} from '../build/app-common';
|
||||
} from '../build/utils/proxy';
|
||||
|
||||
describe('makeUrlFromTunnelNgConfig() function', function () {
|
||||
it('should return a URL given a GlobalTunnelNgConfig object', () => {
|
Loading…
Reference in New Issue
Block a user