Skip to content

Commit 6dc3d6c

Browse files
committed
return _wasWrapped symbol but it only for wrapped functions, not the "original" handler, add unit tests
1 parent c7d9530 commit 6dc3d6c

File tree

2 files changed

+223
-3
lines changed

2 files changed

+223
-3
lines changed

src/vs/workbench/api/common/extensionHostMain.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface IConsolePatchFn {
3232
(mainThreadConsole: MainThreadConsoleShape): any;
3333
}
3434

35-
abstract class ErrorHandler {
35+
export abstract class ErrorHandler {
3636

3737
static {
3838
// increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
@@ -89,6 +89,7 @@ abstract class ErrorHandler {
8989
return result;
9090
}
9191

92+
const _wasWrapped = Symbol('prepareStackTrace wrapped');
9293
let _prepareStackTrace = prepareStackTraceAndFindExtension;
9394

9495
Object.defineProperty(Error, 'prepareStackTrace', {
@@ -97,8 +98,7 @@ abstract class ErrorHandler {
9798
return _prepareStackTrace;
9899
},
99100
set(v) {
100-
if (v === prepareStackTraceAndFindExtension) {
101-
// back to default
101+
if (v === prepareStackTraceAndFindExtension || v[_wasWrapped]) {
102102
_prepareStackTrace = v;
103103
return;
104104
}
@@ -107,6 +107,8 @@ abstract class ErrorHandler {
107107
prepareStackTraceAndFindExtension(error, stackTrace);
108108
return v.call(Error, error, stackTrace);
109109
};
110+
111+
Object.assign(_prepareStackTrace, { [_wasWrapped]: true });
110112
},
111113
});
112114

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import { SerializedError, errorHandler, onUnexpectedError } from 'vs/base/common/errors';
8+
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
9+
import { URI } from 'vs/base/common/uri';
10+
import { mock } from 'vs/base/test/common/mock';
11+
import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
12+
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
13+
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
14+
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
15+
import { MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol';
16+
import { ExtensionPaths, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
17+
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
18+
import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry';
19+
import { ErrorHandler } from 'vs/workbench/api/common/extensionHostMain';
20+
import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
21+
import { ProxyIdentifier, Proxied } from 'vs/workbench/services/extensions/common/proxyIdentifier';
22+
23+
24+
suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 ', function () {
25+
26+
const extensionsIndex = TernarySearchTree.forUris<IExtensionDescription>();
27+
const mainThreadExtensionsService = new class extends mock<MainThreadExtensionServiceShape>() {
28+
override $onExtensionRuntimeError(extensionId: ExtensionIdentifier, data: SerializedError): void {
29+
30+
}
31+
};
32+
33+
const collection = new ServiceCollection(
34+
[ILogService, new NullLogService()],
35+
[IExtHostTelemetry, new class extends mock<IExtHostTelemetry>() {
36+
declare readonly _serviceBrand: undefined;
37+
override onExtensionError(extension: ExtensionIdentifier, error: Error): boolean {
38+
return true;
39+
}
40+
}],
41+
[IExtHostExtensionService, new class extends mock<IExtHostExtensionService & any>() {
42+
declare readonly _serviceBrand: undefined;
43+
getExtensionPathIndex() {
44+
return new class extends ExtensionPaths {
45+
override findSubstr(key: URI): Readonly<IRelaxedExtensionDescription> | undefined {
46+
findSubstrCount++;
47+
return nullExtensionDescription;
48+
}
49+
50+
}(extensionsIndex);
51+
}
52+
}],
53+
[IExtHostRpcService, new class extends mock<IExtHostRpcService>() {
54+
declare readonly _serviceBrand: undefined;
55+
override getProxy<T>(identifier: ProxyIdentifier<T>): Proxied<T> {
56+
return <any>mainThreadExtensionsService;
57+
}
58+
}]
59+
);
60+
61+
const insta = new InstantiationService(collection, false);
62+
63+
let existingErrorHandler: (e: any) => void;
64+
let findSubstrCount = 0;
65+
66+
suiteSetup(async function () {
67+
existingErrorHandler = errorHandler.getUnexpectedErrorHandler();
68+
await insta.invokeFunction(ErrorHandler.installFullHandler);
69+
});
70+
71+
suiteTeardown(function () {
72+
errorHandler.setUnexpectedErrorHandler(existingErrorHandler);
73+
});
74+
75+
setup(async function () {
76+
findSubstrCount = 0;
77+
});
78+
79+
test('basics', function () {
80+
81+
const err = new Error('test1');
82+
83+
onUnexpectedError(err);
84+
85+
assert.strictEqual(findSubstrCount, 1);
86+
87+
});
88+
89+
test('set/reset prepareStackTrace-callback', function () {
90+
91+
const original = Error.prepareStackTrace;
92+
Error.prepareStackTrace = (_error, _stack) => 'stack';
93+
const probeErr = new Error();
94+
const stack = probeErr.stack;
95+
assert.ok(stack);
96+
Error.prepareStackTrace = original;
97+
assert.strictEqual(findSubstrCount, 1);
98+
99+
// already checked
100+
onUnexpectedError(probeErr);
101+
assert.strictEqual(findSubstrCount, 1);
102+
103+
// one more error
104+
const err = new Error('test2');
105+
onUnexpectedError(err);
106+
107+
assert.strictEqual(findSubstrCount, 2);
108+
});
109+
110+
test('wrap prepareStackTrace-callback', function () {
111+
112+
function do_something_else(params: string) {
113+
return params;
114+
}
115+
116+
const original = Error.prepareStackTrace;
117+
Error.prepareStackTrace = (...args) => {
118+
return do_something_else(original?.(...args));
119+
};
120+
const probeErr = new Error();
121+
const stack = probeErr.stack;
122+
assert.ok(stack);
123+
124+
125+
onUnexpectedError(probeErr);
126+
assert.strictEqual(findSubstrCount, 1);
127+
});
128+
129+
test('prevent rewrapping', function () {
130+
131+
let do_something_count = 0;
132+
function do_something(params: any) {
133+
do_something_count++;
134+
}
135+
136+
Error.prepareStackTrace = (result, stack) => {
137+
do_something(stack);
138+
return 'fakestack';
139+
};
140+
141+
for (let i = 0; i < 2_500; ++i) {
142+
Error.prepareStackTrace = Error.prepareStackTrace;
143+
}
144+
145+
const probeErr = new Error();
146+
const stack = probeErr.stack;
147+
assert.strictEqual(stack, 'fakestack');
148+
149+
onUnexpectedError(probeErr);
150+
assert.strictEqual(findSubstrCount, 1);
151+
152+
const probeErr2 = new Error();
153+
onUnexpectedError(probeErr2);
154+
assert.strictEqual(findSubstrCount, 2);
155+
assert.strictEqual(do_something_count, 2);
156+
});
157+
});
158+
159+
suite('ExtensionHostMain#ErrorHandler - from https://gist.github.com/thecrypticace/f0f2e182082072efdaf0f8e1537d2cce', function () {
160+
161+
test("Restored, separate operations", () => {
162+
// Actual Test
163+
let original;
164+
165+
// Operation 1
166+
original = Error.prepareStackTrace;
167+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
168+
assert.ok(new Error().stack);
169+
Error.prepareStackTrace = original;
170+
171+
// Operation 2
172+
original = Error.prepareStackTrace;
173+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
174+
assert.ok(new Error().stack);
175+
Error.prepareStackTrace = original;
176+
177+
// Operation 3
178+
original = Error.prepareStackTrace;
179+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
180+
assert.ok(new Error().stack);
181+
Error.prepareStackTrace = original;
182+
183+
// Operation 4
184+
original = Error.prepareStackTrace;
185+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
186+
assert.ok(new Error().stack);
187+
Error.prepareStackTrace = original;
188+
});
189+
190+
test("Never restored, separate operations", () => {
191+
// Operation 1
192+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
193+
assert.ok(new Error().stack);
194+
195+
// Operation 2
196+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
197+
assert.ok(new Error().stack);
198+
199+
// Operation 3
200+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
201+
assert.ok(new Error().stack);
202+
203+
// Operation 4
204+
for (let i = 0; i < 12_500; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
205+
assert.ok(new Error().stack);
206+
});
207+
208+
test("Restored, too many uses before restoration", async () => {
209+
const original = Error.prepareStackTrace;
210+
Error.prepareStackTrace = (_, stack) => stack;
211+
212+
// Operation 1 — more uses of `prepareStackTrace`
213+
for (let i = 0; i < 10_000; ++i) { Error.prepareStackTrace = Error.prepareStackTrace; }
214+
assert.ok(new Error().stack);
215+
216+
Error.prepareStackTrace = original;
217+
});
218+
});

0 commit comments

Comments
 (0)