balena-cli/lib/utils/ssh.ts

114 lines
3.1 KiB
TypeScript
Raw Normal View History

/**
* @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 * as Bluebird from 'bluebird';
import { spawn, StdioOptions } from 'child_process';
import * as _ from 'lodash';
import { TypedError } from 'typed-error';
export class ExecError extends TypedError {
public cmd: string;
public exitCode: number;
constructor(cmd: string, exitCode: number) {
super(`Command '${cmd}' failed with error: ${exitCode}`);
this.cmd = cmd;
this.exitCode = exitCode;
}
}
export async function exec(
deviceIp: string,
cmd: string,
stdout?: NodeJS.WritableStream,
): Promise<void> {
const { which } = await import('./helpers');
const program = await which('ssh');
const args = [
'-n',
'-t',
'-p',
'22222',
'-o',
'LogLevel=ERROR',
'-o',
'StrictHostKeyChecking=no',
'-o',
'UserKnownHostsFile=/dev/null',
`root@${deviceIp}`,
cmd,
];
if (process.env.DEBUG) {
const logger = (await import('./logger')).getLogger();
logger.logDebug(`Executing [${program},${args}]`);
}
// Note: stdin must be 'inherit' to workaround a bug in older versions of
// the built-in Windows 10 ssh client that otherwise prints the following
// to stderr and hangs: "GetConsoleMode on STD_INPUT_HANDLE failed with 6"
// They fixed the bug in newer versions of the ssh client:
// https://github.com/PowerShell/Win32-OpenSSH/issues/856
// but users whould have to manually download and install a new client.
// Note that "ssh -n" does not solve the problem, but should in theory
// prevent the ssh client from using the CLI process stdin, even if it
// is connected with 'inherit'.
const stdio: StdioOptions = [
'inherit',
stdout ? 'pipe' : 'inherit',
'inherit',
];
const exitCode = await new Bluebird<number>((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', resolve);
if (stdout) {
ps.stdout.pipe(stdout);
}
});
if (exitCode !== 0) {
throw new ExecError(cmd, exitCode);
}
}
export async function execBuffered(
deviceIp: string,
cmd: string,
enc?: string,
): Promise<string> {
const through = await import('through2');
const buffer: string[] = [];
await exec(
deviceIp,
cmd,
through(function(data, _enc, cb) {
buffer.push(data.toString(enc));
cb();
}),
);
return buffer.join('');
}
/**
* Return a device's balenaOS release by executing 'cat /etc/os-release'
* over ssh to the given deviceIp address. The result is cached with
* lodash's memoize.
*/
export const getDeviceOsRelease = _.memoize(async (deviceIp: string) =>
execBuffered(deviceIp, 'cat /etc/os-release'),
);