feat(core): add SSH data access proxy CLI and core managers
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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, `'"'"'`)}'`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user