mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-05-02 17:13:05 +00:00
Merge pull request #2378 from balena-io/events-timeout
Improve UX for offline usage
This commit is contained in:
commit
e22aa847e3
15
lib/app.ts
15
lib/app.ts
@ -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) {
|
||||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
if (process.env.DEBUG) {
|
||||||
|
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,7 +93,9 @@ async function init() {
|
|||||||
setupBalenaSdkSharedOptions(settings);
|
setupBalenaSdkSharedOptions(settings);
|
||||||
|
|
||||||
// check for CLI updates once a day
|
// check for CLI updates once a day
|
||||||
(await import('./utils/update')).notify();
|
if (!process.env.BALENARC_OFFLINE_MODE) {
|
||||||
|
(await import('./utils/update')).notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Execute the oclif parser and the CLI command. */
|
/** Execute the oclif parser and the CLI command. */
|
||||||
@ -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
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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>(
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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>(
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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');
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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.`);
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user