未验证 提交 54b6e8fa 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #100011 from microsoft/tyriar/term_errors

Refactor terminal launch, improve error handling and add troubleshoot link to notification
......@@ -22,6 +22,8 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert';
await config.update('windowsEnableConpty', false, ConfigurationTarget.Global);
// Disable exit alerts as tests may trigger then and we're not testing the notifications
await config.update('showExitAlert', false, ConfigurationTarget.Global);
// Canvas may cause problems when running in a container
await config.update('rendererType', 'dom', ConfigurationTarget.Global);
});
suite('Terminal', () => {
......@@ -67,7 +69,9 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert';
if (data.indexOf(expected) !== 0) {
dataDisposable.dispose();
terminal.dispose();
disposables.push(window.onDidCloseTerminal(() => done()));
disposables.push(window.onDidCloseTerminal(() => {
done();
}));
}
});
disposables.push(dataDisposable);
......
......@@ -22,8 +22,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private _proxy: ExtHostTerminalServiceShape;
private _remoteAuthority: string | null;
private readonly _toDispose = new DisposableStore();
private readonly _terminalProcesses = new Map<number, Promise<ITerminalProcessExtHostProxy>>();
private readonly _terminalProcessesReady = new Map<number, (proxy: ITerminalProcessExtHostProxy) => void>();
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
private _dataEventTracker: TerminalDataEventTracker | undefined;
private _linkHandler: IDisposable | undefined;
......@@ -106,7 +105,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
isExtensionTerminal: launchConfig.isExtensionTerminal
};
const terminal = this._terminalService.createTerminal(shellLaunchConfig);
this._terminalProcesses.set(terminal.id, new Promise<ITerminalProcessExtHostProxy>(r => this._terminalProcessesReady.set(terminal.id, r)));
return Promise.resolve({
id: terminal.id,
name: terminal.title
......@@ -232,13 +230,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
const proxy = request.proxy;
const ready = this._terminalProcessesReady.get(proxy.terminalId);
if (ready) {
ready(proxy);
this._terminalProcessesReady.delete(proxy.terminalId);
} else {
this._terminalProcesses.set(proxy.terminalId, Promise.resolve(proxy));
}
this._terminalProcessProxies.set(proxy.terminalId, proxy);
const shellLaunchConfigDto: IShellLaunchConfigDto = {
name: request.shellLaunchConfig.name,
executable: request.shellLaunchConfig.executable,
......@@ -246,7 +238,16 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
cwd: request.shellLaunchConfig.cwd,
env: request.shellLaunchConfig.env
};
this._proxy.$spawnExtHostProcess(proxy.terminalId, shellLaunchConfigDto, request.activeWorkspaceRootUri, request.cols, request.rows, request.isWorkspaceShellAllowed);
this._proxy.$spawnExtHostProcess(
proxy.terminalId,
shellLaunchConfigDto,
request.activeWorkspaceRootUri,
request.cols,
request.rows,
request.isWorkspaceShellAllowed
).then(request.callback);
proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data));
proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows));
proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate));
......@@ -257,13 +258,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private _onRequestStartExtensionTerminal(request: IStartExtensionTerminalRequest): void {
const proxy = request.proxy;
const ready = this._terminalProcessesReady.get(proxy.terminalId);
if (!ready) {
this._terminalProcesses.set(proxy.terminalId, Promise.resolve(proxy));
} else {
ready(proxy);
this._terminalProcessesReady.delete(proxy.terminalId);
}
this._terminalProcessProxies.set(proxy.terminalId, proxy);
// Note that onReisze is not being listened to here as it needs to fire when max dimensions
// change, excluding the dimension override
......@@ -271,7 +266,12 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
columns: request.cols,
rows: request.rows
} : undefined;
this._proxy.$startExtensionTerminal(proxy.terminalId, initialDimensions);
this._proxy.$startExtensionTerminal(
proxy.terminalId,
initialDimensions
).then(request.callback);
proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data));
proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate));
proxy.onRequestCwd(() => this._proxy.$acceptProcessRequestCwd(proxy.terminalId));
......@@ -280,38 +280,38 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
public $sendProcessTitle(terminalId: number, title: string): void {
this._getTerminalProcess(terminalId).then(e => e.emitTitle(title));
this._getTerminalProcess(terminalId).emitTitle(title);
}
public $sendProcessData(terminalId: number, data: string): void {
this._getTerminalProcess(terminalId).then(e => e.emitData(data));
this._getTerminalProcess(terminalId).emitData(data);
}
public $sendProcessReady(terminalId: number, pid: number, cwd: string): void {
this._getTerminalProcess(terminalId).then(e => e.emitReady(pid, cwd));
this._getTerminalProcess(terminalId).emitReady(pid, cwd);
}
public $sendProcessExit(terminalId: number, exitCode: number | undefined): void {
this._getTerminalProcess(terminalId).then(e => e.emitExit(exitCode));
this._terminalProcesses.delete(terminalId);
this._getTerminalProcess(terminalId).emitExit(exitCode);
this._terminalProcessProxies.delete(terminalId);
}
public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void {
this._getTerminalProcess(terminalId).then(e => e.emitOverrideDimensions(dimensions));
this._getTerminalProcess(terminalId).emitOverrideDimensions(dimensions);
}
public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void {
this._getTerminalProcess(terminalId).then(e => e.emitInitialCwd(initialCwd));
this._getTerminalProcess(terminalId).emitInitialCwd(initialCwd);
}
public $sendProcessCwd(terminalId: number, cwd: string): void {
this._getTerminalProcess(terminalId).then(e => e.emitCwd(cwd));
this._getTerminalProcess(terminalId).emitCwd(cwd);
}
public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void {
const instance = this._terminalService.getInstanceFromId(terminalId);
if (instance) {
this._getTerminalProcess(terminalId).then(e => e.emitResolvedShellLaunchConfig(shellLaunchConfig));
this._getTerminalProcess(terminalId).emitResolvedShellLaunchConfig(shellLaunchConfig);
}
}
......@@ -324,7 +324,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
sw.stop();
sum += sw.elapsed();
}
this._getTerminalProcess(terminalId).then(e => e.emitLatency(sum / COUNT));
this._getTerminalProcess(terminalId).emitLatency(sum / COUNT);
}
private _isPrimaryExtHost(): boolean {
......@@ -349,8 +349,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
}
private _getTerminalProcess(terminalId: number): Promise<ITerminalProcessExtHostProxy> {
const terminal = this._terminalProcesses.get(terminalId);
private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy {
const terminal = this._terminalProcessProxies.get(terminalId);
if (!terminal) {
throw new Error(`Unknown terminal: ${terminalId}`);
}
......
......@@ -41,7 +41,7 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks';
import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views';
import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug';
import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder';
import { ITerminalDimensions, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions';
import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import * as search from 'vs/workbench/services/search/common/search';
......@@ -1390,8 +1390,8 @@ export interface ExtHostTerminalServiceShape {
$acceptTerminalTitleChange(id: number, name: string): void;
$acceptTerminalDimensions(id: number, cols: number, rows: number): void;
$acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void;
$spawnExtHostProcess(id: number, shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void;
$startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): void;
$spawnExtHostProcess(id: number, shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined>;
$startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise<ITerminalLaunchError | undefined>;
$acceptProcessInput(id: number, data: string): void;
$acceptProcessResize(id: number, cols: number, rows: number): void;
$acceptProcessShutdown(id: number, immediate: boolean): void;
......
......@@ -9,7 +9,7 @@ import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShap
import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ITerminalChildProcess, ITerminalDimensions, EXT_HOST_CREATION_DELAY } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalChildProcess, ITerminalDimensions, EXT_HOST_CREATION_DELAY, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { timeout } from 'vs/base/common/async';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
......@@ -17,6 +17,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { localize } from 'vs/nls';
export interface IExtHostTerminalService extends ExtHostTerminalServiceShape {
......@@ -243,6 +244,10 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess {
constructor(private readonly _pty: vscode.Pseudoterminal) { }
async start(): Promise<undefined> {
return undefined;
}
shutdown(): void {
this._pty.close();
}
......@@ -332,7 +337,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
public abstract createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal;
public abstract getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string;
public abstract getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string;
public abstract $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<void>;
public abstract $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined>;
public abstract $getAvailableShells(): Promise<IShellDefinitionDto[]>;
public abstract $getDefaultShellAndArgs(useAutomationShell: boolean): Promise<IShellAndArgsDto>;
public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void;
......@@ -454,20 +459,17 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
}
}
public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise<void> {
public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise<ITerminalLaunchError | undefined> {
// Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call
// Pseudoterminal.start
const terminal = await this._getTerminalByIdEventually(id);
if (!terminal) {
return;
return { message: localize('launchFail.idMissingOnExtHost', "Could not find the terminal with id {0} on the extension host", id) };
}
// Wait for onDidOpenTerminal to fire
let openPromise: Promise<void>;
if (terminal.isOpen) {
openPromise = Promise.resolve();
} else {
openPromise = new Promise<void>(r => {
if (!terminal.isOpen) {
await new Promise<void>(r => {
// Ensure open is called after onDidOpenTerminal
const listener = this.onDidOpenTerminal(async e => {
if (e === terminal) {
......@@ -477,7 +479,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
});
});
}
await openPromise;
if (this._terminalProcesses[id]) {
(this._terminalProcesses[id] as ExtHostPseudoterminal).startSendingEvents(initialDimensions);
......@@ -486,6 +487,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
this._extensionTerminalAwaitingStart[id] = { initialDimensions };
}
return undefined;
}
protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): IDisposable {
......@@ -721,7 +723,7 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService {
throw new Error('Not implemented');
}
public $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<void> {
public $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined> {
throw new Error('Not implemented');
}
......
......@@ -12,7 +12,7 @@ import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/termi
import { IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostConfiguration, ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration';
import { ILogService } from 'vs/platform/log/common/log';
import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/workbench/contrib/terminal/common/terminal';
import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess';
import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
......@@ -129,7 +129,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
this._variableResolver = new ExtHostVariableResolverService(workspaceFolders || [], this._extHostDocumentsAndEditors, configProvider, process.env as platform.IProcessEnvironment);
}
public async $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<void> {
public async $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined> {
const shellLaunchConfig: IShellLaunchConfig = {
name: shellLaunchConfigDto.name,
executable: shellLaunchConfigDto.executable,
......@@ -209,7 +209,15 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
// TODO: Support conpty on remote, it doesn't seem to work for some reason?
// TODO: When conpty is enabled, only enable it when accessibilityMode is off
const enableConpty = false; //terminalConfig.get('windowsEnableConpty') as boolean;
this._setupExtHostProcessListeners(id, new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, enableConpty, this._logService));
const terminalProcess = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, enableConpty, this._logService);
this._setupExtHostProcessListeners(id, terminalProcess);
const error = await terminalProcess.start();
if (error) {
// TODO: Teardown?
return error;
}
return undefined;
}
public $getAvailableShells(): Promise<IShellDefinitionDto[]> {
......
......@@ -7,7 +7,7 @@ import { Terminal as XTermTerminal } from 'xterm';
import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search';
import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11';
import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl';
import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal';
import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProcessEnvironment, Platform } from 'vs/base/common/platform';
import { Event } from 'vs/base/common/event';
......@@ -161,8 +161,8 @@ export interface ITerminalService {
preparePathForTerminalAsync(path: string, executable: string | undefined, title: string, shellType: TerminalShellType): Promise<string>;
extHostReady(remoteAuthority: string): void;
requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void;
requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void;
requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined>;
requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined>;
}
export interface ISearchOptions {
......
......@@ -37,6 +37,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { localize } from 'vs/nls';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
import { IOpenerService } from 'vs/platform/opener/common/opener';
async function getCwdForSplit(configHelper: ITerminalConfigHelper, instance: ITerminalInstance, folders?: IWorkspaceFolder[], commandService?: ICommandService): Promise<string | URI | undefined> {
switch (configHelper.config.splitCwd) {
......@@ -392,6 +393,19 @@ export class ClearTerminalAction extends Action {
}
}
export class TerminalLaunchTroubleshootAction extends Action {
constructor(
@IOpenerService private readonly _openerService: IOpenerService
) {
super('workbench.action.terminal.launchHelp', localize('terminalLaunchTroubleshoot', "Troubleshoot"));
}
async run(): Promise<void> {
this._openerService.open('https://aka.ms/vscode-troubleshoot-terminal-launch');
}
}
export function registerTerminalActions() {
const category: ILocalizedString = { value: TERMINAL_ACTION_CATEGORY, original: 'Terminal' };
......
......@@ -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, 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';
......@@ -42,6 +42,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget';
import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { TerminalLaunchTroubleshootAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
// How long in milliseconds should an average frame take to render for a notification to appear
// which suggests the fallback DOM-based renderer
......@@ -914,11 +915,13 @@ 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);
// Create the process asynchronously to allow the terminal's container to be created so
// dimensions are accurate
this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows, this._accessibilityService.isScreenReaderOptimized()).then(error => {
if (error) {
this._onProcessExit(error);
}
});
}
private getShellType(executable: string): TerminalShellType {
......@@ -953,48 +956,54 @@ 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(' ');
switch (typeof exitCodeOrError) {
case 'number':
// Only show the error if the exit code is non-zero
this._exitCode = exitCodeOrError;
if (this._exitCode === 0) {
break;
}
let commandLine: string | undefined = undefined;
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);
} else {
exitCodeMessage = nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode);
commandLine = this._shellLaunchConfig.executable;
if (typeof this._shellLaunchConfig.args === 'string') {
commandLine += ` ${this._shellLaunchConfig.args}`;
} else if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) {
commandLine += this._shellLaunchConfig.args.map(a => ` '${a}'`).join();
}
}
} else {
exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode);
}
if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
if (commandLine) {
exitCodeMessage = nls.localize('launchFailed.exitCodeAndCommandLine', "The terminal process \"{0}\" failed to launch (exit code: {1})", commandLine, this._exitCode);
break;
}
exitCodeMessage = nls.localize('launchFailed.exitCodeOnly', "The terminal process failed to launch (exit code: {0})", this._exitCode);
break;
}
if (commandLine) {
exitCodeMessage = nls.localize('terminated.exitCodeAndCommandLine', "The terminal process \"{0}\" terminated with exit code: {1}", commandLine, this._exitCode);
break;
}
exitCodeMessage = nls.localize('terminated.exitCodeOnly', "The terminal process terminated with exit code: {0}", this._exitCode);
break;
case 'object':
this._exitCode = exitCodeOrError.code;
exitCodeMessage = nls.localize('launchFailed.errorMessage', "The terminal process failed to launch: {0}", exitCodeOrError.message);
break;
}
this._logService.debug(`Terminal process exit (id: ${this.id}) state ${this._processManager.processState}`);
......@@ -1021,19 +1030,23 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
} else {
this.dispose();
if (exitCodeMessage) {
if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
this._notificationService.error(exitCodeMessage);
const failedDuringLaunch = this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH;
if (failedDuringLaunch || this._configHelper.config.showExitAlert) {
// Always show launch failures
this._notificationService.notify({
message: exitCodeMessage,
severity: Severity.Error,
actions: { primary: [this._instantiationService.createInstance(TerminalLaunchTroubleshootAction)] }
});
} else {
if (this._configHelper.config.showExitAlert) {
this._notificationService.error(exitCodeMessage);
} else {
console.warn(exitCodeMessage);
}
// Log to help surface the error in case users report issues with showExitAlert
// disabled
this._logService.warn(exitCodeMessage);
}
}
}
this._onExit.fire(exitCode);
this._onExit.fire(this._exitCode);
}
private _attachPressAnyKeyToCloseListener(xterm: XTermTerminal) {
......
......@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
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';
import * as nls from 'vs/nls';
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
let hasReceivedResponse: boolean = false;
let hasReceivedResponseFromRemoteExtHost: boolean = false;
export class TerminalProcessExtHostProxy extends Disposable implements ITerminalChildProcess, ITerminalProcessExtHostProxy {
......@@ -28,6 +28,8 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
public get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { return this._onProcessResolvedShellLaunchConfig.event; }
private readonly _onStart = this._register(new Emitter<void>());
public readonly onStart: Event<void> = this._onStart.event;
private readonly _onInput = this._register(new Emitter<string>());
public readonly onInput: Event<string> = 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 {
......@@ -79,7 +65,7 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal
}
public emitTitle(title: string): void {
hasReceivedResponse = true;
hasReceivedResponseFromRemoteExtHost = true;
this._onProcessTitleChanged.fire(title);
}
......@@ -118,6 +104,29 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal
}
}
public async start(): Promise<ITerminalLaunchError | undefined> {
// 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) {
return this._terminalService.requestStartExtensionTerminal(this, this._cols, this._rows);
}
// Add a loading title if the extension host has not started yet as there could be a
// decent wait for the user
if (!hasReceivedResponseFromRemoteExtHost) {
setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0);
}
// Fetch the environment to check shell permissions
const env = await this._remoteAgentService.getEnvironment();
if (!env) {
// Extension host processes are only allowed in remote extension hosts currently
throw new Error('Could not fetch remote environment');
}
return this._terminalService.requestSpawnExtHostProcess(this, this._shellLaunchConfig, this._activeWorkspaceRootUri, this._cols, this._rows, this._configHelper.checkWorkspaceShellPermissions(env.os));
}
public shutdown(immediate: boolean): void {
this._onShutdown.fire(immediate);
}
......
......@@ -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<void> {
): Promise<ITerminalLaunchError | undefined> {
if (shellLaunchConfig.isExtensionTerminal) {
this._processType = ProcessType.ExtensionTerminal;
this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, undefined, cols, rows, this._configHelper);
......@@ -162,6 +162,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this._process = await this._launchProcess(shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled);
}
}
this.processState = ProcessState.LAUNCHING;
this._process.onProcessData(data => {
......@@ -198,6 +199,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this.processState = ProcessState.RUNNING;
}
}, LAUNCHING_DURATION);
const error = await this._process.start();
if (error) {
return error;
}
return undefined;
}
private async _launchProcess(
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ITerminalNativeService, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE } from 'vs/workbench/contrib/terminal/common/terminal';
import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ITerminalNativeService, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
......@@ -147,18 +147,23 @@ export class TerminalService implements ITerminalService {
return activeInstance ? activeInstance : this.createTerminal(undefined);
}
public async requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<void> {
public async requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined> {
await this._extensionService.whenInstalledExtensionsRegistered();
// Wait for the remoteAuthority to be ready (and listening for events) before firing
// the event to spawn the ext host process
const conn = this._remoteAgentService.getConnection();
const remoteAuthority = conn ? conn.remoteAuthority : 'null';
await this._whenExtHostReady(remoteAuthority);
this._onInstanceRequestSpawnExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, isWorkspaceShellAllowed });
return new Promise<ITerminalLaunchError | undefined>(callback => {
this._onInstanceRequestSpawnExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, isWorkspaceShellAllowed, callback });
});
}
public requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void {
this._onInstanceRequestStartExtensionTerminal.fire({ proxy, cols, rows });
public requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined> {
// The initial request came from the extension host, no need to wait for it
return new Promise<ITerminalLaunchError | undefined>(callback => {
this._onInstanceRequestStartExtensionTerminal.fire({ proxy, cols, rows, callback });
});
}
public async extHostReady(remoteAuthority: string): Promise<void> {
......
......@@ -70,10 +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 +304,7 @@ export interface ITerminalProcessManager extends IDisposable {
readonly onEnvironmentVariableInfoChanged: Event<IEnvironmentVariableInfo>;
dispose(immediate?: boolean): void;
createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, isScreenReaderModeEnabled: boolean): Promise<void>;
createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, isScreenReaderModeEnabled: boolean): Promise<ITerminalLaunchError | undefined>;
write(data: string): void;
setDimensions(cols: number, rows: number): void;
......@@ -364,12 +360,14 @@ export interface ISpawnExtHostProcessRequest {
cols: number;
rows: number;
isWorkspaceShellAllowed: boolean;
callback: (error: ITerminalLaunchError | undefined) => void;
}
export interface IStartExtensionTerminalRequest {
proxy: ITerminalProcessExtHostProxy;
cols: number;
rows: number;
callback: (error: ITerminalLaunchError | undefined) => void;
}
export interface IAvailableShellsRequest {
......@@ -402,6 +400,11 @@ export interface IWindowsShellHelper extends IDisposable {
getShellName(): Promise<string>;
}
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 +417,14 @@ export interface ITerminalChildProcess {
onProcessOverrideDimensions?: Event<ITerminalDimensions | undefined>;
onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig>;
/**
* Starts the process.
*
* @returns undefined when the process was successfully started, otherwise an object containing
* information on what went wrong.
*/
start(): Promise<ITerminalLaunchError | undefined>;
/**
* Shutdown the terminal process.
*
......
......@@ -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<void> | 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<string>());
public get onProcessData(): Event<string> { return this._onProcessData.event; }
......@@ -38,7 +43,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
constructor(
shellLaunchConfig: IShellLaunchConfig,
private readonly _shellLaunchConfig: IShellLaunchConfig,
cwd: string,
cols: number,
rows: number,
......@@ -47,73 +52,80 @@ 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);
}
public async start(): Promise<ITerminalLaunchError | undefined> {
const results = await Promise.all([this._validateCwd(), this._validateExecutable()]);
const firstError = results.find(r => r !== undefined);
if (firstError) {
return firstError;
}
try {
this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions);
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);
} catch (err) {
this._logService.trace('IPty#spawn native exception', err);
return { message: `A native exception occurred during launch (${err.message})` };
}
}
private async _validateCwd(): Promise<undefined | ITerminalLaunchError> {
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()) };
}
return undefined;
});
} catch (err) {
if (err?.code === 'ENOENT') {
return { message: localize('launchFail.cwdDoesNotExist', "Starting directory (cwd) \"{0}\" does not exist", this._initialCwd.toString()) };
}
}
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);
private async _validateExecutable(): Promise<undefined | ITerminalLaunchError> {
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) };
}
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);
} 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 Promise.reject(SHELL_PATH_INVALID_EXIT_CODE);
return { message: localize('launchFail.executableDoesNotExist', "Shell path \"{0}\" does not exist", slc.executable) };
}
}
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);
}
return undefined;
}
private setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): void {
......@@ -124,22 +136,19 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
this._processStartupComplete = new Promise<void>(c => {
this.onProcessReady(() => c());
});
ptyProcess.on('data', data => {
ptyProcess.onData(data => {
this._onProcessData.fire(data);
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
this._queueProcessExit();
}
});
ptyProcess.on('exit', code => {
this._exitCode = code;
ptyProcess.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 {
......@@ -200,8 +209,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
this.dispose();
}
private _sendProcessId(ptyProcess: pty.IPty) {
this._onProcessReady.fire({ pid: ptyProcess.pid, cwd: this._initialCwd });
private _sendProcessId(pid: number) {
this._onProcessReady.fire({ pid, cwd: this._initialCwd });
}
private _sendProcessTitle(ptyProcess: pty.IPty): void {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册