Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt utility process for pty host on Electron #182345

Merged
merged 11 commits into from
May 15, 2023
3 changes: 1 addition & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,14 +931,13 @@ export class CodeApplication extends Disposable {
graceTime: LocalReconnectConstants.GraceTime,
shortGraceTime: LocalReconnectConstants.ShortGraceTime,
scrollback: this.configurationService.getValue<number>(TerminalSettingId.PersistentSessionScrollback) ?? 100
}, this.environmentMainService);
}, this.environmentMainService, this.lifecycleMainService, this.logService);
const ptyHostService = new PtyHostService(
ptyHostStarter,
this.configurationService,
this.logService,
this.loggerService
);
ptyHostService.initialize();
services.set(ILocalPtyService, ptyHostService);

// External terminal
Expand Down
6 changes: 5 additions & 1 deletion src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export enum TerminalIpcChannels {
* Communicates between the shared process and the pty host process.
*/
PtyHost = 'ptyHost',
/**
* Communicates between the renderer process and the pty host process.
*/
PtyHostWindow = 'ptyHostWindow',
/**
* Deals with logging from the pty host process.
*/
Expand Down Expand Up @@ -269,7 +273,7 @@ export interface IPtyHostController {
readonly onPtyHostResponsive?: Event<void>;
readonly onPtyHostRequestResolveVariables?: Event<IRequestResolveVariablesEvent>;

restartPtyHost?(): Promise<void>;
restartPtyHost?(): void;
acceptPtyHostResolvedVariables?(requestId: number, resolved: string[]): Promise<void>;
}

Expand Down
131 changes: 88 additions & 43 deletions src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,105 @@
*--------------------------------------------------------------------------------------------*/

import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { parsePtyHostDebugPort } from 'vs/platform/environment/node/environmentService';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { ILogService } from 'vs/platform/log/common/log';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IReconnectConstants } from 'vs/platform/terminal/common/terminal';
import { NodePtyHostStarter } from 'vs/platform/terminal/node/nodePtyHostStarter';
import { IPtyHostConnection, IPtyHostStarter } from 'vs/platform/terminal/node/ptyHost';
// import { FileAccess } from 'vs/base/common/network';
// import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
// import { parsePtyHostDebugPort } from 'vs/platform/environment/node/environmentService';
// import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess';
import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/electron-main/ipc.mp';
import { IpcMainEvent } from 'electron';
import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';

export class ElectronPtyHostStarter implements IPtyHostStarter {

// private utilityProcess: UtilityProcess | undefined = undefined;
private utilityProcess: UtilityProcess | undefined = undefined;

private readonly _onBeforeWindowConnection = new Emitter<void>();
readonly onBeforeWindowConnection = this._onBeforeWindowConnection.event;
private readonly _onWillShutdown = new Emitter<void>();
readonly onWillShutdown = this._onWillShutdown.event;

constructor(
private readonly _reconnectConstants: IReconnectConstants,
@IEnvironmentService private readonly _environmentService: INativeEnvironmentService
@IEnvironmentService private readonly _environmentService: INativeEnvironmentService,
@ILifecycleMainService private readonly _lifecycleMainService: ILifecycleMainService,
@ILogService private readonly _logService: ILogService
) {
this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire());
// Listen for new windows to establish connection directly to pty host
validatedIpcMain.on('vscode:createPtyHostMessageChannel', (e, nonce) => this._onWindowConnection(e, nonce));
}

