Add remote commands (#267)

pull/285/head
Kelvin Schoofs 3 years ago
parent 315c25568f
commit 7d930d3449

@ -378,6 +378,9 @@ function parseFlagList(list: string[] | undefined, origin: string): Record<strin
- Makes it that commands are joined together using ` && ` instead of `; ` - Makes it that commands are joined together using ` && ` instead of `; `
CHECK_HOME (boolean) (default=true) CHECK_HOME (boolean) (default=true)
- Determines whether we check if the home directory exists during `createFileSystem` in the Manager - Determines whether we check if the home directory exists during `createFileSystem` in the Manager
REMOTE_COMMANDS (boolean) (default=false)
- Enables automatically launching a background command terminal during connection setup
- Enables attempting to inject a file to be sourced by the remote shells (which adds the `code` alias)
*/ */
export type FlagValue = string | boolean | null; export type FlagValue = string | boolean | null;
export type FlagCombo = [value: FlagValue, origin: string]; export type FlagCombo = [value: FlagValue, origin: string];

@ -1,4 +1,6 @@
import type { Client, ClientChannel } from 'ssh2'; import { posix as path } from 'path';
import * as readline from 'readline';
import type { Client, ClientChannel, SFTPWrapper } from 'ssh2';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { configMatches, getFlagBoolean, loadConfigs } from './config'; import { configMatches, getFlagBoolean, loadConfigs } from './config';
import type { EnvironmentVariable, FileSystemConfig } from './fileSystemConfig'; import type { EnvironmentVariable, FileSystemConfig } from './fileSystemConfig';
@ -65,6 +67,23 @@ async function tryGetHome(ssh: Client): Promise<string | null> {
return mat[1]; return mat[1];
} }
const TMP_PROFILE_SCRIPT = `
if type code > /dev/null 2> /dev/null; then
return 0;
fi
code() {
if [ ! -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
}
echo "Injected 'code' alias";
`;
export class ConnectionManager { export class ConnectionManager {
protected onConnectionAddedEmitter = new vscode.EventEmitter<Connection>(); protected onConnectionAddedEmitter = new vscode.EventEmitter<Connection>();
protected onConnectionRemovedEmitter = new vscode.EventEmitter<Connection>(); protected onConnectionRemovedEmitter = new vscode.EventEmitter<Connection>();
@ -90,6 +109,55 @@ export class ConnectionManager {
public getPendingConnections(): [string, FileSystemConfig | undefined][] { public getPendingConnections(): [string, FileSystemConfig | undefined][] {
return Object.keys(this.pendingConnections).map(name => [name, this.pendingConnections[name][1]]); return Object.keys(this.pendingConnections).map(name => [name, this.pendingConnections[name][1]]);
} }
protected async _createCommandTerminal(client: Client, authority: string): Promise<string> {
const logging = Logging.scope(`CmdTerm(${authority})`);
const shell = await toPromise<ClientChannel>(cb => client.shell({}, cb));
shell.write('echo ::sshfs:TTY:$(tty)\n');
return new Promise((resolvePath, rejectPath) => {
const rl = readline.createInterface(shell.stdout);
shell.stdout.once('error', rejectPath);
shell.once('close', () => rejectPath());
rl.on('line', async line => {
// logging.debug('<< ' + line);
const [, prefix, cmd, args] = line.match(/(.*?)::sshfs:(\w+):(.*)$/) || [];
if (!cmd || prefix.endsWith('echo ')) return;
switch (cmd) {
case 'TTY':
logging.info('Got TTY path: ' + args);
resolvePath(args);
break;
case 'code':
let [pwd, target] = args.split(':::');
if (!pwd || !target) {
logging.error(`Malformed 'code' command args: ${args}`);
return;
}
pwd = pwd.trim();
target = target.trim();
logging.info(`Received command to open '${target}' while in '${pwd}'`);
const absolutePath = target.startsWith('/') ? target : path.join(pwd, target);
const uri = vscode.Uri.parse(`ssh://${authority}/${absolutePath}`);
try {
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type & vscode.FileType.Directory) {
await vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, 0, { uri });
} else {
await vscode.window.showTextDocument(uri);
}
} catch (e) {
if (e instanceof vscode.FileSystemError) {
vscode.window.showErrorMessage(`Error opening ${absolutePath}: ${e.name.replace(/ \(FileSystemError\)/g, '')}`);
} else {
vscode.window.showErrorMessage(`Error opening ${absolutePath}: ${e.message || e}`);
}
}
return;
default:
logging.error(`Unrecognized command ${cmd} with args: ${args}`);
}
});
})
}
protected async _createConnection(name: string, config?: FileSystemConfig): Promise<Connection> { protected async _createConnection(name: string, config?: FileSystemConfig): Promise<Connection> {
const logging = Logging.scope(`createConnection(${name},${config ? 'config' : 'undefined'})`); const logging = Logging.scope(`createConnection(${name},${config ? 'config' : 'undefined'})`);
logging.info(`Creating a new connection for '${name}'`); logging.info(`Creating a new connection for '${name}'`);
@ -110,6 +178,15 @@ export class ConnectionManager {
} }
// Calculate the environment // Calculate the environment
const environment: EnvironmentVariable[] = mergeEnvironment([], config.environment); const environment: EnvironmentVariable[] = mergeEnvironment([], config.environment);
// Set up stuff for receiving remote commands
const [flagRCV, flagRCR] = getFlagBoolean('REMOTE_COMMANDS', false, actualConfig.flags);
if (flagRCV) {
logging.info(`Flag REMOTE_COMMANDS provided in '${flagRCR}', setting up command terminal`);
const cmdPath = await this._createCommandTerminal(client, name);
environment.push({ key: 'KELVIN_SSHFS_CMD_PATH', value: cmdPath });
const sftp = await toPromise<SFTPWrapper>(cb => client.sftp(cb));
await toPromise(cb => sftp.writeFile('/tmp/.Kelvin_sshfs', TMP_PROFILE_SCRIPT, { mode: 0o666 }, cb));
}
// Set up the Connection object // Set up the Connection object
let timeoutCounter = 0; let timeoutCounter = 0;
const con: Connection = { const con: Connection = {

@ -166,15 +166,26 @@ export async function createTerminal(options: TerminalOptions): Promise<SSHPseud
const [useWinCmdSep] = getFlagBoolean('WINDOWS_COMMAND_SEPARATOR', false, actualConfig.flags); const [useWinCmdSep] = getFlagBoolean('WINDOWS_COMMAND_SEPARATOR', false, actualConfig.flags);
const separator = useWinCmdSep ? ' && ' : '; '; const separator = useWinCmdSep ? ' && ' : '; ';
let commands: string[] = []; let commands: string[] = [];
let SHELL = '$SHELL';
// Add exports for environment variables if needed // Add exports for environment variables if needed
const env = mergeEnvironment(connection.environment, options.environment); const env = mergeEnvironment(connection.environment, options.environment);
commands.push(environmentToExportString(env)); commands.push(environmentToExportString(env));
// Beta feature to add a "code <file>" command in terminals to open the file locally
if (getFlagBoolean('REMOTE_COMMANDS', false, actualConfig.flags)[0]) {
// For bash
commands.push(`export ORIG_PROMPT_COMMAND="$PROMPT_COMMAND"`);
commands.push(`export PROMPT_COMMAND='source /tmp/.Kelvin_sshfs PC; $ORIG_PROMPT_COMMAND'`);
// For sh
commands.push(`export OLD_ENV="$ENV"`); // not actually used (yet?)
commands.push(`export ENV=/tmp/.Kelvin_sshfs`);
}
// Push the actual command or (default) shell command with replaced variables // Push the actual command or (default) shell command with replaced variables
if (options.command) { if (options.command) {
commands.push(replaceVariables(options.command, actualConfig)); commands.push(replaceVariables(options.command.replace(/$SHELL/g, SHELL), actualConfig));
} else { } else {
const tc = joinCommands(actualConfig.terminalCommand, separator); const tc = joinCommands(actualConfig.terminalCommand, separator);
commands.push(tc ? replaceVariables(tc, actualConfig) : '$SHELL'); let cmd = tc ? replaceVariables(tc.replace(/$SHELL/g, SHELL), actualConfig) : SHELL;
commands.push(cmd);
} }
// There isn't a proper way of setting the working directory, but this should work in most cases // There isn't a proper way of setting the working directory, but this should work in most cases
let { workingDirectory } = options; let { workingDirectory } = options;

@ -176,6 +176,7 @@ export class SSHFileSystem implements vscode.FileSystemProvider {
// Helper function to handle/report errors with proper (and minimal) stacktraces and such // Helper function to handle/report errors with proper (and minimal) stacktraces and such
protected handleError(uri: vscode.Uri, e: Error & { code?: any }, doThrow: (boolean | ((error: any) => void)) = false): any { protected handleError(uri: vscode.Uri, e: Error & { code?: any }, doThrow: (boolean | ((error: any) => void)) = false): any {
if (e.code === 2 && shouldIgnoreNotFound(uri.path)) { if (e.code === 2 && shouldIgnoreNotFound(uri.path)) {
e = vscode.FileSystemError.FileNotFound(uri);
// Whenever a workspace opens, VSCode (and extensions) (indirectly) stat a bunch of files // Whenever a workspace opens, VSCode (and extensions) (indirectly) stat a bunch of files
// (.vscode/tasks.json etc, .git/, node_modules for NodeJS, pom.xml for Maven, ...) // (.vscode/tasks.json etc, .git/, node_modules for NodeJS, pom.xml for Maven, ...)
this.logging.debug(`Ignored FileNotFound error for: ${uri}`, LOGGING_NO_STACKTRACE); this.logging.debug(`Ignored FileNotFound error for: ${uri}`, LOGGING_NO_STACKTRACE);

Loading…
Cancel
Save