diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index c385606345f..e3c69055cdb 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -243,6 +243,10 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { constructor(private readonly _pty: vscode.Pseudoterminal) { } + async start(): Promise { + return undefined; + } + shutdown(): void { this._pty.close(); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index ac5745e13ed..5ae66e068ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -25,7 +25,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { IShellLaunchConfig, ITerminalDimensions, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, IWindowsShellHelper, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, LEGACY_CONSOLE_MODE_EXIT_CODE, DEFAULT_COMMANDS_TO_SKIP_SHELL } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalDimensions, ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, IWindowsShellHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, LEGACY_CONSOLE_MODE_EXIT_CODE, DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; @@ -911,9 +911,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Create the process asynchronously to allow the terminal's container // to be created so dimensions are accurate - setTimeout(() => { - this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows, this._accessibilityService.isScreenReaderOptimized()); - }, 0); + this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows, this._accessibilityService.isScreenReaderOptimized()).then(error => { + if (error) { + // TODO: Tear down + this._onProcessExit(error); + } + }); } private getShellType(executable: string): TerminalShellType { @@ -948,48 +951,53 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { * @param exitCode The exit code of the process, this is undefined when the terminal was exited * through user action. */ - private _onProcessExit(exitCode?: number): void { + private _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError): void { // Prevent dispose functions being triggered multiple times if (this._isExiting) { return; } - this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${exitCode}`); + this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${this._exitCode}`); - this._exitCode = exitCode; this._isExiting = true; let exitCodeMessage: string | undefined; // Create exit code message - if (exitCode) { - if (exitCode === SHELL_PATH_INVALID_EXIT_CODE) { - exitCodeMessage = nls.localize('terminal.integrated.exitedWithInvalidPath', 'The terminal shell path "{0}" does not exist', this._shellLaunchConfig.executable); - } else if (exitCode === SHELL_PATH_DIRECTORY_EXIT_CODE) { - exitCodeMessage = nls.localize('terminal.integrated.exitedWithInvalidPathDirectory', 'The terminal shell path "{0}" is a directory', this._shellLaunchConfig.executable); - } else if (exitCode === SHELL_CWD_INVALID_EXIT_CODE && this._shellLaunchConfig.cwd) { - exitCodeMessage = nls.localize('terminal.integrated.exitedWithInvalidCWD', 'The terminal shell CWD "{0}" does not exist', this._shellLaunchConfig.cwd.toString()); - } else if (exitCode === LEGACY_CONSOLE_MODE_EXIT_CODE) { - exitCodeMessage = nls.localize('terminal.integrated.legacyConsoleModeError', 'The terminal failed to launch properly because your system has legacy console mode enabled, uncheck "Use legacy console" cmd.exe\'s properties to fix this.'); - } else if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) { - let args = ''; - if (typeof this._shellLaunchConfig.args === 'string') { - args = ` ${this._shellLaunchConfig.args}`; - } else if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) { - args = ' ' + this._shellLaunchConfig.args.map(a => { - if (typeof a === 'string' && a.indexOf(' ') !== -1) { - return `'${a}'`; - } - return a; - }).join(' '); - } - if (this._shellLaunchConfig.executable) { - exitCodeMessage = nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode); + switch (typeof exitCodeOrError) { + case 'number': + this._exitCode = exitCodeOrError; + // TODO: Add button for all failures to a help page + + if (this._exitCode === LEGACY_CONSOLE_MODE_EXIT_CODE) { + exitCodeMessage = nls.localize('terminal.integrated.legacyConsoleModeError', 'The terminal failed to launch properly because your system has legacy console mode enabled, uncheck "Use legacy console" cmd.exe\'s properties to fix this.'); + } else if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) { + let args = ''; + if (typeof this._shellLaunchConfig.args === 'string') { + args = ` ${this._shellLaunchConfig.args}`; + } else if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) { + args = ' ' + this._shellLaunchConfig.args.map(a => { + if (typeof a === 'string' && a.indexOf(' ') !== -1) { + return `'${a}'`; + } + return a; + }).join(' '); + } + if (this._shellLaunchConfig.executable) { + console.log('FAIL 1', this._shellLaunchConfig.executable); + console.log('FAIL 2', args); + console.log('FAIL 3', '"' + this._exitCode + '"'); + exitCodeMessage = nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, this._exitCode); + } else { + exitCodeMessage = nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', this._exitCode); + } } else { - exitCodeMessage = nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode); + exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', this._exitCode); } - } else { - exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode); - } + break; + case 'object': + this._exitCode = exitCodeOrError.code; + exitCodeMessage = `${nls.localize('launchError.failed', "The terminal failed to launch:")} ${exitCodeOrError.message}`; + break; } this._logService.debug(`Terminal process exit (id: ${this.id}) state ${this._processManager.processState}`); @@ -1028,7 +1036,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - this._onExit.fire(exitCode); + this._onExit.fire(this._exitCode); } private _attachPressAnyKeyToCloseListener(xterm: XTermTerminal) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index eee8ddafca5..4237d02462e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { ITerminalProcessExtHostProxy, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessExtHostProxy, IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalDimensions, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -28,6 +28,8 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } + private readonly _onStart = this._register(new Emitter()); + public readonly onStart: Event = this._onStart.event; private readonly _onInput = this._register(new Emitter()); public readonly onInput: Event = this._onInput.event; private readonly _onResize: Emitter<{ cols: number, rows: number }> = this._register(new Emitter<{ cols: number, rows: number }>()); @@ -47,31 +49,15 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal constructor( public terminalId: number, - shellLaunchConfig: IShellLaunchConfig, - activeWorkspaceRootUri: URI | undefined, - cols: number, - rows: number, - configHelper: ITerminalConfigHelper, + private _shellLaunchConfig: IShellLaunchConfig, + private _activeWorkspaceRootUri: URI | undefined, + private _cols: number, + private _rows: number, + private _configHelper: ITerminalConfigHelper, @ITerminalService private readonly _terminalService: ITerminalService, - @IRemoteAgentService readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService ) { super(); - - // Request a process if needed, if this is a virtual process this step can be skipped as - // there is no real "process" and we know it's ready on the ext host already. - if (shellLaunchConfig.isExtensionTerminal) { - this._terminalService.requestStartExtensionTerminal(this, cols, rows); - } else { - remoteAgentService.getEnvironment().then(env => { - if (!env) { - throw new Error('Could not fetch environment'); - } - this._terminalService.requestSpawnExtHostProcess(this, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper.checkWorkspaceShellPermissions(env.os)); - }); - if (!hasReceivedResponse) { - setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0); - } - } } public emitData(data: string): void { @@ -118,6 +104,26 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal } } + public async start(): Promise { + // Request a process if needed, if this is a virtual process this step can be skipped as + // there is no real "process" and we know it's ready on the ext host already. + if (this._shellLaunchConfig.isExtensionTerminal) { + this._terminalService.requestStartExtensionTerminal(this, this._cols, this._rows); + } else { + const env = await this._remoteAgentService.getEnvironment(); + if (!env) { + throw new Error('Could not fetch environment'); + } + this._terminalService.requestSpawnExtHostProcess(this, this._shellLaunchConfig, this._activeWorkspaceRootUri, this._cols, this._rows, this._configHelper.checkWorkspaceShellPermissions(env.os)); + if (!hasReceivedResponse) { + setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0); + } + } + + // TODO: Wait and return + return undefined; + } + public shutdown(immediate: boolean): void { this._onShutdown.fire(immediate); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 2cf53d92986..f81b442e8b9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -6,7 +6,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { env as processEnv } from 'vs/base/common/process'; -import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent, ITerminalEnvironment, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper, ITerminalChildProcess, IBeforeProcessDataEvent, ITerminalEnvironment, ITerminalDimensions, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -127,7 +127,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce cols: number, rows: number, isScreenReaderModeEnabled: boolean - ): Promise { + ): Promise { if (shellLaunchConfig.isExtensionTerminal) { this._processType = ProcessType.ExtensionTerminal; this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, undefined, cols, rows, this._configHelper); @@ -162,7 +162,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._process = await this._launchProcess(shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled); } } + this.processState = ProcessState.LAUNCHING; + const error = await this._process.start(); + if (error) { + return error; + } this._process.onProcessData(data => { const beforeProcessDataEvent: IBeforeProcessDataEvent = { data }; @@ -198,6 +203,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this.processState = ProcessState.RUNNING; } }, LAUNCHING_DURATION); + + return undefined; } private async _launchProcess( diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index dcb635fca96..ae04a256cb5 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -70,9 +70,6 @@ export const TERMINAL_ACTION_CATEGORY = nls.localize('terminalCategory', "Termin export const DEFAULT_LETTER_SPACING = 0; export const MINIMUM_LETTER_SPACING = -5; export const DEFAULT_LINE_HEIGHT = 1; -export const SHELL_PATH_INVALID_EXIT_CODE = -1; -export const SHELL_PATH_DIRECTORY_EXIT_CODE = -2; -export const SHELL_CWD_INVALID_EXIT_CODE = -3; export const LEGACY_CONSOLE_MODE_EXIT_CODE = 3221225786; // microsoft/vscode#73790 export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; @@ -308,7 +305,7 @@ export interface ITerminalProcessManager extends IDisposable { readonly onEnvironmentVariableInfoChanged: Event; dispose(immediate?: boolean): void; - createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, isScreenReaderModeEnabled: boolean): Promise; + createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, isScreenReaderModeEnabled: boolean): Promise; write(data: string): void; setDimensions(cols: number, rows: number): void; @@ -402,6 +399,11 @@ export interface IWindowsShellHelper extends IDisposable { getShellName(): Promise; } +export interface ITerminalLaunchError { + message: string; + code?: number; +} + /** * An interface representing a raw terminal child process, this contains a subset of the * child_process.ChildProcess node.js interface. @@ -414,6 +416,14 @@ export interface ITerminalChildProcess { onProcessOverrideDimensions?: Event; onProcessResolvedShellLaunchConfig?: Event; + /** + * Starts the process. + * + * @returns undefined when the process was successfully started, otherwise an object containing + * information on what went wrong. + */ + start(): Promise; + /** * Shutdown the terminal process. * diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index 6d49ecb8d38..0b26a04ccb2 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -11,22 +11,27 @@ import * as fs from 'fs'; import { Event, Emitter } from 'vs/base/common/event'; import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalChildProcess, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; import { exec } from 'child_process'; import { ILogService } from 'vs/platform/log/common/log'; import { stat } from 'vs/base/node/pfs'; import { findExecutable } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; export class TerminalProcess extends Disposable implements ITerminalChildProcess { private _exitCode: number | undefined; + private _exitMessage: string | undefined; private _closeTimeout: any; private _ptyProcess: pty.IPty | undefined; private _currentTitle: string = ''; private _processStartupComplete: Promise | undefined; private _isDisposed: boolean = false; private _titleInterval: NodeJS.Timer | null = null; - private _initialCwd: string; + private readonly _initialCwd: string; + private readonly _ptyOptions: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions; + + public get exitMessage(): string | undefined { return this._exitMessage; } private readonly _onProcessData = this._register(new Emitter()); public get onProcessData(): Event { return this._onProcessData.event; } @@ -38,7 +43,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } constructor( - shellLaunchConfig: IShellLaunchConfig, + private readonly _shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, @@ -47,73 +52,89 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess @ILogService private readonly _logService: ILogService ) { super(); - let shellName: string; + let name: string; if (os.platform() === 'win32') { - shellName = path.basename(shellLaunchConfig.executable || ''); + name = path.basename(this._shellLaunchConfig.executable || ''); } else { // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a // color prompt as defined in the default ~/.bashrc file. - shellName = 'xterm-256color'; + name = 'xterm-256color'; } - this._initialCwd = cwd; - const useConpty = windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309; - const options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions = { - name: shellName, + this._ptyOptions = { + name, cwd, env, cols, rows, useConpty, // This option will force conpty to not redraw the whole viewport on launch - conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText + conptyInheritCursor: useConpty && !!_shellLaunchConfig.initialText }; - - // TODO: Pull verification out into its own function - const cwdVerification = stat(cwd).then(async stat => { - if (!stat.isDirectory()) { - return Promise.reject(SHELL_CWD_INVALID_EXIT_CODE); - } - return undefined; - }, async err => { - if (err && err.code === 'ENOENT') { - // So we can include in the error message the specified CWD - shellLaunchConfig.cwd = cwd; - return Promise.reject(SHELL_CWD_INVALID_EXIT_CODE); - } - return undefined; - }); - - const executableVerification = stat(shellLaunchConfig.executable!).then(async stat => { - if (!stat.isFile() && !stat.isSymbolicLink()) { - return Promise.reject(stat.isDirectory() ? SHELL_PATH_DIRECTORY_EXIT_CODE : SHELL_PATH_INVALID_EXIT_CODE); - } - return undefined; - }, async (err) => { - if (err && err.code === 'ENOENT') { - let cwd = shellLaunchConfig.cwd instanceof URI ? shellLaunchConfig.cwd.path : shellLaunchConfig.cwd!; - // Try to get path - const envPaths: string[] | undefined = (shellLaunchConfig.env && shellLaunchConfig.env.PATH) ? shellLaunchConfig.env.PATH.split(path.delimiter) : undefined; - const executable = await findExecutable(shellLaunchConfig.executable!, cwd, envPaths); - if (!executable) { - return Promise.reject(SHELL_PATH_INVALID_EXIT_CODE); - } - } - return undefined; - }); - - Promise.all([cwdVerification, executableVerification]).then(() => { - this.setupPtyProcess(shellLaunchConfig, options); - }).catch((exitCode: number) => { - return this._launchFailed(exitCode); - }); } - private _launchFailed(exitCode: number): void { - this._exitCode = exitCode; - this._queueProcessExit(); - this._processStartupComplete = Promise.resolve(undefined); + public async start(): Promise { + const results = await Promise.all([this._validateCwd(), this._validateExecutable()]); + const firstError = results.find(r => r !== undefined); + if (firstError) { + // TODO: Anything else we need to do here? + // this._processStartupComplete = Promise.resolve(undefined); + // this._onProcessExit.fire(0); + + return firstError; + } + + try { + this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); + return undefined; + } catch (err) { + + // TODO: Anything else we need to do here? + + // TODO: Extract code and message from the native exception, currently they're combined + // A native exception occurred + this._logService.trace('IPty#spawn native exception', err); + return { message: `A native exception occurred during launch (${err.message})` }; + } + } + + private async _validateCwd(): Promise { + try { + const result = await stat(this._initialCwd); + if (!result.isDirectory()) { + return { message: localize('launchFail.cwdNotDirectory', 'Starting directory (cwd) "{0}" is not a directory', this._initialCwd.toString()) }; + } + } catch (err) { + if (err?.code === 'ENOENT') { + return { message: localize('launchFail.cwdDoesNotExist', 'Starting directory (cwd) "{0}" does not exist', this._initialCwd.toString()) }; + } + } + return undefined; + } + + private async _validateExecutable(): Promise { + const slc = this._shellLaunchConfig; + if (!slc.executable) { + throw new Error('IShellLaunchConfig.executable not set'); + } + try { + const result = await stat(slc.executable); + if (!result.isFile() && !result.isSymbolicLink()) { + return { message: localize('launchFail.executableIsNotFileOrSymlink', 'Shell path "{0}" is not a file of a symlink', slc.executable) }; + } + } catch (err) { + if (err?.code === 'ENOENT') { + // The executable isn't an absolute path, try find it on the PATH or CWD + let cwd = slc.cwd instanceof URI ? slc.cwd.path : slc.cwd!; + const envPaths: string[] | undefined = (slc.env && slc.env.PATH) ? slc.env.PATH.split(path.delimiter) : undefined; + const executable = await findExecutable(slc.executable!, cwd, envPaths); + if (!executable) { + return { message: localize('launchFail.executableDoesNotExist', 'Shell path "{0}" does not exist') }; + } + } + } + return undefined; } private setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): void { @@ -132,14 +153,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } }); ptyProcess.onExit(e => { + console.log('onExit', e); this._exitCode = e.exitCode; this._queueProcessExit(); }); this._setupTitlePolling(ptyProcess); - // TODO: We should no longer need to delay this since pty.spawn is sync - setTimeout(() => { - this._sendProcessId(ptyProcess); - }, 500); + this._sendProcessId(ptyProcess.pid); } public dispose(): void {