Merge pull request #21879 from Microsoft/joao/shared-process

Move shared process to a browser window
This commit is contained in:
João Moreno
2017-03-07 11:34:35 +01:00
committed by GitHub
18 changed files with 327 additions and 152 deletions

View File

@@ -20,6 +20,6 @@ exports.collectModules= function() {
createModuleDescription('vs/code/electron-main/main', []),
createModuleDescription('vs/code/node/cli', []),
createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']),
createModuleDescription('vs/code/node/sharedProcessMain', [])
createModuleDescription('vs/code/electron-browser/sharedProcessMain', [])
];
};

View File

@@ -0,0 +1,16 @@
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body aria-label="">
Shared Process
</body>
<!-- Startup via index.js -->
<script src="sharedProcess.js"></script>
</html>

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Warning: Do not use the `let` declarator in this file, it breaks our minification
'use strict';
/*global window,document,define*/
const path = require('path');
const electron = require('electron');
const remote = electron.remote;
const ipc = electron.ipcRenderer;
function assign(destination, source) {
return Object.keys(source)
.reduce(function (r, key) { r[key] = source[key]; return r; }, destination);
}
function parseURLQueryArgs() {
const search = window.location.search || '';
return search.split(/[?&]/)
.filter(function (param) { return !!param; })
.map(function (param) { return param.split('='); })
.filter(function (param) { return param.length === 2; })
.reduce(function (r, param) { r[param[0]] = decodeURIComponent(param[1]); return r; }, {});
}
function createScript(src, onload) {
const script = document.createElement('script');
script.src = src;
script.addEventListener('load', onload);
const head = document.getElementsByTagName('head')[0];
head.insertBefore(script, head.lastChild);
}
function uriFromPath(_path) {
var pathName = path.resolve(_path).replace(/\\/g, '/');
if (pathName.length > 0 && pathName.charAt(0) !== '/') {
pathName = '/' + pathName;
}
return encodeURI('file://' + pathName);
}
function main() {
const args = parseURLQueryArgs();
const configuration = JSON.parse(args['config'] || '{}') || {};
// Correctly inherit the parent's environment
assign(process.env, configuration.userEnv);
// Get the nls configuration into the process.env as early as possible.
var nlsConfig = { availableLanguages: {} };
const config = process.env['VSCODE_NLS_CONFIG'];
if (config) {
process.env['VSCODE_NLS_CONFIG'] = config;
try {
nlsConfig = JSON.parse(config);
} catch (e) { /*noop*/ }
}
var locale = nlsConfig.availableLanguages['*'] || 'en';
if (locale === 'zh-tw') {
locale = 'zh-Hant';
} else if (locale === 'zh-cn') {
locale = 'zh-Hans';
}
window.document.documentElement.setAttribute('lang', locale);
// Load the loader and start loading the workbench
const rootUrl = uriFromPath(configuration.appRoot) + '/out';
// In the bundled version the nls plugin is packaged with the loader so the NLS Plugins
// loads as soon as the loader loads. To be able to have pseudo translation
createScript(rootUrl + '/vs/loader.js', function () {
define('fs', ['original-fs'], function (originalFS) { return originalFS; }); // replace the patched electron fs with the original node fs for all AMD code
window.MonacoEnvironment = {};
const nodeCachedDataErrors = window.MonacoEnvironment.nodeCachedDataErrors = [];
require.config({
baseUrl: rootUrl,
'vs/nls': nlsConfig,
nodeCachedDataDir: configuration.nodeCachedDataDir,
onNodeCachedDataError: function (err) { nodeCachedDataErrors.push(err) },
nodeModules: [/*BUILD->INSERT_NODE_MODULES*/]
});
if (nlsConfig.pseudo) {
require(['vs/nls'], function (nlsPlugin) {
nlsPlugin.setPseudoTranslation(nlsConfig.pseudo);
});
}
require(['vs/code/electron-browser/sharedProcessMain'], function () { });
});
}
main();

View File

