98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
|
|
import * as plugins from './plugins.js';
|
||
|
|
import type { ICommandResult } from './types.js';
|
||
|
|
|
||
|
|
export interface ISshRunOptions {
|
||
|
|
extraArgs?: string[];
|
||
|
|
localForwards?: string[];
|
||
|
|
reverseForwards?: string[];
|
||
|
|
remoteCommand?: string;
|
||
|
|
exitOnForwardFailure?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class SshClient {
|
||
|
|
public buildSshArgs(host: string, options: ISshRunOptions = {}): string[] {
|
||
|
|
const args: string[] = [];
|
||
|
|
if (options.exitOnForwardFailure || options.localForwards?.length || options.reverseForwards?.length) {
|
||
|
|
args.push('-o', 'ExitOnForwardFailure=yes');
|
||
|
|
}
|
||
|
|
for (const localForward of options.localForwards ?? []) {
|
||
|
|
args.push('-L', localForward);
|
||
|
|
}
|
||
|
|
for (const reverseForward of options.reverseForwards ?? []) {
|
||
|
|
args.push('-R', reverseForward);
|
||
|
|
}
|
||
|
|
args.push(...(options.extraArgs ?? []));
|
||
|
|
args.push(host);
|
||
|
|
if (options.remoteCommand) {
|
||
|
|
args.push(options.remoteCommand);
|
||
|
|
}
|
||
|
|
return args;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async ssh(host: string, options: ISshRunOptions = {}): Promise<number> {
|
||
|
|
return this.spawnInteractive('ssh', this.buildSshArgs(host, options));
|
||
|
|
}
|
||
|
|
|
||
|
|
public async spawnInteractive(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise<number> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const child = plugins.childProcess.spawn(command, args, {
|
||
|
|
stdio: 'inherit',
|
||
|
|
shell: false,
|
||
|
|
...options,
|
||
|
|
});
|
||
|
|
|
||
|
|
child.once('error', reject);
|
||
|
|
child.once('exit', (code, signal) => {
|
||
|
|
if (signal) {
|
||
|
|
resolve(128);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
resolve(code ?? 0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public async spawnDetached(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise<number> {
|
||
|
|
const child = plugins.childProcess.spawn(command, args, {
|
||
|
|
stdio: 'ignore',
|
||
|
|
detached: true,
|
||
|
|
shell: false,
|
||
|
|
...options,
|
||
|
|
});
|
||
|
|
child.unref();
|
||
|
|
return child.pid ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async runCapture(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise<ICommandResult> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const child = plugins.childProcess.spawn(command, args, {
|
||
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||
|
|
shell: false,
|
||
|
|
...options,
|
||
|
|
});
|
||
|
|
|
||
|
|
const stdoutChunks: Buffer[] = [];
|
||
|
|
const stderrChunks: Buffer[] = [];
|
||
|
|
child.stdout?.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
||
|
|
child.stderr?.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
||
|
|
child.once('error', reject);
|
||
|
|
child.once('exit', (code) => {
|
||
|
|
resolve({
|
||
|
|
exitCode: code ?? 0,
|
||
|
|
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
||
|
|
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public async commandExists(command: string): Promise<boolean> {
|
||
|
|
const result = await this.runCapture('/bin/sh', ['-lc', `command -v ${SshClient.quoteForSh(command)}`]);
|
||
|
|
return result.exitCode === 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static quoteForSh(value: string): string {
|
||
|
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
||
|
|
}
|
||
|
|
}
|