start(lastPtyId: number): IPtyHostConnection {
return new NodePtyHostStarter(this._reconnectConstants, this._environmentService).start(lastPtyId);

// console.log('use utility proc');

// // TODO: Convert to use utility process
// const opts: IIPCOptions = {
// serverName: 'Pty Host',
// args: ['--type=ptyHost', '--logsPath', this._environmentService.logsHome.fsPath],
// env: {
// VSCODE_LAST_PTY_ID: lastPtyId,
// VSCODE_PTY_REMOTE: this._isRemote,
// VSCODE_AMD_ENTRYPOINT: 'vs/platform/terminal/node/ptyHostMain',
// VSCODE_PIPE_LOGGING: 'true',
// VSCODE_VERBOSE_LOGGING: 'true', // transmit console logs from server to client,
// VSCODE_RECONNECT_GRACE_TIME: this._reconnectConstants.graceTime,
// VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.shortGraceTime,
// VSCODE_RECONNECT_SCROLLBACK: this._reconnectConstants.scrollback
// }
// };

// const ptyHostDebug = parsePtyHostDebugPort(this._environmentService.args, this._environmentService.isBuilt);
// if (ptyHostDebug) {
// if (ptyHostDebug.break && ptyHostDebug.port) {
// opts.debugBrk = ptyHostDebug.port;
// } else if (!ptyHostDebug.break && ptyHostDebug.port) {
// opts.debug = ptyHostDebug.port;
// }
// }

// const client = new Client(FileAccess.asFileUri('bootstrap-fork').fsPath, opts);

// return {
// client,
// dispose: client.dispose,
// onDidProcessExit: client.onDidProcessExit
// };
this.utilityProcess = new UtilityProcess(this._logService, NullTelemetryService, this._lifecycleMainService);

const inspectParams = parsePtyHostDebugPort(this._environmentService.args, this._environmentService.isBuilt);
let execArgv: string[] | undefined = undefined;
if (inspectParams) {
execArgv = ['--nolazy'];
if (inspectParams.break) {
execArgv.push(`--inspect-brk=${inspectParams.port}`);
} else if (!inspectParams.break) {
execArgv.push(`--inspect=${inspectParams.port}`);
}
}

this.utilityProcess.start({
type: 'ptyHost',
entryPoint: 'vs/platform/terminal/node/ptyHostMain',
payload: this._createPtyHostConfiguration(lastPtyId),
execArgv
});

const port = this.utilityProcess.connect();
const client = new MessagePortClient(port, 'ptyHost');

const store = new DisposableStore();
store.add(client);
store.add(this.utilityProcess);
store.add(toDisposable(() => {
validatedIpcMain.removeHandler('vscode:createPtyHostMessageChannel');
this.utilityProcess = undefined;
}));

return {
client,
store,
onDidProcessExit: this.utilityProcess.onExit
};
}

private _createPtyHostConfiguration(lastPtyId: number) {
return {
VSCODE_LAST_PTY_ID: lastPtyId,
VSCODE_AMD_ENTRYPOINT: 'vs/platform/terminal/node/ptyHostMain',
VSCODE_PIPE_LOGGING: 'true',
VSCODE_VERBOSE_LOGGING: 'true', // transmit console logs from server to client,
VSCODE_RECONNECT_GRACE_TIME: this._reconnectConstants.graceTime,
VSCODE_RECONNECT_SHORT_GRACE_TIME: this._reconnectConstants.shortGraceTime,
VSCODE_RECONNECT_SCROLLBACK: this._reconnectConstants.scrollback
};
}

private _onWindowConnection(e: IpcMainEvent, nonce: string) {
this._onBeforeWindowConnection.fire();

const port = this.utilityProcess!.connect();

// Check back if the requesting window meanwhile closed
// Since shared process is delayed on startup there is
// a chance that the window close before the shared process
// was ready for a connection.

if (e.sender.isDestroyed()) {
port.close();
return;
}

e.sender.postMessage('vscode:createPtyHostMessageChannelResult', nonce, [port]);
}
}
6 changes: 5 additions & 1 deletion src/vs/platform/terminal/node/nodePtyHostStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { DisposableStore } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
Expand Down Expand Up @@ -43,9 +44,12 @@ export class NodePtyHostStarter implements IPtyHostStarter {

const client = new Client(FileAccess.asFileUri('bootstrap-fork').fsPath, opts);

const store = new DisposableStore();
store.add(client);

return {
client,
dispose: client.dispose,
store,
onDidProcessExit: client.onDidProcessExit
};
}
Expand Down
8 changes: 6 additions & 2 deletions src/vs/platform/terminal/node/ptyHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
*--------------------------------------------------------------------------------------------*/

import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IChannelClient } from 'vs/base/parts/ipc/common/ipc';

export interface IPtyHostConnection extends IDisposable {
export interface IPtyHostConnection {
readonly client: IChannelClient;
readonly store: DisposableStore;
readonly onDidProcessExit: Event<{ code: number; signal: string }>;
}

export interface IPtyHostStarter {
onBeforeWindowConnection?: Event<void>;
onWillShutdown?: Event<void>;

/**
* Creates a pty host and connects to it.
*
Expand Down
10 changes: 8 additions & 2 deletions src/vs/platform/terminal/node/ptyHostMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import { HeartbeatService } from 'vs/platform/terminal/node/heartbeatService';
import { PtyService } from 'vs/platform/terminal/node/ptyService';
import { isUtilityProcess } from 'vs/base/parts/sandbox/node/electronTypes';

const _isUtilityProcess = isUtilityProcess(process);

let server: ChildProcessServer<string> | UtilityProcessServer;
if (isUtilityProcess(process)) {
if (_isUtilityProcess) {
server = new UtilityProcessServer();
} else {
server = new ChildProcessServer(TerminalIpcChannels.PtyHost);
Expand Down Expand Up @@ -53,7 +55,11 @@ delete process.env.VSCODE_RECONNECT_SHORT_GRACE_TIME;
delete process.env.VSCODE_RECONNECT_SCROLLBACK;

const ptyService = new PtyService(lastPtyId, logService, productService, reconnectConstants);
server.registerChannel(TerminalIpcChannels.PtyHost, ProxyChannel.fromService(ptyService));
const ptyServiceChannel = ProxyChannel.fromService(ptyService);
server.registerChannel(TerminalIpcChannels.PtyHost, ptyServiceChannel);
if (_isUtilityProcess) {
server.registerChannel(TerminalIpcChannels.PtyHostWindow, ptyServiceChannel);
}

process.once('exit', () => {
logService.dispose();
Expand Down
57 changes: 41 additions & 16 deletions src/vs/platform/terminal/node/ptyHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,28 @@ let lastPtyId = 0;
export class PtyHostService extends Disposable implements IPtyService {
declare readonly _serviceBrand: undefined;

private _connection: IPtyHostConnection;
private __connection?: IPtyHostConnection;
// ProxyChannel is not used here because events get lost when forwarding across multiple proxies
private _proxy: IPtyService;
private __proxy?: IPtyService;

private get _connection(): IPtyHostConnection {
this._ensurePtyHost();
return this.__connection!;
}
private get _proxy(): IPtyService {
this._ensurePtyHost();
return this.__proxy!;
}

private _ensurePtyHost() {
if (!this.__connection) {
[this.__connection, this.__proxy] = this._startPtyHost();
}
}

private readonly _shellEnv: Promise<typeof process.env>;
private readonly _resolveVariablesRequestStore: RequestStore<string[], { workspaceId: string; originalText: string[] }>;
private _wasQuitRequested = false;
private _restartCount = 0;
private _isResponsive = true;
private _isDisposed = false;
Expand Down Expand Up @@ -90,20 +106,19 @@ export class PtyHostService extends Disposable implements IPtyService {

this._register(toDisposable(() => this._disposePtyHost()));


this._resolveVariablesRequestStore = this._register(new RequestStore(undefined, this._logService));
this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables);

[this._connection, this._proxy] = this._startPtyHost();

this._register(this._configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(TerminalSettingId.IgnoreProcessNames)) {
await this._refreshIgnoreProcessNames();
}
}));
}
// Force the pty host to start as the first window is starting if the starter has that
// capability
if (this._ptyHostStarter.onBeforeWindowConnection) {
Event.once(this._ptyHostStarter.onBeforeWindowConnection)(() => this._ensurePtyHost());
} else {
this._ensurePtyHost();
}

initialize(): void {
this._refreshIgnoreProcessNames();
this._ptyHostStarter.onWillShutdown?.(() => this._wasQuitRequested = true);
}

private get _ignoreProcessNames(): string[] {
Expand Down Expand Up @@ -142,7 +157,7 @@ export class PtyHostService extends Disposable implements IPtyService {
// Handle exit
this._register(connection.onDidProcessExit(e => {
this._onPtyHostExit.fire(e.code);
if (!this._isDisposed) {
if (!this._wasQuitRequested && !this._isDisposed) {
if (this._restartCount <= Constants.MaxRestarts) {
this._logService.error(`ptyHost terminated unexpectedly with code ${e.code}`);
this._restartCount++;
Expand All @@ -169,6 +184,16 @@ export class PtyHostService extends Disposable implements IPtyService {
this._register(new RemoteLoggerChannelClient(this._loggerService, client.getChannel(TerminalIpcChannels.Logger)));
});

this.__connection = connection;
this.__proxy = proxy;

this._register(this._configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(TerminalSettingId.IgnoreProcessNames)) {
await this._refreshIgnoreProcessNames();
}
}));
this._refreshIgnoreProcessNames();

return [connection, proxy];
}

Expand Down Expand Up @@ -315,14 +340,14 @@ export class PtyHostService extends Disposable implements IPtyService {
}

async restartPtyHost(): Promise<void> {
this._isResponsive = true;
this._disposePtyHost();
[this._connection, this._proxy] = this._startPtyHost();
this._isResponsive = true;
this._startPtyHost();
}

private _disposePtyHost(): void {
this._proxy.shutdownAll?.();
this._connection.dispose();
this._connection.store.dispose();
}

private _handleHeartbeat() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export abstract class BaseTerminalBackend extends Disposable {
let unresponsiveNotification: INotificationHandle | undefined;
if (eventSource.onPtyHostStart) {
this._register(eventSource.onPtyHostStart(() => {
this._logService.info(`ptyHost restarted`);
this._onPtyHostRestart.fire();
unresponsiveNotification?.close();
unresponsiveNotification = undefined;
Expand Down
Loading