parent
16ffd1efa6
commit
cc823c6bba
@ -0,0 +1,95 @@
|
|||||||
|
import { posix as path } from 'path';
|
||||||
|
import type { Client, ClientChannel } from "ssh2";
|
||||||
|
import type { Logger } from "./logging";
|
||||||
|
import { toPromise } from "./utils";
|
||||||
|
|
||||||
|
export interface ShellConfig {
|
||||||
|
shell: string;
|
||||||
|
setEnv(key: string, value: string): string;
|
||||||
|
setupRemoteCommands(path: string): string;
|
||||||
|
embedSubstitutions(command: TemplateStringsArray, ...substitutions: (string | number)[]): string;
|
||||||
|
}
|
||||||
|
const KNOWN_SHELL_CONFIGS: Record<string, ShellConfig> = {}; {
|
||||||
|
const add = (shell: string,
|
||||||
|
setEnv: (key: string, value: string) => string,
|
||||||
|
setupRemoteCommands: (path: string) => string,
|
||||||
|
embedSubstitution: (command: TemplateStringsArray, ...substitutions: (string | number)[]) => string) => {
|
||||||
|
KNOWN_SHELL_CONFIGS[shell] = { shell, setEnv, setupRemoteCommands, embedSubstitutions: embedSubstitution };
|
||||||
|
}
|
||||||
|
// Ways to set an environment variable
|
||||||
|
const setEnvExport = (key: string, value: string) => `export ${key}=${value}`;
|
||||||
|
const setEnvSetGX = (key: string, value: string) => `set -gx ${key} ${value}`;
|
||||||
|
const setEnvSetEnv = (key: string, value: string) => `setenv ${key} ${value}`;
|
||||||
|
// Ways to set up the remote commands script auto-execution
|
||||||
|
const setupRemoteCommandsENV = (path: string) => [
|
||||||
|
`export OLD_ENV="$ENV"`, // OLD_ENV ignored for now
|
||||||
|
`export ENV="${path}"`].join('; ');
|
||||||
|
const setupRemoteCommandsPROMPT_COMMAND = (path: string) => [
|
||||||
|
`export ORIG_PROMPT_COMMAND="$PROMPT_COMMAND"`,
|
||||||
|
`export PROMPT_COMMAND='source "${path}" PC; $ORIG_PROMPT_COMMAND'`].join('; ');
|
||||||
|
const setupRemoteCommandsUnknown = () => 'echo "This shell does not yet have REMOTE_COMMANDS support"';
|
||||||
|
// Ways to embed a substitution
|
||||||
|
const embedSubstitutionsBackticks = (command: TemplateStringsArray, ...substitutions: (string | number)[]): string =>
|
||||||
|
'"' + substitutions.reduce((str, sub, i) => `${str}\`${sub}\`${command[i + 1]}`, command[0]) + '"';
|
||||||
|
const embedSubstitutionsFish = (command: TemplateStringsArray, ...substitutions: (string | number)[]) =>
|
||||||
|
substitutions.reduce((str, sub, i) => `${str}"(${sub})"${command[i + 1]}`, '"' + command[0]) + '"';
|
||||||
|
// Register the known shells
|
||||||
|
add('sh', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
||||||
|
add('bash', setEnvExport, setupRemoteCommandsPROMPT_COMMAND, embedSubstitutionsBackticks);
|
||||||
|
add('rbash', setEnvExport, setupRemoteCommandsPROMPT_COMMAND, embedSubstitutionsBackticks);
|
||||||
|
add('ash', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
||||||
|
add('dash', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
||||||
|
add('ksh', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
||||||
|
// Shells that we know `setEnv` and `embedSubstitution` for, but don't support `setupRemoteCommands` for yet
|
||||||
|
add('zsh', setEnvExport, setupRemoteCommandsUnknown, embedSubstitutionsBackticks);
|
||||||
|
add('fish', setEnvSetGX, setupRemoteCommandsUnknown, embedSubstitutionsFish); // https://fishshell.com/docs/current/tutorial.html#autoloading-functions
|
||||||
|
add('csh', setEnvSetEnv, setupRemoteCommandsUnknown, embedSubstitutionsBackticks);
|
||||||
|
add('tcsh', setEnvSetEnv, setupRemoteCommandsUnknown, embedSubstitutionsBackticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryCommand(ssh: Client, command: string): Promise<string | null> {
|
||||||
|
const exec = await toPromise<ClientChannel>(cb => ssh.exec(command, cb));
|
||||||
|
const output = ['', ''] as [string, string];
|
||||||
|
exec.stdout.on('data', (chunk: any) => output[0] += chunk);
|
||||||
|
exec.stderr.on('data', (chunk: any) => output[1] += chunk);
|
||||||
|
await toPromise(cb => {
|
||||||
|
exec.once('error', cb);
|
||||||
|
exec.once('close', cb);
|
||||||
|
}).catch(e => {
|
||||||
|
if (typeof e !== 'number') throw e;
|
||||||
|
throw new Error(`Command '${command}' failed with exit code ${e}${output[1] ? `:\n${output[1].trim()}` : ''}`);
|
||||||
|
});
|
||||||
|
if (!output[0]) {
|
||||||
|
if (!output[1]) return null;
|
||||||
|
throw new Error(`Command '${command}' only produced stderr:\n${output[1].trim()}`);
|
||||||
|
}
|
||||||
|
return output[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryEcho(ssh: Client, shellConfig: ShellConfig, variable: string): Promise<string | null> {
|
||||||
|
const uniq = Date.now() % 1e5;
|
||||||
|
const output = await tryCommand(ssh, `echo ${shellConfig.embedSubstitutions`::${'echo ' + uniq}:echo_result:${`echo ${variable}`}:${'echo ' + uniq}::`}`);
|
||||||
|
return output?.match(`::${uniq}:echo_result:(.*?):${uniq}::`)?.[1] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function calculateShellConfig(client: Client, logging?: Logger): Promise<ShellConfig> {
|
||||||
|
try {
|
||||||
|
const shellStdout = await tryCommand(client, 'echo :::SHELL:$SHELL:SHELL:::');
|
||||||
|
const shell = shellStdout?.match(/:::SHELL:([^$].*?):SHELL:::/)?.[1];
|
||||||
|
if (!shell) {
|
||||||
|
if (shellStdout) logging?.error(`Could not get $SHELL from following output:\n${shellStdout}`);
|
||||||
|
throw new Error('Could not get $SHELL');
|
||||||
|
}
|
||||||
|
const known = KNOWN_SHELL_CONFIGS[path.basename(shell)];
|
||||||
|
if (known) {
|
||||||
|
logging?.debug(`Detected known $SHELL '${shell}' (${known.shell})`);
|
||||||
|
return known;
|
||||||
|
} else {
|
||||||
|
logging?.warning(`Unrecognized $SHELL '${shell}', using default ShellConfig instead`);
|
||||||
|
return { ...KNOWN_SHELL_CONFIGS['sh'], shell };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logging && logging.error`Error calculating ShellConfig: ${e}`;
|
||||||
|
return { ...KNOWN_SHELL_CONFIGS['sh'], shell: '???' };
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue