|
|
@ -1,18 +1,63 @@
|
|
|
|
import { posix as path } from 'path';
|
|
|
|
import { posix as path } from 'path';
|
|
|
|
import type { Client, ClientChannel } from "ssh2";
|
|
|
|
import type { Client, ClientChannel, SFTPWrapper } from "ssh2";
|
|
|
|
import type { Logger } from "./logging";
|
|
|
|
import type { Connection } from './connection';
|
|
|
|
|
|
|
|
import { Logger, Logging } from "./logging";
|
|
|
|
import { toPromise } from "./utils";
|
|
|
|
import { toPromise } from "./utils";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const SCRIPT_COMMAND_CODE = `#!/bin/sh
|
|
|
|
|
|
|
|
if [ "$#" -ne 1 ] || [ $1 = "help" ] || [ $1 = "--help" ] || [ $1 = "-h" ] || [ $1 = "-?" ]; then
|
|
|
|
|
|
|
|
echo "Usage:";
|
|
|
|
|
|
|
|
echo " code <path_to_existing_file> Will make VS Code open the file";
|
|
|
|
|
|
|
|
echo " code <path_to_existing_folder> Will make VS Code add the folder as an additional workspace folder";
|
|
|
|
|
|
|
|
echo " code <path_to_nonexisting_file> Will prompt VS Code to create an empty file, then open it afterwards";
|
|
|
|
|
|
|
|
elif [ ! -n "$KELVIN_SSHFS_CMD_PATH" ]; then
|
|
|
|
|
|
|
|
echo "Not running in a terminal spawned by SSH FS? Failed to sent!"
|
|
|
|
|
|
|
|
elif [ -c "$KELVIN_SSHFS_CMD_PATH" ]; then
|
|
|
|
|
|
|
|
echo "::sshfs:code:$(pwd):::$1" >> $KELVIN_SSHFS_CMD_PATH;
|
|
|
|
|
|
|
|
echo "Command sent to SSH FS extension";
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
echo "Missing command shell pty of SSH FS extension? Failed to sent!"
|
|
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type RemoteCommandInitializer = (connection: Connection) => void
|
|
|
|
|
|
|
|
| string | string[] | undefined
|
|
|
|
|
|
|
|
| Promise<void | string | string[] | undefined>;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureCachedFile(connection: Connection, key: string, path: string, content: string, sftp?: SFTPWrapper):
|
|
|
|
|
|
|
|
Promise<[written: boolean, path: string | null]> {
|
|
|
|
|
|
|
|
const rc_files: Record<string, string> = connection.cache.rc_files ||= {};
|
|
|
|
|
|
|
|
if (rc_files[key]) return [false, rc_files[key]];
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
sftp ||= await toPromise<SFTPWrapper>(cb => connection.client.sftp(cb));
|
|
|
|
|
|
|
|
await toPromise(cb => sftp!.writeFile(path, content, { mode: 0o755 }, cb));
|
|
|
|
|
|
|
|
return [true, rc_files[key] = path];
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
Logging.error`Failed to write ${key} file to '${path}':\n${e}`;
|
|
|
|
|
|
|
|
return [false, null];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function rcInitializePATH(connection: Connection): Promise<string[] | string> {
|
|
|
|
|
|
|
|
const dir = `/tmp/.Kelvin_sshfs.RcBin.${connection.actualConfig.username || Date.now()}`;
|
|
|
|
|
|
|
|
const sftp = await toPromise<SFTPWrapper>(cb => connection.client.sftp(cb));
|
|
|
|
|
|
|
|
await toPromise(cb => sftp!.mkdir(dir, { mode: 0o755 }, cb)).catch(() => { });
|
|
|
|
|
|
|
|
const [, path] = await ensureCachedFile(connection, 'CmdCode', `${dir}/code`, SCRIPT_COMMAND_CODE, sftp);
|
|
|
|
|
|
|
|
return path ? [
|
|
|
|
|
|
|
|
connection.shellConfig.setEnv('PATH', `${dir}:$PATH`),
|
|
|
|
|
|
|
|
] : 'echo "An error occured while adding REMOTE_COMMANDS support"';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ShellConfig {
|
|
|
|
export interface ShellConfig {
|
|
|
|
shell: string;
|
|
|
|
shell: string;
|
|
|
|
setEnv(key: string, value: string): string;
|
|
|
|
setEnv(key: string, value: string): string;
|
|
|
|
setupRemoteCommands(path: string): string;
|
|
|
|
setupRemoteCommands: RemoteCommandInitializer;
|
|
|
|
embedSubstitutions(command: TemplateStringsArray, ...substitutions: (string | number)[]): string;
|
|
|
|
embedSubstitutions(command: TemplateStringsArray, ...substitutions: (string | number)[]): string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const KNOWN_SHELL_CONFIGS: Record<string, ShellConfig> = {}; {
|
|
|
|
const KNOWN_SHELL_CONFIGS: Record<string, ShellConfig> = {}; {
|
|
|
|
const add = (shell: string,
|
|
|
|
const add = (shell: string,
|
|
|
|
setEnv: (key: string, value: string) => string,
|
|
|
|
setEnv: (key: string, value: string) => string,
|
|
|
|
setupRemoteCommands: (path: string) => string,
|
|
|
|
setupRemoteCommands: RemoteCommandInitializer,
|
|
|
|
embedSubstitution: (command: TemplateStringsArray, ...substitutions: (string | number)[]) => string) => {
|
|
|
|
embedSubstitution: (command: TemplateStringsArray, ...substitutions: (string | number)[]) => string) => {
|
|
|
|
KNOWN_SHELL_CONFIGS[shell] = { shell, setEnv, setupRemoteCommands, embedSubstitutions: embedSubstitution };
|
|
|
|
KNOWN_SHELL_CONFIGS[shell] = { shell, setEnv, setupRemoteCommands, embedSubstitutions: embedSubstitution };
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -20,31 +65,22 @@ const KNOWN_SHELL_CONFIGS: Record<string, ShellConfig> = {}; {
|
|
|
|
const setEnvExport = (key: string, value: string) => `export ${key}=${value}`;
|
|
|
|
const setEnvExport = (key: string, value: string) => `export ${key}=${value}`;
|
|
|
|
const setEnvSetGX = (key: string, value: string) => `set -gx ${key} ${value}`;
|
|
|
|
const setEnvSetGX = (key: string, value: string) => `set -gx ${key} ${value}`;
|
|
|
|
const setEnvSetEnv = (key: string, value: string) => `setenv ${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
|
|
|
|
// Ways to embed a substitution
|
|
|
|
const embedSubstitutionsBackticks = (command: TemplateStringsArray, ...substitutions: (string | number)[]): string =>
|
|
|
|
const embedSubstitutionsBackticks = (command: TemplateStringsArray, ...substitutions: (string | number)[]): string =>
|
|
|
|
'"' + substitutions.reduce((str, sub, i) => `${str}\`${sub}\`${command[i + 1]}`, command[0]) + '"';
|
|
|
|
'"' + substitutions.reduce((str, sub, i) => `${str}\`${sub}\`${command[i + 1]}`, command[0]) + '"';
|
|
|
|
const embedSubstitutionsFish = (command: TemplateStringsArray, ...substitutions: (string | number)[]) =>
|
|
|
|
const embedSubstitutionsFish = (command: TemplateStringsArray, ...substitutions: (string | number)[]) =>
|
|
|
|
substitutions.reduce((str, sub, i) => `${str}"(${sub})"${command[i + 1]}`, '"' + command[0]) + '"';
|
|
|
|
substitutions.reduce((str, sub, i) => `${str}"(${sub})"${command[i + 1]}`, '"' + command[0]) + '"';
|
|
|
|
// Register the known shells
|
|
|
|
// Register the known shells
|
|
|
|
add('sh', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
|
|
|
add('sh', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('bash', setEnvExport, setupRemoteCommandsPROMPT_COMMAND, embedSubstitutionsBackticks);
|
|
|
|
add('bash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('rbash', setEnvExport, setupRemoteCommandsPROMPT_COMMAND, embedSubstitutionsBackticks);
|
|
|
|
add('rbash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('ash', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
|
|
|
add('ash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('dash', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
|
|
|
add('dash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('ksh', setEnvExport, setupRemoteCommandsENV, embedSubstitutionsBackticks);
|
|
|
|
add('ksh', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
// Shells that we know `setEnv` and `embedSubstitution` for, but don't support `setupRemoteCommands` for yet
|
|
|
|
add('zsh', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('zsh', setEnvExport, setupRemoteCommandsUnknown, embedSubstitutionsBackticks);
|
|
|
|
add('fish', setEnvSetGX, rcInitializePATH, embedSubstitutionsFish); // https://fishshell.com/docs/current/tutorial.html#autoloading-functions
|
|
|
|
add('fish', setEnvSetGX, setupRemoteCommandsUnknown, embedSubstitutionsFish); // https://fishshell.com/docs/current/tutorial.html#autoloading-functions
|
|
|
|
add('csh', setEnvSetEnv, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('csh', setEnvSetEnv, setupRemoteCommandsUnknown, embedSubstitutionsBackticks);
|
|
|
|
add('tcsh', setEnvSetEnv, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
add('tcsh', setEnvSetEnv, setupRemoteCommandsUnknown, embedSubstitutionsBackticks);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function tryCommand(ssh: Client, command: string): Promise<string | null> {
|
|
|
|
export async function tryCommand(ssh: Client, command: string): Promise<string | null> {
|
|
|
|