Make createTerminal use exec (with $SHELL for shell)

Task type ssh-shell now supports workingDirectory option
Uses config.root as default (fixes #210)
createTerminal no longer needs active FS for relative workingDirectory
feature/ssh-config
Kelvin Schoofs 4 years ago
parent e158bf251c
commit 03fd60d36e

@ -217,6 +217,10 @@
"command": { "command": {
"type": "string", "type": "string",
"description": "The command to run on the server" "description": "The command to run on the server"
},
"workingDirectory": {
"type": "string",
"description": "The working directory (relative to the config-defined root) to run the command in"
} }
}, },
"required": [ "required": [

@ -8,6 +8,7 @@ import type { SSHPseudoTerminal } from './pseudoTerminal';
import type { SSHFileSystem } from './sshFileSystem'; import type { SSHFileSystem } from './sshFileSystem';
import { catchingPromise, toPromise } from './toPromise'; import { catchingPromise, toPromise } from './toPromise';
import { Navigation } from './webviewMessages'; import { Navigation } from './webviewMessages';
import * as path from 'path';
export enum ConfigStatus { export enum ConfigStatus {
Idle = 'Idle', Idle = 'Idle',
@ -57,6 +58,12 @@ interface Connection {
pendingUserCount: number; pendingUserCount: number;
} }
interface SSHShellTaskOptions {
host: string;
command: string;
workingDirectory?: string;
}
export class Manager implements vscode.TreeDataProvider<string | FileSystemConfig>, vscode.TaskProvider { export class Manager implements vscode.TreeDataProvider<string | FileSystemConfig>, vscode.TaskProvider {
public onDidChangeTreeData: vscode.Event<string | null>; public onDidChangeTreeData: vscode.Event<string | null>;
protected connections: Connection[] = []; protected connections: Connection[] = [];
@ -233,20 +240,21 @@ export class Manager implements vscode.TreeDataProvider<string | FileSystemConfi
}); });
return this.creatingFileSystems[name] = promise; return this.creatingFileSystems[name] = promise;
} }
public getRemotePath(config: FileSystemConfig, relativePath: string) {
if (relativePath.startsWith('/')) relativePath = relativePath.substr(1);
if (!config.root) return '/' + relativePath;
const result = path.posix.join(config.root, relativePath);
if (result.startsWith('~')) return result; // Home directory, leave the ~/
if (result.startsWith('/')) return result; // Already starts with /
return '/' + result; // Add the / to make sure it isn't seen as a relative path
}
public async createTerminal(name: string, config?: FileSystemConfig, uri?: vscode.Uri): Promise<void> { public async createTerminal(name: string, config?: FileSystemConfig, uri?: vscode.Uri): Promise<void> {
const { createTerminal } = await import('./pseudoTerminal'); const { createTerminal } = await import('./pseudoTerminal');
// Create connection (early so we have .actualConfig.root) // Create connection (early so we have .actualConfig.root)
const con = await this.createConnection(name, config); const con = await this.createConnection(name, config);
// Calculate working directory if applicable // Calculate working directory if applicable
let workingDirectory: string | undefined = uri && uri.path; let workingDirectory: string | undefined = uri && uri.path;
if (workingDirectory) { if (workingDirectory) workingDirectory = this.getRemotePath(con.actualConfig, workingDirectory);
// Normally there should be a fs, as (currently) workingDirectory is only provided
// when the user uses "Open remote SSH terminal" on a directory in the explorer view
const fs = this.fileSystems.find(fs => fs.config.name === name);
workingDirectory = fs ? fs.relative(workingDirectory) : (con.actualConfig.root || '/');
// If we don't have an FS (e.g. SSH View > Open Terminal without an active FS), we
// just use the declared `root` field, which _hopefully_ works out nicely with `cd`
}
// Create pseudo terminal // Create pseudo terminal
con.pendingUserCount++; con.pendingUserCount++;
const pty = await createTerminal({ client: con.client, config: con.actualConfig, workingDirectory }); const pty = await createTerminal({ client: con.client, config: con.actualConfig, workingDirectory });
@ -299,11 +307,13 @@ export class Manager implements vscode.TreeDataProvider<string | FileSystemConfi
return []; return [];
} }
public async resolveTask(task: vscode.Task, token?: vscode.CancellationToken | undefined): Promise<vscode.Task> { public async resolveTask(task: vscode.Task, token?: vscode.CancellationToken | undefined): Promise<vscode.Task> {
const { host, command } = task.definition as { host?: string, command?: string }; let { host, command, workingDirectory } = task.definition as unknown as SSHShellTaskOptions;
if (!host) throw new Error('Missing field \'host\' for ssh-shell task'); if (!host) throw new Error('Missing field \'host\' for ssh-shell task');
if (!command) throw new Error('Missing field \'command\' for ssh-shell task'); if (!command) throw new Error('Missing field \'command\' for ssh-shell task');
const config = getConfig(host); const config = getConfig(host);
if (!config) throw new Error(`No configuration with the name '${host}' found for ssh-shell task`); if (!config) throw new Error(`No configuration with the name '${host}' found for ssh-shell task`);
// Calculate working directory if applicable
if (workingDirectory) workingDirectory = this.getRemotePath(config, workingDirectory);
return new vscode.Task( return new vscode.Task(
task.definition, task.definition,
vscode.TaskScope.Workspace, vscode.TaskScope.Workspace,
@ -314,7 +324,7 @@ export class Manager implements vscode.TreeDataProvider<string | FileSystemConfi
connection.pendingUserCount++; connection.pendingUserCount++;
const { createTerminal } = await import('./pseudoTerminal'); const { createTerminal } = await import('./pseudoTerminal');
const psy = await createTerminal({ const psy = await createTerminal({
command, command, workingDirectory,
client: connection.client, client: connection.client,
config: connection.actualConfig, config: connection.actualConfig,
}); });

@ -23,8 +23,9 @@ export interface SSHPseudoTerminal extends vscode.Pseudoterminal {
export interface TerminalOptions { export interface TerminalOptions {
client: Client; client: Client;
config: FileSystemConfig; config: FileSystemConfig;
/** If absent, this defaults to config.root if present, otherwise whatever the remote shell picks as default */
workingDirectory?: string; workingDirectory?: string;
/** The command to run in the remote shell. If undefined, a (regular interactive) shell is started instead */ /** The command to run in the remote shell. If undefined, a (regular interactive) shell is started instead by running $SHELL*/
command?: string; command?: string;
} }
@ -54,9 +55,10 @@ export async function createTerminal(options: TerminalOptions): Promise<SSHPseud
console.log('Called pseudo.open'); console.log('Called pseudo.open');
onDidWrite.fire(`Connecting to ${config.label || config.name}...\r\n`); onDidWrite.fire(`Connecting to ${config.label || config.name}...\r\n`);
try { try {
let setupCommand: string | undefined; let commands = [options.command || '$SHELL'];
// 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;
workingDirectory = workingDirectory || config.root;
if (workingDirectory) { if (workingDirectory) {
if (workingDirectory.startsWith('~')) { if (workingDirectory.startsWith('~')) {
// So `cd "~/a/b/..." apparently doesn't work, but `~/"a/b/..."` does // So `cd "~/a/b/..." apparently doesn't work, but `~/"a/b/..."` does
@ -65,14 +67,11 @@ export async function createTerminal(options: TerminalOptions): Promise<SSHPseud
} else { } else {
workingDirectory = `"${workingDirectory}"`; workingDirectory = `"${workingDirectory}"`;
} }
setupCommand = `cd ${workingDirectory}`; commands.unshift(`cd ${workingDirectory}`);
} }
const pseudoTtyOptions: PseudoTtyOptions = { ...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows }; const pseudoTtyOptions: PseudoTtyOptions = { ...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows };
const channel = await toPromise<ClientChannel | undefined>(cb => command ? const channel = await toPromise<ClientChannel | undefined>(cb => client.exec(commands.join(';'), { pty: pseudoTtyOptions }, cb));
client.exec(setupCommand ? `${setupCommand}; ${command}` : command, { pty: pseudoTtyOptions }, cb) :
client.shell(pseudoTtyOptions, cb));
if (!channel) throw new Error('Could not create remote terminal'); if (!channel) throw new Error('Could not create remote terminal');
if (!command && setupCommand) channel.write(setupCommand + '\n');
pseudo.channel = channel; pseudo.channel = channel;
channel.on('exit', onDidClose.fire); channel.on('exit', onDidClose.fire);
channel.on('close', () => onDidClose.fire(0)); channel.on('close', () => onDidClose.fire(0));

Loading…
Cancel
Save