@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
337 lines (336 loc) • 12.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import { BrowserWindow } from 'electron';
import { validatedIpcMain } from '@sussudio/base/parts/ipc/electron-main/ipcMain.mjs';
import { Barrier } from '@sussudio/base/common/async.mjs';
import { Emitter, Event } from '@sussudio/base/common/event.mjs';
import { Disposable, DisposableStore } from '@sussudio/base/common/lifecycle.mjs';
import { FileAccess } from '@sussudio/base/common/network.mjs';
import { assertIsDefined } from '@sussudio/base/common/types.mjs';
import { connect as connectMessagePort } from '@sussudio/base/parts/ipc/electron-main/ipc.mp.mjs';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.mjs';
import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.mjs';
import { ILogService } from '../../log/common/log.mjs';
import product from '../../product/common/product.mjs';
import { IProtocolMainService } from '../../protocol/electron-main/protocol.mjs';
import { IThemeMainService } from '../../theme/electron-main/themeMainService.mjs';
import { toErrorMessage } from '@sussudio/base/common/errorMessage.mjs';
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.mjs';
import { IPolicyService } from '../../policy/common/policy.mjs';
let SharedProcess = class SharedProcess extends Disposable {
machineId;
userEnv;
environmentMainService;
userDataProfilesService;
lifecycleMainService;
logService;
policyService;
themeMainService;
protocolMainService;
firstWindowConnectionBarrier = new Barrier();
window = undefined;
windowCloseListener = undefined;
_onDidError = this._register(new Emitter());
onDidError = Event.buffer(this._onDidError.event); // buffer until we have a listener!
constructor(
machineId,
userEnv,
environmentMainService,
userDataProfilesService,
lifecycleMainService,
logService,
policyService,
themeMainService,
protocolMainService,
) {
super();
this.machineId = machineId;
this.userEnv = userEnv;
this.environmentMainService = environmentMainService;
this.userDataProfilesService = userDataProfilesService;
this.lifecycleMainService = lifecycleMainService;
this.logService = logService;
this.policyService = policyService;
this.themeMainService = themeMainService;
this.protocolMainService = protocolMainService;
this.registerListeners();
}
registerListeners() {
// Shared process connections from workbench windows
validatedIpcMain.on('vscode:createSharedProcessMessageChannel', (e, nonce) => this.onWindowConnection(e, nonce));
// Shared process worker relay
validatedIpcMain.on('vscode:relaySharedProcessWorkerMessageChannel', (e, configuration) =>
this.onWorkerConnection(e, configuration),
);
// Lifecycle
this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown()));
}
async onWindowConnection(e, nonce) {
this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel');
// release barrier if this is the first window connection
if (!this.firstWindowConnectionBarrier.isOpen()) {
this.firstWindowConnectionBarrier.open();
}
// await the shared process to be overall ready
// we do not just wait for IPC ready because the
// workbench window will communicate directly
await this.whenReady();
// connect to the shared process window
const port = await this.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()) {
return port.close();
}
// send the port back to the requesting window
e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]);
}
onWorkerConnection(e, configuration) {
this.logService.trace('SharedProcess: onWorkerConnection', configuration);
const disposables = new DisposableStore();
const disposeWorker = (reason) => {
if (!this.isAlive()) {
return; // the shared process is already gone, no need to dispose anything
}
this.logService.trace(`SharedProcess: disposing worker (reason: '${reason}')`, configuration);
// Only once!
disposables.dispose();
// Send this into the shared process who owns workers
this.send('vscode:electron-main->shared-process=disposeWorker', configuration);
};
// Ensure the sender is a valid target to send to
const receiverWindow = BrowserWindow.fromId(configuration.reply.windowId);
if (
!receiverWindow ||
receiverWindow.isDestroyed() ||
receiverWindow.webContents.isDestroyed() ||
!configuration.reply.channel
) {
disposeWorker('unavailable');
return;
}
// Attach to lifecycle of receiver to manage worker lifecycle
disposables.add(
Event.filter(
this.lifecycleMainService.onWillLoadWindow,
(e) => e.window.win === receiverWindow,
)(() => disposeWorker('load')),
);
disposables.add(Event.fromNodeEventEmitter(receiverWindow, 'closed')(() => disposeWorker('closed')));
// The shared process window asks us to relay a `MessagePort`
// from a shared process worker to the target window. It needs
// to be send via `postMessage` to transfer the port.
receiverWindow.webContents.postMessage(configuration.reply.channel, configuration.reply.nonce, e.ports);
}
onWillShutdown() {
const window = this.window;
if (!window) {
return; // possibly too early before created
}
// Signal exit to shared process when shutting down
this.send('vscode:electron-main->shared-process=exit');
// Shut the shared process down when we are quitting
//
// Note: because we veto the window close, we must first remove our veto.
// Otherwise the application would never quit because the shared process
// window is refusing to close!
//
if (this.windowCloseListener) {
window.removeListener('close', this.windowCloseListener);
this.windowCloseListener = undefined;
}
// Electron seems to crash on Windows without this setTimeout :|
setTimeout(() => {
try {
window.close();
} catch (err) {
// ignore, as electron is already shutting down
}
this.window = undefined;
}, 0);
}
send(channel, ...args) {
if (!this.isAlive()) {
this.logService.warn(`Sending IPC message to channel '${channel}' for shared process window that is destroyed`);
return;
}
try {
this.window?.webContents.send(channel, ...args);
} catch (error) {
this.logService.warn(
`Error sending IPC message to channel '${channel}' of shared process: ${toErrorMessage(error)}`,
);
}
}
_whenReady = undefined;
whenReady() {
if (!this._whenReady) {
// Overall signal that the shared process window was loaded and
// all services within have been created.
this._whenReady = new Promise((resolve) =>
validatedIpcMain.once('vscode:shared-process->electron-main=init-done', () => {
this.logService.trace('SharedProcess: Overall ready');
resolve();
}),
);
}
return this._whenReady;
}
_whenIpcReady = undefined;
get whenIpcReady() {
if (!this._whenIpcReady) {
this._whenIpcReady = (async () => {
// Always wait for first window asking for connection
await this.firstWindowConnectionBarrier.wait();
// Create window for shared process
this.createWindow();
// Listeners
this.registerWindowListeners();
// Wait for window indicating that IPC connections are accepted
await new Promise((resolve) =>
validatedIpcMain.once('vscode:shared-process->electron-main=ipc-ready', () => {
this.logService.trace('SharedProcess: IPC ready');
resolve();
}),
);
})();
}
return this._whenIpcReady;
}
createWindow() {
const configObjectUrl = this._register(this.protocolMainService.createIPCObjectUrl());
// shared process is a hidden window by default
this.window = new BrowserWindow({
show: false,
backgroundColor: this.themeMainService.getBackgroundColor(),
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js').fsPath,
additionalArguments: [
`--vscode-window-config=${configObjectUrl.resource.toString()}`,
'--vscode-window-kind=shared-process',
],
v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none',
nodeIntegration: true,
nodeIntegrationInWorker: true,
contextIsolation: false,
enableWebSQL: false,
spellcheck: false,
images: false,
webgl: false,
},
});
// Store into config object URL
configObjectUrl.update({
machineId: this.machineId,
windowId: this.window.id,
appRoot: this.environmentMainService.appRoot,
codeCachePath: this.environmentMainService.codeCachePath,
profiles: this.userDataProfilesService.profiles,
userEnv: this.userEnv,
args: this.environmentMainService.args,
logLevel: this.logService.getLevel(),
product,
policiesData: this.policyService.serialize(),
});
// Load with config
this.window.loadURL(
FileAccess.asBrowserUri(
`vs/code/electron-browser/sharedProcess/sharedProcess${this.environmentMainService.isBuilt ? '' : '-dev'}.html`,
).toString(true),
);
}
registerWindowListeners() {
if (!this.window) {
return;
}
// Prevent the window from closing
this.windowCloseListener = (e) => {
this.logService.trace('SharedProcess#close prevented');
// We never allow to close the shared process unless we get explicitly disposed()
e.preventDefault();
// Still hide the window though if visible
if (this.window?.isVisible()) {
this.window.hide();
}
};
this.window.on('close', this.windowCloseListener);
// Crashes & Unresponsive & Failed to load
// We use `onUnexpectedError` explicitly because the error handler
// will send the error to the active window to log in devtools too
this.window.webContents.on('render-process-gone', (event, details) =>
this._onDidError.fire({ type: 2 /* WindowError.PROCESS_GONE */, details }),
);
this.window.on('unresponsive', () => this._onDidError.fire({ type: 1 /* WindowError.UNRESPONSIVE */ }));
this.window.webContents.on('did-fail-load', (event, exitCode, reason) =>
this._onDidError.fire({ type: 3 /* WindowError.LOAD */, details: { reason, exitCode } }),
);
}
async connect() {
// Wait for shared process being ready to accept connection
await this.whenIpcReady;
// Connect and return message port
const window = assertIsDefined(this.window);
return connectMessagePort(window);
}
async toggle() {
// wait for window to be created
await this.whenIpcReady;
if (!this.window) {
return; // possibly disposed already
}
if (this.window.isVisible()) {
this.window.webContents.closeDevTools();
this.window.hide();
} else {
this.window.show();
this.window.webContents.openDevTools();
}
}
isVisible() {
return this.window?.isVisible() ?? false;
}
isAlive() {
const window = this.window;
if (!window) {
return false;
}
return !window.isDestroyed() && !window.webContents.isDestroyed();
}
};
SharedProcess = __decorate(
[
__param(2, IEnvironmentMainService),
__param(3, IUserDataProfilesService),
__param(4, ILifecycleMainService),
__param(5, ILogService),
__param(6, IPolicyService),
__param(7, IThemeMainService),
__param(8, IProtocolMainService),
],
SharedProcess,
);
export { SharedProcess };