|
|
|
@ -11,106 +11,89 @@ const PSEUDO_TTY_OPTIONS: PseudoTtyOptions = {
|
|
|
|
|
|
|
|
|
|
export interface SSHPseudoTerminal extends vscode.Pseudoterminal {
|
|
|
|
|
onDidClose: vscode.Event<number>; // Redeclaring that it isn't undefined
|
|
|
|
|
onDidOpen: vscode.Event<void>;
|
|
|
|
|
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<SSHPseudoTerminal> {
|
|
|
|
|
const channel = await toPromise<ClientChannel | undefined>(cb => client.shell(PSEUDO_TTY_OPTIONS, cb));
|
|
|
|
|
if (!channel) throw new Error('Could not create remote terminal');
|
|
|
|
|
const onDidWrite = new vscode.EventEmitter<string>();
|
|
|
|
|
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<number>();
|
|
|
|
|
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<SSHPseudoTerminal> {
|
|
|
|
|
export async function createTerminal(options: TerminalOptions): Promise<SSHPseudoTerminal> {
|
|
|
|
|
const { client, config, command } = options;
|
|
|
|
|
const onDidWrite = new vscode.EventEmitter<string>();
|
|
|
|
|
onDidWrite.fire(`Connecting to ${config.label || config.name}...\n`);
|
|
|
|
|
const onDidClose = new vscode.EventEmitter<number>();
|
|
|
|
|
let channel: ClientChannel;
|
|
|
|
|
const onDidOpen = new vscode.EventEmitter<void>();
|
|
|
|
|
// 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<ClientChannel | undefined>(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}"`;
|
|
|
|
|
}
|
|
|
|
|
pseudo.channel = channel = ch;
|
|
|
|
|
setupCommand = `cd ${workingDirectory}`;
|
|
|
|
|
}
|
|
|
|
|
const pseudoTtyOptions: PseudoTtyOptions = { ...PSEUDO_TTY_OPTIONS, cols: dims?.columns, rows: dims?.rows };
|
|
|
|
|
const channel = await toPromise<ClientChannel | undefined>(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;
|
|
|
|
|