Share console wrapping code

This commit is contained in:
Alex Dima 2022-06-23 21:47:39 +02:00
parent 5e5e4c86ba
commit 938124008c
No known key found for this signature in database
GPG key ID: 39563C1504FDD0C9
6 changed files with 89 additions and 131 deletions

View file

@ -9,7 +9,6 @@ import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks';
import { FileAccess } from 'vs/base/common/network';
import { join, delimiter } from 'vs/base/common/path';
import { VSBuffer } from 'vs/base/common/buffer';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { Emitter, Event } from 'vs/base/common/event';
import { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv';
@ -18,7 +17,6 @@ import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remot
import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
import { IProcessEnvironment, isWindows } from 'vs/base/common/platform';
import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil';
import { removeDangerousEnvVariables } from 'vs/base/common/processes';
import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
@ -271,14 +269,6 @@ export class ExtensionHostConnection {
onStdout((e) => this._log(`<${pid}> ${e}`));
onStderr((e) => this._log(`<${pid}><stderr> ${e}`));
// Support logging from extension host
this._extensionHostProcess.on('message', msg => {
if (msg && (<IRemoteConsoleLog>msg).type === '__$console') {
logRemoteEntry(this._logService, (<IRemoteConsoleLog>msg), `${this._logPrefix}<${pid}>`);
}
});
// Lifecycle
this._extensionHostProcess.on('error', (err) => {
this._logError(`<${pid}> Extension Host Process had an error`);

View file

@ -9,14 +9,59 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
export abstract class AbstractExtHostConsoleForwarder {
protected readonly _mainThreadConsole: MainThreadConsoleShape;
private readonly _mainThreadConsole: MainThreadConsoleShape;
private readonly _includeStack: boolean;
private readonly _logNative: boolean;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService,
) {
this._mainThreadConsole = extHostRpc.getProxy(MainContext.MainThreadConsole);
this._includeStack = initData.consoleForward.includeStack;
this._logNative = initData.consoleForward.logNative;
// Pass console logging to the outside so that we have it in the main side if told so
this._wrapConsoleMethod('info', 'log');
this._wrapConsoleMethod('log', 'log');
this._wrapConsoleMethod('warn', 'warn');
this._wrapConsoleMethod('error', 'error');
}
/**
* Wraps a console message so that it is transmitted to the renderer. If
* native logging is turned on, the original console message will be written
* as well. This is needed since the console methods are "magic" in V8 and
* are the only methods that allow later introspection of logged variables.
*
* The wrapped property is not defined with `writable: false` to avoid
* throwing errors, but rather a no-op setting. See https://github.com/microsoft/vscode-extension-telemetry/issues/88
*/
private _wrapConsoleMethod(method: 'log' | 'info' | 'warn' | 'error', severity: 'log' | 'warn' | 'error') {
const that = this;
const original = console[method];
Object.defineProperty(console, method, {
set: () => { },
get: () => function () {
that._handleConsoleCall(method, severity, original, arguments);
},
});
}
private _handleConsoleCall(method: 'log' | 'info' | 'warn' | 'error', severity: 'log' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments): void {
this._mainThreadConsole.$logExtensionHostMessage({
type: '__$console',
severity,
arguments: safeStringifyArgumentsToArray(args, this._includeStack)
});
if (this._logNative) {
this._nativeConsoleLogMessage(method, original, args);
}
}
protected abstract _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments): void;
}
const MAX_LENGTH = 100000;
@ -24,7 +69,7 @@ const MAX_LENGTH = 100000;
/**
* Prevent circular stringify and convert arguments to real array
*/
export function safeStringifyArgumentsToArray(args: IArguments, includeStack: boolean): string {
function safeStringifyArgumentsToArray(args: IArguments, includeStack: boolean): string {
const seen: any[] = [];
const argsArray = [];

View file

@ -3,84 +3,34 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractExtHostConsoleForwarder, safeStringifyArgumentsToArray } from 'vs/workbench/api/common/extHostConsoleForwarder';
import { AbstractExtHostConsoleForwarder } from 'vs/workbench/api/common/extHostConsoleForwarder';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { NativeLogMarkers } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024;
export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder {
private _isMakingConsoleCall: boolean = false;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService,
) {
super(extHostRpc, initData);
pipeLoggingToParent(initData.consoleForward.includeStack, initData.consoleForward.logNative);
// Use IPC messages to forward console-calls, note that the console is
// already patched to use`process.send()`
const nativeProcessSend = process.send!;
// const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole);
process.send = (...args) => {
if ((args as unknown[]).length === 0 || !args[0] || args[0].type !== '__$console') {
return nativeProcessSend.apply(process, args);
}
this._mainThreadConsole.$logExtensionHostMessage(args[0]);
return false;
};
}
}
// TODO@Alex: remove duplication
function pipeLoggingToParent(includeStack: boolean, logNative: boolean) {
const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024;
function safeSend(arg: { type: string; severity: string; arguments: string }) {
try {
if (process.send) {
process.send(arg);
}
} catch (error) {
// Can happen if the parent channel is closed meanwhile
}
this._wrapStream('stderr', 'error');
this._wrapStream('stdout', 'log');
}
function safeSendConsoleMessage(severity: 'log' | 'warn' | 'error', args: string) {
safeSend({ type: '__$console', severity, arguments: args });
}
let isMakingConsoleCall = false;
/**
* Wraps a console message so that it is transmitted to the renderer. If
* native logging is turned on, the original console message will be written
* as well. This is needed since the console methods are "magic" in V8 and
* are the only methods that allow later introspection of logged variables.
*
* The wrapped property is not defined with `writable: false` to avoid
* throwing errors, but rather a no-op setting. See https://github.com/microsoft/vscode-extension-telemetry/issues/88
*/
function wrapConsoleMethod(method: 'log' | 'info' | 'warn' | 'error', severity: 'log' | 'warn' | 'error') {
if (logNative) {
const original = console[method];
const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout;
Object.defineProperty(console, method, {
set: () => { },
get: () => function () {
safeSendConsoleMessage(severity, safeStringifyArgumentsToArray(arguments, includeStack));
isMakingConsoleCall = true;
stream.write(`\n${NativeLogMarkers.Start}\n`);
original.apply(console, arguments as any);
stream.write(`\n${NativeLogMarkers.End}\n`);
isMakingConsoleCall = false;
},
});
} else {
Object.defineProperty(console, method, {
set: () => { },
get: () => function () { safeSendConsoleMessage(severity, safeStringifyArgumentsToArray(arguments, includeStack)); },
});
}
protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments) {
const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout;
this._isMakingConsoleCall = true;
stream.write(`\n${NativeLogMarkers.Start}\n`);
original.apply(console, args as any);
stream.write(`\n${NativeLogMarkers.End}\n`);
this._isMakingConsoleCall = false;
}
/**
@ -89,7 +39,7 @@ function pipeLoggingToParent(includeStack: boolean, logNative: boolean) {
* as to console.log with complete lines so that they're made available
* to the debugger/CLI.
*/
function wrapStream(streamName: 'stdout' | 'stderr', severity: 'log' | 'warn' | 'error') {
private _wrapStream(streamName: 'stdout' | 'stderr', severity: 'log' | 'warn' | 'error') {
const stream = process[streamName];
const original = stream.write;
@ -98,7 +48,7 @@ function pipeLoggingToParent(includeStack: boolean, logNative: boolean) {
Object.defineProperty(stream, 'write', {
set: () => { },
get: () => (chunk: Uint8Array | string, encoding?: BufferEncoding, callback?: (err?: Error) => void) => {
if (!isMakingConsoleCall) {
if (!this._isMakingConsoleCall) {
buf += (chunk as any).toString(encoding);
const eol = buf.length > MAX_STREAM_BUFFER_LENGTH ? buf.length : buf.lastIndexOf('\n');
if (eol !== -1) {
@ -111,13 +61,4 @@ function pipeLoggingToParent(includeStack: boolean, logNative: boolean) {
},
});
}
// Pass console logging to the outside so that we have it in the main side if told so
wrapConsoleMethod('info', 'log');
wrapConsoleMethod('log', 'log');
wrapConsoleMethod('warn', 'warn');
wrapConsoleMethod('error', 'error');
wrapStream('stderr', 'error');
wrapStream('stdout', 'log');
}

View file

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractExtHostConsoleForwarder } from 'vs/workbench/api/common/extHostConsoleForwarder';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
export class ExtHostConsoleForwarder extends AbstractExtHostConsoleForwarder {
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService,
) {
super(extHostRpc, initData);
}
protected override _nativeConsoleLogMessage(method: 'log' | 'info' | 'warn' | 'error', original: (...args: any[]) => void, args: IArguments) {
original.apply(console, args as any);
}
}

View file

@ -11,8 +11,7 @@ import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterc
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
import { timeout } from 'vs/base/common/async';
import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol';
import { safeStringifyArgumentsToArray } from 'vs/workbench/api/common/extHostConsoleForwarder';
import { ExtHostConsoleForwarder } from 'vs/workbench/api/worker/extHostConsoleForwarder';
class WorkerRequireInterceptor extends RequireInterceptor {
@ -40,8 +39,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
private _fakeModules?: WorkerRequireInterceptor;
protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {
const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole);
wrapConsoleMethods(mainThreadConsole, this._initData.consoleForward.includeStack, this._initData.environment.isExtensionDevelopmentDebug);
// make sure console.log calls make it to the render
this._instaService.createInstance(ExtHostConsoleForwarder);
// initialize API and register actors
const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);
@ -141,22 +140,3 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
function ensureSuffix(path: string, suffix: string): string {
return path.endsWith(suffix) ? path : path + suffix;
}
// TODO@Alex: remove duplication
// copied from bootstrap-fork.js
function wrapConsoleMethods(service: MainThreadConsoleShape, includeStack: boolean, logNative: boolean) {
wrapConsoleMethod('info', 'log');
wrapConsoleMethod('log', 'log');
wrapConsoleMethod('warn', 'warn');
wrapConsoleMethod('error', 'error');
function wrapConsoleMethod(method: 'log' | 'info' | 'warn' | 'error', severity: 'log' | 'warn' | 'error') {
const original = console[method];
console[method] = function () {
service.$logExtensionHostMessage({ type: '__$console', severity, arguments: safeStringifyArgumentsToArray(arguments, includeStack) });
if (logNative) {
original.apply(console, arguments as any);
}
};
}
}

View file

@ -11,8 +11,6 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import * as objects from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { IRemoteConsoleLog, log } from 'vs/base/common/console';
import { logRemoteEntry, logRemoteEntryIfError } from 'vs/workbench/services/extensions/common/remoteConsoleUtil';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
@ -309,13 +307,6 @@ export class SandboxLocalProcessExtensionHost implements IExtensionHost {
}
});
// Support logging from extension host
this._extensionHostProcess.onMessage(msg => {
if (msg && (<IRemoteConsoleLog>msg).type === '__$console') {
this._logExtensionHostMessage(<IRemoteConsoleLog>msg);
}
});
// Lifecycle
this._extensionHostProcess.onError((e) => this._onExtHostProcessError(e.error));
@ -491,17 +482,6 @@ export class SandboxLocalProcessExtensionHost implements IExtensionHost {
};
}
private _logExtensionHostMessage(entry: IRemoteConsoleLog) {
if (this._isExtensionDevTestFromCli) {
// If running tests from cli, log to the log service everything
logRemoteEntry(this._logService, entry);
} else {
// Log to the log service only errors and log everything to local console
logRemoteEntryIfError(this._logService, entry, 'Extension Host');
log(entry, 'Extension Host');
}
}
private _onExtHostProcessError(_err: SerializedError): void {
let err: any = _err;
if (_err && _err.$isError) {