mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-30 02:28:51 +00:00
Handle ssh process exit codes
Change-type: patch
This commit is contained in:
parent
43b1c5c24f
commit
a74f0413df
@ -230,12 +230,11 @@ export const ssh: CommandDefinition<
|
|||||||
const applicationOrDevice =
|
const applicationOrDevice =
|
||||||
params.applicationOrDevice_raw || params.applicationOrDevice;
|
params.applicationOrDevice_raw || params.applicationOrDevice;
|
||||||
const { ExpectedError } = await import('../errors');
|
const { ExpectedError } = await import('../errors');
|
||||||
const { getProxyConfig, which, whichSpawn } = await import(
|
const { getProxyConfig, which } = await import('../utils/helpers');
|
||||||
'../utils/helpers'
|
|
||||||
);
|
|
||||||
const { checkLoggedIn, getOnlineTargetUuid } = await import(
|
const { checkLoggedIn, getOnlineTargetUuid } = await import(
|
||||||
'../utils/patterns'
|
'../utils/patterns'
|
||||||
);
|
);
|
||||||
|
const { spawnSshAndExitOnError } = await import('../utils/ssh');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
const verbose = options.verbose === true;
|
const verbose = options.verbose === true;
|
||||||
@ -356,6 +355,6 @@ export const ssh: CommandDefinition<
|
|||||||
username: username!,
|
username: username!,
|
||||||
});
|
});
|
||||||
|
|
||||||
await whichSpawn('ssh', command);
|
return spawnSshAndExitOnError(command);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -28,7 +28,7 @@ export async function performLocalDeviceSSH(
|
|||||||
opts: DeviceSSHOpts,
|
opts: DeviceSSHOpts,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reduce = await import('lodash/reduce');
|
const reduce = await import('lodash/reduce');
|
||||||
const { whichSpawn } = await import('../helpers');
|
const { spawnSshAndExitOnError } = await import('../ssh');
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
const { stripIndent } = await import('common-tags');
|
const { stripIndent } = await import('common-tags');
|
||||||
const { isatty } = await import('tty');
|
const { isatty } = await import('tty');
|
||||||
@ -107,7 +107,7 @@ export async function performLocalDeviceSSH(
|
|||||||
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return whichSpawn('ssh', [
|
return spawnSshAndExitOnError([
|
||||||
...(opts.verbose ? ['-vvv'] : []),
|
...(opts.verbose ? ['-vvv'] : []),
|
||||||
'-t',
|
'-t',
|
||||||
...['-p', opts.port ? opts.port.toString() : '22222'],
|
...['-p', opts.port ? opts.port.toString() : '22222'],
|
||||||
|
@ -380,36 +380,50 @@ export async function which(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Call which(programName) and spawn() with the given arguments.
|
* Call which(programName) and spawn() with the given arguments.
|
||||||
* Reject the promise if the process exit code is not zero.
|
*
|
||||||
|
* If returnExitCodeOrSignal is true, the returned promise will resolve to
|
||||||
|
* an array [code, signal] with the child process exit code number or exit
|
||||||
|
* signal string respectively (as provided by the spawn close event).
|
||||||
|
*
|
||||||
|
* If returnExitCodeOrSignal is false, the returned promise will reject with
|
||||||
|
* a custom error if the child process returns a non-zero exit code or a
|
||||||
|
* non-empty signal string (as reported by the spawn close event).
|
||||||
|
*
|
||||||
|
* In either case and if spawn itself emits an error event or fails synchronously,
|
||||||
|
* the returned promise will reject with a custom error that includes the error
|
||||||
|
* message of spawn's error.
|
||||||
*/
|
*/
|
||||||
export async function whichSpawn(
|
export async function whichSpawn(
|
||||||
programName: string,
|
programName: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
options: SpawnOptions = { stdio: 'inherit' },
|
options: SpawnOptions = { stdio: 'inherit' },
|
||||||
): Promise<void> {
|
returnExitCodeOrSignal = false,
|
||||||
|
): Promise<[number | undefined, string | undefined]> {
|
||||||
const program = await which(programName);
|
const program = await which(programName);
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
console.error(`[debug] [${program}, ${args.join(', ')}]`);
|
console.error(`[debug] [${program}, ${args.join(', ')}]`);
|
||||||
}
|
}
|
||||||
let error: Error | undefined;
|
let error: Error | undefined;
|
||||||
let exitCode: number | undefined;
|
let exitCode: number | undefined;
|
||||||
|
let exitSignal: string | undefined;
|
||||||
try {
|
try {
|
||||||
exitCode = await new Promise<number>((resolve, reject) => {
|
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
|
||||||
spawn(program, args, options)
|
spawn(program, args, options)
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('close', resolve);
|
.on('close', (code, signal) => resolve([code, signal]));
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
}
|
}
|
||||||
if (error || exitCode) {
|
if (error || (!returnExitCodeOrSignal && (exitCode || exitSignal))) {
|
||||||
const msg = [
|
const msg = [
|
||||||
`${programName} failed with exit code ${exitCode}:`,
|
`${programName} failed with exit code=${exitCode} signal=${exitSignal}:`,
|
||||||
`[${program}, ${args.join(', ')}]`,
|
`[${program}, ${args.join(', ')}]`,
|
||||||
...(error ? [`${error}`] : []),
|
...(error ? [`${error}`] : []),
|
||||||
];
|
];
|
||||||
throw new Error(msg.join('\n'));
|
throw new Error(msg.join('\n'));
|
||||||
}
|
}
|
||||||
|
return [exitCode, exitSignal];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProxyConfig {
|
export interface ProxyConfig {
|
||||||
|
@ -111,3 +111,49 @@ export async function execBuffered(
|
|||||||
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
|
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
|
||||||
execBuffered(deviceIp, 'cat /etc/os-release'),
|
execBuffered(deviceIp, 'cat /etc/os-release'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: consolidate the various forms of executing ssh child processes
|
||||||
|
// in the CLI, like exec and spawn, starting with the files:
|
||||||
|
// lib/actions/ssh.ts
|
||||||
|
// lib/utils/ssh.ts
|
||||||
|
// lib/utils/device/ssh.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the full path for ssh using which, then spawn a child process.
|
||||||
|
* - If the child process returns error code 0, return the function normally
|
||||||
|
* (do not call process.exit()).
|
||||||
|
* - If the child process returns a non-zero error code, print a single-line
|
||||||
|
* warning message and call process.exit(code) with the same non-zero error
|
||||||
|
* code.
|
||||||
|
* - If the child process is terminated by a process signal, print a
|
||||||
|
* single-line warning message and call process.exit(1).
|
||||||
|
*/
|
||||||
|
export async function spawnSshAndExitOnError(
|
||||||
|
args: string[],
|
||||||
|
options?: import('child_process').SpawnOptions,
|
||||||
|
) {
|
||||||
|
const { whichSpawn } = await import('./helpers');
|
||||||
|
const [exitCode, exitSignal] = await whichSpawn(
|
||||||
|
'ssh',
|
||||||
|
args,
|
||||||
|
options,
|
||||||
|
true, // returnExitCodeOrSignal
|
||||||
|
);
|
||||||
|
if (exitCode || exitSignal) {
|
||||||
|
// ssh returns a wide range of exit codes, including return codes of
|
||||||
|
// interactive shells. For example, if the user types CTRL-C on an
|
||||||
|
// interactive shell and then `exit`, ssh returns error code 130.
|
||||||
|
// Another example, typing "exit 1" on an interactive shell causes ssh
|
||||||
|
// to return exit code 1. In these cases, print a short one-line warning
|
||||||
|
// message, and exits the CLI process with the same error code.
|
||||||
|
const codeMsg = exitSignal
|
||||||
|
? `was terminated with signal "${exitSignal}"`
|
||||||
|
: `exited with non-zero code "${exitCode}"`;
|
||||||
|
console.error(`Warning: ssh process ${codeMsg}`);
|
||||||
|
// TODO: avoid process.exit by refactoring CLI error handling to allow
|
||||||
|
// exiting with an error code and single-line warning "without a fuss"
|
||||||
|
// about contacting support and filing Github issues. (ExpectedError
|
||||||
|
// does not currently devlivers that.)
|
||||||
|
process.exit(exitCode || 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user