Merge pull request #2378 from balena-io/events-timeout

Improve UX for offline usage
This commit is contained in:
bulldozer-balena[bot] 2021-11-25 21:45:47 +00:00 committed by GitHub
commit e22aa847e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 101 additions and 10 deletions

View File

@ -77,11 +77,13 @@ export function setMaxListeners(maxListeners: number) {
/** Selected CLI initialization steps */ /** Selected CLI initialization steps */
async function init() { async function init() {
if (process.env.BALENARC_NO_SENTRY) { if (process.env.BALENARC_NO_SENTRY) {
if (process.env.DEBUG) {
console.error(`WARN: disabling Sentry.io error reporting`); console.error(`WARN: disabling Sentry.io error reporting`);
}
} else { } else {
await setupSentry(); await setupSentry();
} }
checkNodeVersion(); await checkNodeVersion();
const settings = new CliSettings(); const settings = new CliSettings();
@ -91,8 +93,10 @@ async function init() {
setupBalenaSdkSharedOptions(settings); setupBalenaSdkSharedOptions(settings);
// check for CLI updates once a day // check for CLI updates once a day
if (!process.env.BALENARC_OFFLINE_MODE) {
(await import('./utils/update')).notify(); (await import('./utils/update')).notify();
} }
}
/** Execute the oclif parser and the CLI command. */ /** Execute the oclif parser and the CLI command. */
async function oclifRun(command: string[], options: AppOptions) { async function oclifRun(command: string[], options: AppOptions) {
@ -149,7 +153,10 @@ async function oclifRun(command: string[], options: AppOptions) {
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */ /** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
export async function run(cliArgs = process.argv, options: AppOptions = {}) { export async function run(cliArgs = process.argv, options: AppOptions = {}) {
try { try {
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap'); const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
'./utils/bootstrap'
);
setOfflineModeEnvVars();
normalizeEnvVars(); normalizeEnvVars();
// The 'pkgExec' special/internal command provides a Node.js interpreter // The 'pkgExec' special/internal command provides a Node.js interpreter

View File

@ -16,7 +16,11 @@
*/ */
import Command from '@oclif/command'; import Command from '@oclif/command';
import { InsufficientPrivilegesError } from './errors'; import {
InsufficientPrivilegesError,
NotAvailableInOfflineModeError,
} from './errors';
import { stripIndent } from './utils/lazy';
export default abstract class BalenaCommand extends Command { export default abstract class BalenaCommand extends Command {
/** /**
@ -40,6 +44,13 @@ export default abstract class BalenaCommand extends Command {
*/ */
public static authenticated = false; public static authenticated = false;
/**
* Require an internet connection to run.
* When set to true, command will exit with an error
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
*/
public static offlineCompatible = false;
/** /**
* Accept piped input. * Accept piped input.
* When set to true, command will read from stdin during init * When set to true, command will read from stdin during init
@ -97,6 +108,29 @@ export default abstract class BalenaCommand extends Command {
} }
} }
/**
* Throw NotAvailableInOfflineModeError if in offline mode.
*
* Called automatically if `onlineOnly=true`.
* Can be called explicitly by command implementation, if e.g.:
* - check should only be done conditionally
* - other code needs to execute before check
*
* Note, currently public to allow use outside of derived commands
* (as some command implementations require this. Can be made protected
* if this changes).
*
* @throws {NotAvailableInOfflineModeError}
*/
public static checkNotUsingOfflineMode() {
if (process.env.BALENARC_OFFLINE_MODE) {
throw new NotAvailableInOfflineModeError(stripIndent`
This command requires an internet connection, and cannot be used in offline mode.
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
`);
}
}
/** /**
* Read stdin contents and make available to command. * Read stdin contents and make available to command.
* *
@ -125,6 +159,10 @@ export default abstract class BalenaCommand extends Command {
await BalenaCommand.checkLoggedIn(); await BalenaCommand.checkLoggedIn();
} }
if (!ctr.offlineCompatible) {
BalenaCommand.checkNotUsingOfflineMode();
}
if (ctr.readStdin) { if (ctr.readStdin) {
await this.getStdin(); await this.getStdin();
} }

View File

@ -63,6 +63,7 @@ export default class ConfigInjectCmd extends Command {
}; };
public static root = true; public static root = true;
public static offlineCompatible = true;
public async run() { public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>( const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(

View File

@ -54,6 +54,7 @@ export default class ConfigReadCmd extends Command {
}; };
public static root = true; public static root = true;
public static offlineCompatible = true;
public async run() { public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd); const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);

View File

@ -63,7 +63,6 @@ export default class ConfigReconfigureCmd extends Command {
}; };
public static authenticated = true; public static authenticated = true;
public static root = true; public static root = true;
public async run() { public async run() {

View File

@ -70,6 +70,7 @@ export default class ConfigWriteCmd extends Command {
}; };
public static root = true; public static root = true;
public static offlineCompatible = true;
public async run() { public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>( const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(

View File

@ -63,6 +63,7 @@ export default class OsinitCmd extends Command {
public static hidden = true; public static hidden = true;
public static root = true; public static root = true;
public static offlineCompatible = true;
public async run() { public async run() {
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd); const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);

View File

@ -56,6 +56,7 @@ export default class LocalConfigureCmd extends Command {
}; };
public static root = true; public static root = true;
public static offlineCompatible = true;
public async run() { public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);

View File

@ -65,6 +65,8 @@ export default class LocalFlashCmd extends Command {
help: cf.help, help: cf.help,
}; };
public static offlineCompatible = true;
public async run() { public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>( const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
LocalFlashCmd, LocalFlashCmd,

View File

@ -68,6 +68,7 @@ export default class ScanCmd extends Command {
public static primary = true; public static primary = true;
public static root = true; public static root = true;
public static offlineCompatible = true;
public async run() { public async run() {
const _ = await import('lodash'); const _ = await import('lodash');

View File

@ -117,6 +117,7 @@ export default class SshCmd extends Command {
}; };
public static primary = true; public static primary = true;
public static offlineCompatible = true;
public async run() { public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>( const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
@ -144,6 +145,7 @@ export default class SshCmd extends Command {
const useProxy = !!proxyConfig && !options.noproxy; const useProxy = !!proxyConfig && !options.noproxy;
// this will be a tunnelled SSH connection... // this will be a tunnelled SSH connection...
await Command.checkNotUsingOfflineMode();
await Command.checkLoggedIn(); await Command.checkLoggedIn();
const deviceUuid = await getOnlineTargetDeviceUuid( const deviceUuid = await getOnlineTargetDeviceUuid(
sdk, sdk,

View File

@ -38,6 +38,8 @@ export default class UtilAvailableDrivesCmd extends Command {
help: cf.help, help: cf.help,
}; };
public static offlineCompatible = true;
public async run() { public async run() {
this.parse<FlagsDef, {}>(UtilAvailableDrivesCmd); this.parse<FlagsDef, {}>(UtilAvailableDrivesCmd);

View File

@ -57,6 +57,8 @@ export default class VersionCmd extends Command {
public static usage = 'version'; public static usage = 'version';
public static offlineCompatible = true;
public static flags: flags.Input<FlagsDef> = { public static flags: flags.Input<FlagsDef> = {
all: flags.boolean({ all: flags.boolean({
default: false, default: false,

View File

@ -104,7 +104,11 @@ export class DeprecationChecker {
const url = this.getNpmUrl(version); const url = this.getNpmUrl(version);
let response: import('got').Response<Dictionary<any>> | undefined; let response: import('got').Response<Dictionary<any>> | undefined;
try { try {
response = await got(url, { responseType: 'json', retry: 0 }); response = await got(url, {
responseType: 'json',
retry: 0,
timeout: 4000,
});
} catch (e) { } catch (e) {
// 404 is expected if `version` hasn't been published yet // 404 is expected if `version` hasn't been published yet
if (e.response?.statusCode !== 404) { if (e.response?.statusCode !== 404) {

View File

@ -31,6 +31,8 @@ export class NotLoggedInError extends ExpectedError {}
export class InsufficientPrivilegesError extends ExpectedError {} export class InsufficientPrivilegesError extends ExpectedError {}
export class NotAvailableInOfflineModeError extends ExpectedError {}
export class InvalidPortMappingError extends ExpectedError { export class InvalidPortMappingError extends ExpectedError {
constructor(mapping: string) { constructor(mapping: string) {
super(`'${mapping}' is not a valid port mapping.`); super(`'${mapping}' is not a valid port mapping.`);

View File

@ -16,7 +16,7 @@
*/ */
import * as packageJSON from '../package.json'; import * as packageJSON from '../package.json';
import { getBalenaSdk } from './utils/lazy'; import { getBalenaSdk, stripIndent } from './utils/lazy';
interface CachedUsername { interface CachedUsername {
token: string; token: string;
@ -129,10 +129,20 @@ async function sendEvent(balenaUrl: string, event: string, username?: string) {
data: Buffer.from(JSON.stringify(trackData)).toString('base64'), data: Buffer.from(JSON.stringify(trackData)).toString('base64'),
}; };
try { try {
await got(url, { searchParams, retry: 0 }); await got(url, { searchParams, retry: 0, timeout: 4000 });
} catch (e) { } catch (e) {
if (process.env.DEBUG) { if (process.env.DEBUG) {
console.error(`[debug] Event tracking error: ${e.message || e}`); console.error(`[debug] Event tracking error: ${e.message || e}`);
} }
if (e instanceof got.TimeoutError) {
console.error(stripIndent`
Timeout submitting analytics event to balenaCloud/openBalena.
If you are using the balena CLI in an air-gapped environment with a filtered
internet connection, set the BALENARC_OFFLINE_MODE=1 environment variable
when using CLI commands that do not strictly require access to balenaCloud.
`);
}
// Note: You can simulate a timeout using non-routable address 10.0.0.0
} }
} }

View File

@ -58,7 +58,13 @@ export function normalizeEnvVar(varName: string) {
process.env[varName] = parseBoolEnvVar(varName) ? '1' : ''; process.env[varName] = parseBoolEnvVar(varName) ? '1' : '';
} }
const bootstrapVars = ['DEBUG', 'BALENARC_NO_SENTRY']; const bootstrapVars = [
'BALENARC_NO_SENTRY',
'BALENARC_NO_ANALYTICS',
'BALENARC_OFFLINE_MODE',
'BALENARC_UNSUPPORTED',
'DEBUG',
];
export function normalizeEnvVars(varNames: string[] = bootstrapVars) { export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
for (const varName of varNames) { for (const varName of varNames) {
@ -66,6 +72,17 @@ export function normalizeEnvVars(varNames: string[] = bootstrapVars) {
} }
} }
/**
* Set the individual env vars implied by BALENARC_OFFLINE_MODE.
*/
export function setOfflineModeEnvVars() {
if (process.env.BALENARC_OFFLINE_MODE) {
process.env.BALENARC_UNSUPPORTED = '1';
process.env.BALENARC_NO_SENTRY = '1';
process.env.BALENARC_NO_ANALYTICS = '1';
}
}
/** /**
* Implements the 'pkgExec' command, used as a way to provide a Node.js * Implements the 'pkgExec' command, used as a way to provide a Node.js
* interpreter for child_process.spawn()-like operations when the CLI is * interpreter for child_process.spawn()-like operations when the CLI is