Skip to content

Commit dcdd3f1

Browse files
committed
terminal: Windows .exe compatibility for VSCode
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>
1 parent 6607185 commit dcdd3f1

File tree

2 files changed

+104
-59
lines changed

2 files changed

+104
-59
lines changed

packages/process/src/node/terminal-process.spec.ts

+17
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ describe('TerminalProcess', function (): void {
4848
expect(error.code).eq('ENOENT');
4949
});
5050

51+
it('test implicit .exe (Windows only)', async function (): Promise<void> {
52+
const match = /^(.+)\.exe$/.exec(process.execPath);
53+
if (!isWindows || !match) {
54+
this.skip();
55+
}
56+
57+
const command = match[1];
58+
const args = ['--version'];
59+
const terminal = await new Promise<IProcessExitEvent>((resolve, reject) => {
60+
const proc = terminalProcessFactory({ command, args });
61+
proc.onExit(resolve);
62+
proc.onError(reject);
63+
});
64+
65+
expect(terminal.code).to.exist;
66+
});
67+
5168
it('test error on trying to execute a directory', async function (): Promise<void> {
5269
const error = await new Promise<ProcessErrorEvent>((resolve, reject) => {
5370
const proc = terminalProcessFactory({ command: __dirname });

packages/process/src/node/terminal-process.ts

+87-59
Original file line numberDiff line numberDiff line change
@@ -79,72 +79,100 @@ export class TerminalProcess extends Process {
7979
}
8080
this.logger.debug('Starting terminal process', JSON.stringify(options, undefined, 2));
8181

82-
try {
83-
this.terminal = spawn(
84-
options.command,
85-
(isWindows && options.commandLine) || options.args || [],
86-
options.options || {}
87-
);
88-
89-
process.nextTick(() => this.emitOnStarted());
90-
91-
// node-pty actually wait for the underlying streams to be closed before emitting exit.
92-
// We should emulate the `exit` and `close` sequence.
93-
this.terminal.on('exit', (code, signal) => {
94-
// Make sure to only pass either code or signal as !undefined, not
95-
// both.
96-
//
97-
// node-pty quirk: On Linux/macOS, if the process exited through the
98-
// exit syscall (with an exit code), signal will be 0 (an invalid
99-
// signal value). If it was terminated because of a signal, the
100-
// signal parameter will hold the signal number and code should
101-
// be ignored.
102-
if (signal === undefined || signal === 0) {
103-
this.onTerminalExit(code, undefined);
104-
} else {
105-
this.onTerminalExit(undefined, signame(signal));
106-
}
107-
process.nextTick(() => {
108-
if (signal === undefined || signal === 0) {
109-
this.emitOnClose(code, undefined);
110-
} else {
111-
this.emitOnClose(undefined, signame(signal));
82+
const startTerminal = (command: string): { terminal: IPty | undefined, inputStream: Writable } => {
83+
try {
84+
return this.createPseudoTerminal(command, options, ringBuffer);
85+
} catch (error) {
86+
// Normalize the error to make it as close as possible as what
87+
// node's child_process.spawn would generate in the same
88+
// situation.
89+
const message: string = error.message;
90+
91+
if (message.startsWith('File not found: ') || message.endsWith(NodePtyErrors.ENOENT)) {
92+
if (isWindows && command && !command.toLowerCase().endsWith('.exe')) {
93+
const commandExe = command + '.exe';
94+
this.logger.debug(`Trying terminal command '${commandExe}' because '${command}' was not found.`);
95+
return startTerminal(commandExe);
11296
}
113-
});
114-
});
11597

116-
this.terminal.on('data', (data: string) => {
117-
ringBuffer.enq(data);
118-
});
98+
// Proceed with failure, reporting the original command because it was
99+
// the intended command and it was not found
100+
error.errno = 'ENOENT';
101+
error.code = 'ENOENT';
102+
error.path = options.command;
103+
} else if (message.endsWith(NodePtyErrors.EACCES)) {
104+
// The shell program exists but was not accessible, so just fail
105+
error.errno = 'EACCES';
106+
error.code = 'EACCES';
107+
error.path = options.command;
108+
}
119109

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

126-
} catch (error) {
127-
this.inputStream = new DevNullStream({ autoDestroy: true });
114+
return { terminal: undefined, inputStream: new DevNullStream({ autoDestroy: true }) };
115+
}
116+
};
117+
118+
const { terminal, inputStream } = startTerminal(options.command);
119+
this.terminal = terminal;
120+
this.inputStream = inputStream;
121+
}
128122

129-
// Normalize the error to make it as close as possible as what
130-
// node's child_process.spawn would generate in the same
131-
// situation.
132-
const message: string = error.message;
133-
134-
if (message.startsWith('File not found: ') || message.endsWith(NodePtyErrors.ENOENT)) {
135-
error.errno = 'ENOENT';
136-
error.code = 'ENOENT';
137-
error.path = options.command;
138-
} else if (message.endsWith(NodePtyErrors.EACCES)) {
139-
error.errno = 'EACCES';
140-
error.code = 'EACCES';
141-
error.path = options.command;
123+
/**
124+
* Helper for the constructor to attempt to create the pseudo-terminal encapsulating the shell process.
125+
*
126+
* @param command the shell command to launch
127+
* @param options options for the shell process
128+
* @param ringBuffer a ring buffer in which to collect terminal output
129+
* @returns the terminal PTY and a stream by which it may be sent input
130+
*/
131+
private createPseudoTerminal(command: string, options: TerminalProcessOptions, ringBuffer: MultiRingBuffer): { terminal: IPty | undefined, inputStream: Writable } {
132+
const terminal = spawn(
133+
command,
134+
(isWindows && options.commandLine) || options.args || [],
135+
options.options || {}
136+
);
137+
138+
process.nextTick(() => this.emitOnStarted());
139+
140+
// node-pty actually wait for the underlying streams to be closed before emitting exit.
141+
// We should emulate the `exit` and `close` sequence.
142+
terminal.onExit(({ exitCode, signal }) => {
143+
// Make sure to only pass either code or signal as !undefined, not
144+
// both.
145+
//
146+
// node-pty quirk: On Linux/macOS, if the process exited through the
147+
// exit syscall (with an exit code), signal will be 0 (an invalid
148+
// signal value). If it was terminated because of a signal, the
149+
// signal parameter will hold the signal number and code should
150+
// be ignored.
151+
if (signal === undefined || signal === 0) {
152+
this.onTerminalExit(exitCode, undefined);
153+
} else {
154+
this.onTerminalExit(undefined, signame(signal));
142155
}
156+
process.nextTick(() => {
157+
if (signal === undefined || signal === 0) {
158+
this.emitOnClose(exitCode, undefined);
159+
} else {
160+
this.emitOnClose(undefined, signame(signal));
161+
}
162+
});
163+
});
143164

144-
// node-pty throws exceptions on Windows.
145-
// Call the client error handler, but first give them a chance to register it.
146-
this.emitOnErrorAsync(error);
147-
}
165+
terminal.onData((data: string) => {
166+
ringBuffer.enq(data);
167+
});
168+
169+
const inputStream = new Writable({
170+
write: (chunk: string) => {
171+
this.write(chunk);
172+
},
173+
});
174+
175+
return { terminal, inputStream };
148176
}
149177

150178
createOutputStream(): MultiRingBufferReadableStream {

0 commit comments

Comments
 (0)