@@ -12,7 +12,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -21,39 +21,23 @@ import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/ex
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { IRequestService } from 'vs/platform/request/node/request';
import { RequestService } from 'vs/platform/request/node/requestService';
import { RequestService } from 'vs/platform/request/electron-browser/requestService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc';
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import { ISharedProcessInitData } from './sharedProcess';
import { IChoiceService } from 'vs/platform/message/common/message';
import { ChoiceChannelClient } from 'vs/platform/message/common/messageIpc';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { WindowsChannelClient } from 'vs/platform/windows/common/windowsIpc';
import { ActiveWindowManager } from 'vs/code/common/windows';
import { ipcRenderer } from 'electron';
function quit(err?: Error) {
if (err) {
console.error(err.stack || err);
}
process.exit(err ? 1 : 0);
}
/**
* Plan B is to kill oneself if one's parent dies. Much drama.
*/
function setupPlanB(parentPid: number): void {
setInterval(function () {
try {
process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore.
} catch (e) {
process.exit();
}
}, 5000);
interface ISharedProcessInitData {
sharedIPCHandle: string;
args: ParsedArgs;
}
const eventPrefix = 'monacoworkbench';
@@ -159,16 +143,17 @@ function setupIPC(hook: string): TPromise<Server> {
return setup(true);
}
function handshake(): TPromise<ISharedProcessInitData> {
function startHandshake(): TPromise<ISharedProcessInitData> {
return new TPromise<ISharedProcessInitData>((c, e) => {
process.once('message', c);
process.once('error', e);
process.send('hello');
ipcRenderer.once('handshake:hey there', (_, r) => c(r));
ipcRenderer.send('handshake:hello');
});
}
setupIPC(process.env['VSCODE_SHARED_IPC_HOOK'])
.then(server => handshake()
.then(data => main(server, data))
.then(() => setupPlanB(process.env['VSCODE_PID']))
.done(null, quit));
function handshake(): TPromise<void> {
return startHandshake()
.then((data) => setupIPC(data.sharedIPCHandle).then(server => main(server, data)))
.then(() => ipcRenderer.send('handshake:im ready'));
}
handshake();

View File

@@ -27,9 +27,8 @@ import { Server, serve, connect } from 'vs/base/parts/ipc/node/ipc.net';
import { TPromise } from 'vs/base/common/winjs.base';
import { AskpassChannel } from 'vs/workbench/parts/git/common/gitIpc';
import { GitAskpassService } from 'vs/workbench/parts/git/electron-main/askpassService';
import { spawnSharedProcess } from 'vs/code/node/sharedProcess';
import { SharedProcess } from 'vs/code/electron-main/sharedProcess';
import { Mutex } from 'windows-mutex';
import { IDisposable } from 'vs/base/common/lifecycle';
import { LaunchService, ILaunchChannel, LaunchChannel, LaunchChannelClient, ILaunchService } from './launch';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
@@ -45,7 +44,7 @@ import { EnvironmentService } from 'vs/platform/environment/node/environmentServ
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { IRequestService } from 'vs/platform/request/node/request';
import { RequestService } from 'vs/platform/request/node/requestService';
import { RequestService } from 'vs/platform/request/electron-main/requestService';
import { IURLService } from 'vs/platform/url/common/url';
import { URLChannel } from 'vs/platform/url/common/urlIpc';
import { URLService } from 'vs/platform/url/electron-main/urlService';
@@ -143,18 +142,9 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
const electronIpcServer = new ElectronIPCServer();
// Spawn shared process
const initData = { args: environmentService.args };
const options = {
allowOutput: !environmentService.isBuilt || environmentService.verbose,
debugPort: environmentService.isBuilt ? null : 5871
};
let sharedProcessDisposable: IDisposable;
const sharedProcess = spawnSharedProcess(initData, options).then(disposable => {
sharedProcessDisposable = disposable;
return connect(environmentService.sharedIPCHandle, 'main');
});
const sharedProcess = new SharedProcess(environmentService, userEnv);
const sharedProcessClient = sharedProcess.onReady
.then(() => connect(environmentService.sharedIPCHandle, 'main'));
// Create a new service collection, because the telemetry service
// requires a connection to shared process, which was only established
@@ -163,11 +153,11 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
services.set(IUpdateService, new SyncDescriptor(UpdateService));
services.set(IWindowsMainService, new SyncDescriptor(WindowsManager));
services.set(IWindowsService, new SyncDescriptor(WindowsService));
services.set(IWindowsService, new SyncDescriptor(WindowsService, sharedProcess));
services.set(ILaunchService, new SyncDescriptor(LaunchService));
if (environmentService.isBuilt && !environmentService.isExtensionDevelopment && !!product.enableTelemetry) {
const channel = getDelayedChannel<ITelemetryAppenderChannel>(sharedProcess.then(c => c.getChannel('telemetryAppender')));
const channel = getDelayedChannel<ITelemetryAppenderChannel>(sharedProcessClient.then(c => c.getChannel('telemetryAppender')));
const appender = new TelemetryAppenderClient(channel);
const commonProperties = resolveCommonProperties(product.commit, pkg.version);
const piiPaths = [environmentService.appRoot, environmentService.extensionsPath];
@@ -183,6 +173,13 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
// TODO@Joao: unfold this
windowsMainService = accessor.get(IWindowsMainService);
// TODO@Joao: so ugly...
windowsMainService.onWindowClose(() => {
if (!platform.isMacintosh && windowsMainService.getWindowCount() === 0) {
sharedProcess.dispose();
}
});
// Register more Main IPC services
const launchService = accessor.get(ILaunchService);
const launchChannel = new LaunchChannel(launchService);
@@ -204,7 +201,7 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
const windowsService = accessor.get(IWindowsService);
const windowsChannel = new WindowsChannel(windowsService);
electronIpcServer.registerChannel('windows', windowsChannel);
sharedProcess.done(client => client.registerChannel('windows', windowsChannel));
sharedProcessClient.done(client => client.registerChannel('windows', windowsChannel));
// Make sure we associate the program with the app user model id
// This will help Windows to associate the running program with
@@ -220,15 +217,12 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo
mainIpcServer = null;
}
if (sharedProcessDisposable) {
sharedProcessDisposable.dispose();
}
if (windowsMutex) {
windowsMutex.release();
}
configurationService.dispose();
sharedProcess.dispose();
}
// Dispose on app quit
@@ -399,7 +393,6 @@ function start(): void {
const instanceEnv: typeof process.env = {
VSCODE_PID: String(process.pid),
VSCODE_IPC_HOOK: environmentService.mainIPCHandle,
VSCODE_SHARED_IPC_HOOK: environmentService.sharedIPCHandle,
VSCODE_NLS_CONFIG: process.env['VSCODE_NLS_CONFIG']
};

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assign } from 'vs/base/common/objects';
import { memoize } from 'vs/base/common/decorators';
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { TPromise } from 'vs/base/common/winjs.base';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { BrowserWindow, ipcMain } from 'electron';
export class SharedProcess {
private window: Electron.BrowserWindow;
private disposables: IDisposable[] = [];
@memoize
get onReady(): TPromise<void> {
this.window = new BrowserWindow({ show: false });
const config = assign({
appRoot: this.environmentService.appRoot,
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
userEnv: this.userEnv
});
const url = `${require.toUrl('vs/code/electron-browser/sharedProcess.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
this.window.loadURL(url);
// Prevent the window from dying
const onClose = e => {
if (this.window.isVisible()) {
e.preventDefault();
this.window.hide();
}
};
this.window.on('close', onClose);
this.disposables.push(toDisposable(() => this.window.removeListener('close', onClose)));
this.disposables.push(toDisposable(() => {
// Electron seems to crash on Windows without this setTimeout :|
setTimeout(() => {
try {
this.window.close();
} catch (err) {
// ignore, as electron is already shutting down
}
this.window = null;
}, 0);
}));
return new TPromise<void>((c, e) => {
ipcMain.once('handshake:hello', ({ sender }) => {
sender.send('handshake:hey there', {
sharedIPCHandle: this.environmentService.sharedIPCHandle,
args: this.environmentService.args
});
sender.once('handshake:im ready', () => c(null));
});
});
}
constructor(
private environmentService: IEnvironmentService,
private userEnv: IProcessEnvironment
) { }
toggle(): void {
if (this.window.isVisible()) {
this.hide();
} else {
this.show();
}
}
show(): void {
this.window.show();
this.window.webContents.openDevTools();
}
hide(): void {
this.window.webContents.closeDevTools();
this.window.hide();
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -1,77 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import URI from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
import { assign } from 'vs/base/common/objects';
import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { TPromise } from 'vs/base/common/winjs.base';
export interface ISharedProcessInitData {
args: ParsedArgs;
}
export interface ISharedProcessOptions {
allowOutput?: boolean;
debugPort?: number;
}
const boostrapPath = URI.parse(require.toUrl('bootstrap')).fsPath;
function _spawnSharedProcess(initData: ISharedProcessInitData, options: ISharedProcessOptions): cp.ChildProcess {
const execArgv: string[] = [];
const env = assign({}, process.env, {
AMD_ENTRYPOINT: 'vs/code/node/sharedProcessMain',
ELECTRON_NO_ASAR: '1'
});
if (options.allowOutput) {
env['VSCODE_ALLOW_IO'] = 'true';
}
if (options.debugPort) {
execArgv.push(`--debug=${options.debugPort}`);
}
const result = cp.fork(boostrapPath, ['--type=SharedProcess'], { env, execArgv });
return result;
}
export function spawnSharedProcess(initData: ISharedProcessInitData, options: ISharedProcessOptions = {}): TPromise<IDisposable> {
let spawnCount = 0;
let child: cp.ChildProcess;
let promise: TPromise<IDisposable>;
const spawn = () => {
if (++spawnCount > 10) {
return;
}
child = _spawnSharedProcess(initData, options);
promise = new TPromise<IDisposable>((c, e) => {
// handshake
child.once('message', () => {
child.send(initData);
c({
dispose: () => {
if (child) {
child.removeListener('exit', spawn);
child.kill();
child = null;
}
}
});
});
});
child.on('exit', spawn);
};
spawn();
return promise;
}