From e158bf251c27fc6807166843c41dc6d1001eb56f Mon Sep 17 00:00:00 2001 From: Kelvin Schoofs Date: Fri, 14 Aug 2020 18:56:13 +0200 Subject: [PATCH] Merge createTerminal and createTaskTerminal --- src/manager.ts | 6 +- src/pseudoTerminal.ts | 125 ++++++++++++++++++------------------------ src/toPromise.ts | 2 +- 3 files changed, 58 insertions(+), 75 deletions(-) diff --git a/src/manager.ts b/src/manager.ts index ceaa406..7692292 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -249,7 +249,7 @@ export class Manager implements vscode.TreeDataProvider con.terminals = con.terminals.filter(t => t !== pty)); con.terminals.push(pty); con.pendingUserCount--; @@ -312,8 +312,8 @@ export class Manager implements vscode.TreeDataProvider { const connection = await this.createConnection(host); connection.pendingUserCount++; - const { createTaskTerminal } = await import('./pseudoTerminal'); - const psy = await createTaskTerminal({ + const { createTerminal } = await import('./pseudoTerminal'); + const psy = await createTerminal({ command, client: connection.client, config: connection.actualConfig, diff --git a/src/pseudoTerminal.ts b/src/pseudoTerminal.ts index f589e90..694f1e4 100644 --- a/src/pseudoTerminal.ts +++ b/src/pseudoTerminal.ts @@ -11,106 +11,89 @@ const PSEUDO_TTY_OPTIONS: PseudoTtyOptions = { export interface SSHPseudoTerminal extends vscode.Pseudoterminal { onDidClose: vscode.Event; // Redeclaring that it isn't undefined + onDidOpen: vscode.Event; + handleInput(data: string): void; // We don't support/need read-only terminals for now + status: 'opening' | 'open' | 'closed'; config: FileSystemConfig; client: Client; /** Could be undefined if it only gets created during psy.open() instead of beforehand */ channel?: ClientChannel; } -export async function createTerminal(client: Client, config: FileSystemConfig, workingDirectory?: string): Promise { - const channel = await toPromise(cb => client.shell(PSEUDO_TTY_OPTIONS, cb)); - if (!channel) throw new Error('Could not create remote terminal'); - const onDidWrite = new vscode.EventEmitter(); - onDidWrite.fire(`Connecting to ${config.label || config.name}...\n`); - (channel as Readable).on('data', chunk => onDidWrite.fire(chunk.toString())); - channel.stderr.on('data', chunk => onDidWrite.fire(chunk.toString())); - const onDidClose = new vscode.EventEmitter(); - channel.on('exit', onDidClose.fire); - // Hopefully the exit event fires first - channel.on('close', () => onDidClose.fire(0)); - // There isn't a proper way of setting the working directory, but this should work in most cases - if (workingDirectory) { - if (workingDirectory.startsWith('~')) { - // So `cd "~/a/b/..." apparently doesn't work, but `~/"a/b/..."` does - // `"~"` would also fail but `~/""` works fine it seems - workingDirectory = `~/"${workingDirectory.substr(2)}"`; - } else { - workingDirectory = `"${workingDirectory}"`; - } - channel.write(`cd ${workingDirectory}\n`); - } - const pseudo: SSHPseudoTerminal = { - config, client, channel, - onDidWrite: onDidWrite.event, - onDidClose: onDidClose.event, - close() { - channel.signal('INT'); - channel.signal('SIGINT'); - channel.write('\x03'); - channel.close(); - }, - open(dims) { - if (!dims) return; - channel.setWindow(dims.rows, dims.columns, HEIGHT, WIDTH); - }, - setDimensions(dims) { - channel.setWindow(dims.rows, dims.columns, HEIGHT, WIDTH); - }, - handleInput(data) { - channel.write(data); - }, - }; - return pseudo; -} - -export interface TaskTerminalOptions { +export interface TerminalOptions { client: Client; config: FileSystemConfig; - command: string; + workingDirectory?: string; + /** The command to run in the remote shell. If undefined, a (regular interactive) shell is started instead */ + command?: string; } -export async function createTaskTerminal(options: TaskTerminalOptions): Promise { +export async function createTerminal(options: TerminalOptions): Promise { const { client, config, command } = options; const onDidWrite = new vscode.EventEmitter(); - onDidWrite.fire(`Connecting to ${config.label || config.name}...\n`); const onDidClose = new vscode.EventEmitter(); - let channel: ClientChannel; + const onDidOpen = new vscode.EventEmitter(); + // Won't actually open the remote terminal until pseudo.open(dims) is called const pseudo: SSHPseudoTerminal = { + status: 'opening', config, client, onDidWrite: onDidWrite.event, onDidClose: onDidClose.event, + onDidOpen: onDidOpen.event, close() { - channel?.signal('INT'); - channel?.signal('SIGINT'); - channel?.write('\x03'); - channel?.close(); + const { channel } = pseudo; + if (!channel) return; + pseudo.status = 'closed'; + channel.signal('INT'); + channel.signal('SIGINT'); + channel.write('\x03'); + channel.close(); + pseudo.channel = undefined; }, - open(dims) { - onDidWrite.fire(`Running command: ${command}\n`); - (async () => { - const ch = await toPromise(cb => client.exec(command, { - pty: { ...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows } - }, cb)); - if (!ch) { - onDidWrite.fire(`Could not create SSH channel, running task failed\n`); - onDidClose.fire(1); - return; + async open(dims) { + console.log('Called pseudo.open'); + onDidWrite.fire(`Connecting to ${config.label || config.name}...\r\n`); + try { + let setupCommand: string | undefined; + // There isn't a proper way of setting the working directory, but this should work in most cases + let { workingDirectory } = options; + if (workingDirectory) { + if (workingDirectory.startsWith('~')) { + // So `cd "~/a/b/..." apparently doesn't work, but `~/"a/b/..."` does + // `"~"` would also fail but `~/""` works fine it seems + workingDirectory = `~/"${workingDirectory.substr(2)}"`; + } else { + workingDirectory = `"${workingDirectory}"`; + } + setupCommand = `cd ${workingDirectory}`; } - pseudo.channel = channel = ch; + const pseudoTtyOptions: PseudoTtyOptions = { ...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows }; + const channel = await toPromise(cb => command ? + client.exec(setupCommand ? `${setupCommand}; ${command}` : command, { pty: pseudoTtyOptions }, cb) : + client.shell(pseudoTtyOptions, cb)); + if (!channel) throw new Error('Could not create remote terminal'); + if (!command && setupCommand) channel.write(setupCommand + '\n'); + pseudo.channel = channel; channel.on('exit', onDidClose.fire); channel.on('close', () => onDidClose.fire(0)); (channel as Readable).on('data', chunk => onDidWrite.fire(chunk.toString())); + // TODO: Keep track of stdout's color, switch to red, output, then switch back? channel.stderr.on('data', chunk => onDidWrite.fire(chunk.toString())); - })().catch(e => { - onDidWrite.fire(`Error starting process over SSH:\n${e}\n`); + // Inform others (e.g. createTaskTerminal) that the terminal is ready to be used + pseudo.status = 'open'; + onDidOpen.fire(); + } catch (e) { + onDidWrite.fire(`Error starting SSH terminal:\r\n${e}\r\n`); onDidClose.fire(1); - }); + pseudo.status = 'closed'; + pseudo.channel?.destroy(); + } }, setDimensions(dims) { - channel?.setWindow(dims.rows, dims.columns, HEIGHT, WIDTH); + pseudo.channel?.setWindow(dims.rows, dims.columns, HEIGHT, WIDTH); }, handleInput(data) { - channel?.write(data); + pseudo.channel?.write(data); }, }; return pseudo; diff --git a/src/toPromise.ts b/src/toPromise.ts index 5e2ba58..48df32f 100644 --- a/src/toPromise.ts +++ b/src/toPromise.ts @@ -1,5 +1,5 @@ -export type toPromiseCallback = (err: Error | null | undefined, res?: T) => void; +export type toPromiseCallback = (err?: Error | null, res?: T) => void; export async function toPromise(func: (cb: toPromiseCallback) => void): Promise { return new Promise((resolve, reject) => { try {