Skip to content

Commit 8a6b8fd

Browse files
committed
Clean up thred service
1 parent cf6b4b5 commit 8a6b8fd

1 file changed

Lines changed: 146 additions & 138 deletions

File tree

src/vs/workbench/services/thread/electron-browser/threadService.ts

Lines changed: 146 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@ class ExtensionHostProcessManager {
132132
PIPE_LOGGING: 'true',
133133
VERBOSE_LOGGING: true,
134134
VSCODE_WINDOW_ID: String(this.windowService.getWindowId())
135-
})
135+
}),
136+
// We only detach the extension host on windows. Linux and Mac orphan by default
137+
// and detach under Linux and Mac create another process group.
138+
// We detach because we have noticed that when the renderer exits, its child processes
139+
// (i.e. extension host) is taken down in a brutal fashion by the OS
140+
detached: !!isWindows,
141+
onExtensionHostMessage
136142
};
137143

138144
// Help in case we fail to start it
@@ -145,20 +151,16 @@ class ExtensionHostProcessManager {
145151
}
146152

147153
// Initialize extension host process with hand shakes
148-
this.initializeExtensionHostProcess = new TPromise<ChildProcess>((c, e) => {
154+
this.initializeExtensionHostProcess = this.doInitializeExtensionHostProcess(opts);
155+
}
149156

157+
private doInitializeExtensionHostProcess(opts: any): TPromise<ChildProcess> {
158+
return new TPromise<ChildProcess>((c, e) => {
150159
// Resolve additional execution args (e.g. debug)
151-
return this.resolveDebugPort(this.environmentService.debugExtensionHost.port, port => {
160+
this.resolveDebugPort(this.environmentService.debugExtensionHost.port).then(port => {
152161
if (port) {
153162
opts.execArgv = ['--nolazy', (this.isExtensionDevelopmentDebugging ? '--debug-brk=' : '--debug=') + port];
154163
}
155-
// We only detach the extension host on windows. Linux and Mac orphan by default
156-
// and detach under Linux and Mac create another process group.
157-
if (isWindows) {
158-
// We detach because we have noticed that when the renderer exits, its child processes
159-
// (i.e. extension host) is taken down in a brutal fashion by the OS
160-
opts.detached = true;
161-
}
162164

163165
// Run Extension Host as fork of current process
164166
this.extensionHostProcessHandle = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
@@ -172,159 +174,165 @@ class ExtensionHostProcessManager {
172174
}
173175

174176
// Messages from Extension host
175-
this.extensionHostProcessHandle.on('message', (msg) => {
176-
177-
// 1) Host is ready to receive messages, initialize it
178-
if (msg === 'ready') {
179-
if (this.initializeTimer) {
180-
window.clearTimeout(this.initializeTimer);
181-
}
182-
183-
let initPayload = stringify({
184-
parentPid: process.pid,
185-
environment: {
186-
appSettingsHome: this.environmentService.appSettingsHome,
187-
disableExtensions: this.environmentService.disableExtensions,
188-
userExtensionsHome: this.environmentService.extensionsPath,
189-
extensionDevelopmentPath: this.environmentService.extensionDevelopmentPath,
190-
extensionTestsPath: this.environmentService.extensionTestsPath
191-
},
192-
contextService: {
193-
workspace: this.contextService.getWorkspace()
194-
}
195-
});
196-
197-
this.extensionHostProcessHandle.send(initPayload);
198-
}
199-
200-
// 2) Host is initialized
201-
else if (msg === 'initialized') {
202-
this.unsentMessages.forEach(m => this.postMessage(m));
203-
this.unsentMessages = [];
204-
205-
this.extensionHostProcessReady = true;
177+
this.extensionHostProcessHandle.on('message', msg => {
178+
if (this.onMessaage(msg, opts.onExtensionHostMessage)) {
206179
c(this.extensionHostProcessHandle);
207180
}
208-
209-
// Support logging from extension host
210-
else if (msg && (<ILogEntry>msg).type === '__$console') {
211-
let logEntry: ILogEntry = msg;
212-
213-
let args = [];
214-
try {
215-
let parsed = JSON.parse(logEntry.arguments);
216-
args.push(...Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
217-
} catch (error) {
218-
args.push(logEntry.arguments);
219-
}
220-
221-
// If the first argument is a string, check for % which indicates that the message
222-
// uses substitution for variables. In this case, we cannot just inject our colored
223-
// [Extension Host] to the front because it breaks substitution.
224-
let consoleArgs = [];
225-
if (typeof args[0] === 'string' && args[0].indexOf('%') >= 0) {
226-
consoleArgs = [`%c[Extension Host]%c ${args[0]}`, 'color: blue', 'color: black', ...args.slice(1)];
227-
} else {
228-
consoleArgs = ['%c[Extension Host]', 'color: blue', ...args];
229-
}
230-
231-
// Send to local console unless we run tests from cli
232-
if (!this.isExtensionDevelopmentTestFromCli) {
233-
console[logEntry.severity].apply(console, consoleArgs);
234-
}
235-
236-
// Log on main side if running tests from cli
237-
if (this.isExtensionDevelopmentTestFromCli) {
238-
ipc.send('vscode:log', logEntry);
239-
}
240-
241-
// Broadcast to other windows if we are in development mode
242-
else if (!this.environmentService.isBuilt || this.isExtensionDevelopmentHost) {
243-
this.windowService.broadcast({
244-
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
245-
payload: logEntry
246-
}, this.environmentService.extensionDevelopmentPath /* target */);
247-
}
248-
}
249-
250-
// Any other message goes to the callback
251-
else {
252-
onExtensionHostMessage(msg);
253-
}
254181
});
255182

256183
// Lifecycle
257184
let onExit = () => this.terminate();
258185
process.once('exit', onExit);
259-
260-
this.extensionHostProcessHandle.on('error', (err) => {
261-
let errorMessage = toErrorMessage(err);
262-
if (errorMessage === this.lastExtensionHostError) {
263-
return; // prevent error spam
264-
}
265-
266-
this.lastExtensionHostError = errorMessage;
267-
268-
this.messageService.show(Severity.Error, nls.localize('extensionHostProcess.error', "Error from the extension host: {0}", errorMessage));
269-
});
270-
271-
this.extensionHostProcessHandle.on('exit', (code: any, signal: any) => {
272-
process.removeListener('exit', onExit);
273-
274-
if (!this.terminating) {
275-
276-
// Unexpected termination
277-
if (!this.isExtensionDevelopmentHost) {
278-
this.messageService.show(Severity.Error, {
279-
message: nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly. Please reload the window to recover."),
280-
actions: [this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)]
281-
});
282-
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
283-
}
284-
285-
// Expected development extension termination: When the extension host goes down we also shutdown the window
286-
else if (!this.isExtensionDevelopmentTestFromCli) {
287-
this.windowService.getWindow().close();
288-
}
289-
290-
// When CLI testing make sure to exit with proper exit code
291-
else {
292-
ipc.send('vscode:exit', code);
293-
}
294-
}
295-
});
186+
this.extensionHostProcessHandle.on('error', (err) => this.onError(err));
187+
this.extensionHostProcessHandle.on('exit', (code: any, signal: any) => this.onExit(code, signal, onExit));
296188
});
297189
}, () => this.terminate());
298190
}
299191

300-
private resolveDebugPort(extensionHostPort: number, clb: (port: number) => void): void {
301-
302-
// Check for a free debugging port
303-
if (typeof extensionHostPort === 'number') {
304-
return findFreePort(extensionHostPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, (port) => {
192+
private resolveDebugPort(extensionHostPort: number): TPromise<number> {
193+
if (typeof extensionHostPort !== 'number') {
194+
return TPromise.wrap(void 0);
195+
}
196+
return new TPromise<number>((c, e) => {
197+
findFreePort(extensionHostPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, (port) => {
305198
if (!port) {
306199
console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color: black');
307-
308-
return clb(void 0);
200+
c(void 0);
309201
}
310-
311202
if (port !== extensionHostPort) {
312203
console.warn('%c[Extension Host] %cProvided debugging port ' + extensionHostPort + ' is not free, using ' + port + ' instead.', 'color: blue', 'color: black');
313204
}
314-
315205
if (this.isExtensionDevelopmentDebugging) {
316206
console.warn('%c[Extension Host] %cSTOPPED on first line for debugging on port ' + port, 'color: blue', 'color: black');
317207
} else {
318208
console.info('%c[Extension Host] %cdebugger listening on port ' + port, 'color: blue', 'color: black');
319209
}
320-
321-
return clb(port);
210+
return c(port);
322211
});
212+
});
213+
}
214+
215+
// @return `true` if ready
216+
private onMessaage(msg : any, onExtensionHostMessage : (msg: any) => void): boolean {
217+
// 1) Host is ready to receive messages, initialize it
218+
if (msg === 'ready') {
219+
this.initializeExtensionHost();
220+
return false;
221+
}
222+
223+
// 2) Host is initialized
224+
if (msg === 'initialized') {
225+
this.unsentMessages.forEach(m => this.postMessage(m));
226+
this.unsentMessages = [];
227+
this.extensionHostProcessReady = true;
228+
return true;
229+
}
230+
231+
// Support logging from extension host
232+
if (msg && (<ILogEntry>msg).type === '__$console') {
233+
this.logExtensionHostMessage(<ILogEntry>msg);
234+
return false;
235+
}
236+
237+
// Any other message goes to the callback
238+
onExtensionHostMessage(msg);
239+
return false;
240+
}
241+
242+
private initializeExtensionHost() {
243+
if (this.initializeTimer) {
244+
window.clearTimeout(this.initializeTimer);
323245
}
324246

325-
// Nothing to do here
326-
else {
327-
return clb(void 0);
247+
let initPayload = stringify({
248+
parentPid: process.pid,
249+
environment: {
250+
appSettingsHome: this.environmentService.appSettingsHome,
251+
disableExtensions: this.environmentService.disableExtensions,
252+
userExtensionsHome: this.environmentService.extensionsPath,
253+
extensionDevelopmentPath: this.environmentService.extensionDevelopmentPath,
254+
extensionTestsPath: this.environmentService.extensionTestsPath
255+
},
256+
contextService: {
257+
workspace: this.contextService.getWorkspace()
258+
}
259+
});
260+
261+
this.extensionHostProcessHandle.send(initPayload);
262+
}
263+
264+
private logExtensionHostMessage(logEntry: ILogEntry) {
265+
let args = [];
266+
try {
267+
let parsed = JSON.parse(logEntry.arguments);
268+
args.push(...Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
269+
} catch (error) {
270+
args.push(logEntry.arguments);
271+
}
272+
273+
// If the first argument is a string, check for % which indicates that the message
274+
// uses substitution for variables. In this case, we cannot just inject our colored
275+
// [Extension Host] to the front because it breaks substitution.
276+
let consoleArgs = [];
277+
if (typeof args[0] === 'string' && args[0].indexOf('%') >= 0) {
278+
consoleArgs = [`%c[Extension Host]%c ${args[0]}`, 'color: blue', 'color: black', ...args.slice(1)];
279+
} else {
280+
consoleArgs = ['%c[Extension Host]', 'color: blue', ...args];
281+
}
282+
283+
// Send to local console unless we run tests from cli
284+
if (!this.isExtensionDevelopmentTestFromCli) {
285+
console[logEntry.severity].apply(console, consoleArgs);
286+
}
287+
288+
// Log on main side if running tests from cli
289+
if (this.isExtensionDevelopmentTestFromCli) {
290+
ipc.send('vscode:log', logEntry);
291+
}
292+
293+
// Broadcast to other windows if we are in development mode
294+
else if (!this.environmentService.isBuilt || this.isExtensionDevelopmentHost) {
295+
this.windowService.broadcast({
296+
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
297+
payload: logEntry
298+
}, this.environmentService.extensionDevelopmentPath /* target */);
299+
}
300+
}
301+
302+
private onError(err: any): void {
303+
let errorMessage = toErrorMessage(err);
304+
if (errorMessage === this.lastExtensionHostError) {
305+
return; // prevent error spam
306+
}
307+
308+
this.lastExtensionHostError = errorMessage;
309+
310+
this.messageService.show(Severity.Error, nls.localize('extensionHostProcess.error', "Error from the extension host: {0}", errorMessage));
311+
}
312+
313+
private onExit(code: any, signal: any, onProcessExit: any): void {
314+
process.removeListener('exit', onProcessExit);
315+
316+
if (!this.terminating) {
317+
318+
// Unexpected termination
319+
if (!this.isExtensionDevelopmentHost) {
320+
this.messageService.show(Severity.Error, {
321+
message: nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly. Please reload the window to recover."),
322+
actions: [this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL)]
323+
});
324+
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
325+
}
326+
327+
// Expected development extension termination: When the extension host goes down we also shutdown the window
328+
else if (!this.isExtensionDevelopmentTestFromCli) {
329+
this.windowService.getWindow().close();
330+
}
331+
332+
// When CLI testing make sure to exit with proper exit code
333+
else {
334+
ipc.send('vscode:exit', code);
335+
}
328336
}
329337
}
330338

0 commit comments

Comments
 (0)