Skip to content

Commit

Permalink
terminal: Windows .exe compatibility for VSCode
Browse files Browse the repository at this point in the history
Support implicit '.exe' extension on the shell
command in terminal profiles as in VS Code.

Fixes #12734

Signed-off-by: Christian W. Damus <cdamus.ext@eclipsesource.com>
  • Loading branch information
cdamus committed Aug 1, 2023
1 parent 6607185 commit dcdd3f1
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 59 deletions.
17 changes: 17 additions & 0 deletions packages/process/src/node/terminal-process.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ describe('TerminalProcess', function (): void {
expect(error.code).eq('ENOENT');
});

it('test implicit .exe (Windows only)', async function (): Promise<void> {
const match = /^(.+)\.exe$/.exec(process.execPath);
if (!isWindows || !match) {
this.skip();
}

const command = match[1];
const args = ['--version'];
const terminal = await new Promise<IProcessExitEvent>((resolve, reject) => {
const proc = terminalProcessFactory({ command, args });
proc.onExit(resolve);
proc.onError(reject);
});

expect(terminal.code).to.exist;
});

it('test error on trying to execute a directory', async function (): Promise<void> {
const error = await new Promise<ProcessErrorEvent>((resolve, reject) => {
const proc = terminalProcessFactory({ command: __dirname });
Expand Down
146 changes: 87 additions & 59 deletions packages/process/src/node/terminal-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,72 +79,100 @@ export class TerminalProcess extends Process {
}
this.logger.debug('Starting terminal process', JSON.stringify(options, undefined, 2));

try {
this.terminal = spawn(
options.command,
(isWindows && options.commandLine) || options.args || [],
options.options || {}
);

process.nextTick(() => this.emitOnStarted());

// node-pty actually wait for the underlying streams to be closed before emitting exit.
// We should emulate the `exit` and `close` sequence.
this.terminal.on('exit', (code, signal) => {
// Make sure to only pass either code or signal as !undefined, not
// both.
//
// node-pty quirk: On Linux/macOS, if the process exited through the
// exit syscall (with an exit code), signal will be 0 (an invalid
// signal value). If it was terminated because of a signal, the
// signal parameter will hold the signal number and code should
// be ignored.
if (signal === undefined || signal === 0) {
this.onTerminalExit(code, undefined);
} else {
this.onTerminalExit(undefined, signame(signal));
}
process.nextTick(() => {
if (signal === undefined || signal === 0) {
this.emitOnClose(code, undefined);
} else {
this.emitOnClose(undefined, signame(signal));
const startTerminal = (command: string): { terminal: IPty | undefined, inputStream: Writable } => {
try {
return this.createPseudoTerminal(command, options, ringBuffer);
} catch (error) {
// Normalize the error to make it as close as possible as what
// node's child_process.spawn would generate in the same
// situation.
const message: string = error.message;

if (message.startsWith('File not found: ') || message.endsWith(NodePtyErrors.ENOENT)) {
if (isWindows && command && !command.toLowerCase().endsWith('.exe')) {
const commandExe = command + '.exe';
this.logger.debug(`Trying terminal command '${commandExe}' because '${command}' was not found.`);
return startTerminal(commandExe);
}
});
});

this.terminal.on('data', (data: string) => {
ringBuffer.enq(data);
});
// Proceed with failure, reporting the original command because it was
// the intended command and it was not found
error.errno = 'ENOENT';
error.code = 'ENOENT';
error.path = options.command;
} else if (message.endsWith(NodePtyErrors.EACCES)) {
// The shell program exists but was not accessible, so just fail
error.errno = 'EACCES';
error.code = 'EACCES';
error.path = options.command;
}

this.inputStream = new Writable({
write: (chunk: string) => {
this.write(chunk);
},
});
// node-pty throws exceptions on Windows.
// Call the client error handler, but first give them a chance to register it.
this.emitOnErrorAsync(error);

} catch (error) {
this.inputStream = new DevNullStream({ autoDestroy: true });
return { terminal: undefined, inputStream: new DevNullStream({ autoDestroy: true }) };
}
};

const { terminal, inputStream } = startTerminal(options.command);
this.terminal = terminal;
this.inputStream = inputStream;
}

// Normalize the error to make it as close as possible as what
// node's child_process.spawn would generate in the same
// situation.
const message: string = error.message;

if (message.startsWith('File not found: ') || message.endsWith(NodePtyErrors.ENOENT)) {
error.errno = 'ENOENT';
error.code = 'ENOENT';
error.path = options.command;
} else if (message.endsWith(NodePtyErrors.EACCES)) {
error.errno = 'EACCES';
error.code = 'EACCES';
error.path = options.command;
/**
* Helper for the constructor to attempt to create the pseudo-terminal encapsulating the shell process.
*
* @param command the shell command to launch
* @param options options for the shell process
* @param ringBuffer a ring buffer in which to collect terminal output
* @returns the terminal PTY and a stream by which it may be sent input
*/
private createPseudoTerminal(command: string, options: TerminalProcessOptions, ringBuffer: MultiRingBuffer): { terminal: IPty | undefined, inputStream: Writable } {
const terminal = spawn(
command,
(isWindows && options.commandLine) || options.args || [],
options.options || {}
);

process.nextTick(() => this.emitOnStarted());

// node-pty actually wait for the underlying streams to be closed before emitting exit.
// We should emulate the `exit` and `close` sequence.
terminal.onExit(({ exitCode, signal }) => {
// Make sure to only pass either code or signal as !undefined, not
// both.
//
// node-pty quirk: On Linux/macOS, if the process exited through the
// exit syscall (with an exit code), signal will be 0 (an invalid
// signal value). If it was terminated because of a signal, the
// signal parameter will hold the signal number and code should
// be ignored.
if (signal === undefined || signal === 0) {
this.onTerminalExit(exitCode, undefined);
} else {
this.onTerminalExit(undefined, signame(signal));
}
process.nextTick(() => {
if (signal === undefined || signal === 0) {
this.emitOnClose(exitCode, undefined);
} else {
this.emitOnClose(undefined, signame(signal));
}
});
});

// node-pty throws exceptions on Windows.
// Call the client error handler, but first give them a chance to register it.
this.emitOnErrorAsync(error);
}
terminal.onData((data: string) => {
ringBuffer.enq(data);
});

const inputStream = new Writable({
write: (chunk: string) => {
this.write(chunk);
},
});

return { terminal, inputStream };
}

createOutputStream(): MultiRingBufferReadableStream {
Expand Down

0 comments on commit dcdd3f1

Please sign in to comment.