From 7d16dde82d7c9729a0f9802ec68f9e34c55ad8db Mon Sep 17 00:00:00 2001 From: leoca Date: Fri, 11 Sep 2026 01:22:24 +0200 Subject: [PATCH 1/2] Fix forked process console forwarding reporting shared references as circular The JSON.stringify replacer in bootstrap-fork's safeToString pushed every object into a `seen` array and never removed anything, so a value that is referenced twice in the console arguments of a forked process (a DAG, not a cycle) was forwarded to the parent as "[Circular]" after its first occurrence. For example console.log(shared, { a: shared, b: shared }) arrived as [{"value":1},{"a":"[Circular]","b":"[Circular]"}]. Track only the current ancestor path instead, using the replacer's `this` (the holder) to drop subtrees that are finished. True cycles are still reported as "[Circular]". The isObject helper is gone: every object has to be tracked for the holder check to work, and RegExp/Date were the only exclusions (a Date is already a string by the time the replacer sees it). Added an integration test that forks bootstrap-fork with a fixture that logs a shared reference and a genuine cycle. --- src/bootstrap-fork.ts | 27 +++++++++--------- .../node/processes/fixtures/fork_console.ts | 11 ++++++++ .../processes/processes.integrationTest.ts | 28 +++++++++++++++++++ 3 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 src/vs/base/test/node/processes/fixtures/fork_console.ts diff --git a/src/bootstrap-fork.ts b/src/bootstrap-fork.ts index b87e855ba85fe5..55bc99f2649d71 100644 --- a/src/bootstrap-fork.ts +++ b/src/bootstrap-fork.ts @@ -19,7 +19,7 @@ function pipeLoggingToParent(): void { * Prevent circular stringify and convert arguments to real array */ function safeToString(args: ArrayLike): string { - const seen: unknown[] = []; + const ancestors: unknown[] = []; const argsArray: unknown[] = []; // Massage some arguments with special treatment @@ -50,15 +50,22 @@ function pipeLoggingToParent(): void { } try { - const res = JSON.stringify(argsArray, function (key, value: unknown) { + const res = JSON.stringify(argsArray, function (this: unknown, key, value: unknown) { + + // Objects get special treatment to prevent circles. Only the current + // ancestor path is tracked, so a value that is shared across arguments + // or sibling properties is serialized in full rather than as circular. + if (typeof value === 'object' && value !== null) { + // `this` is the object holding `key`, pop the subtrees that are already done + while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) { + ancestors.pop(); + } - // Objects get special treatment to prevent circles - if (isObject(value) || Array.isArray(value)) { - if (seen.indexOf(value) !== -1) { + if (ancestors.indexOf(value) !== -1) { return '[Circular]'; } - seen.push(value); + ancestors.push(value); } return value; @@ -84,14 +91,6 @@ function pipeLoggingToParent(): void { } } - function isObject(obj: unknown): boolean { - return typeof obj === 'object' - && obj !== null - && !Array.isArray(obj) - && !(obj instanceof RegExp) - && !(obj instanceof Date); - } - function safeSendConsoleMessage(severity: 'log' | 'warn' | 'error', args: string): void { safeSend({ type: '__$console', severity, arguments: args }); } diff --git a/src/vs/base/test/node/processes/fixtures/fork_console.ts b/src/vs/base/test/node/processes/fixtures/fork_console.ts new file mode 100644 index 00000000000000..c979fc7e30c932 --- /dev/null +++ b/src/vs/base/test/node/processes/fixtures/fork_console.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const sharedValue = { value: 1 }; + +const circularValue: { name: string; self?: unknown } = { name: 'circular' }; +circularValue.self = circularValue; + +console.log(sharedValue, { a: sharedValue, b: sharedValue }, circularValue); diff --git a/src/vs/base/test/node/processes/processes.integrationTest.ts b/src/vs/base/test/node/processes/processes.integrationTest.ts index 3f30e931271d56..0c726f4744627b 100644 --- a/src/vs/base/test/node/processes/processes.integrationTest.ts +++ b/src/vs/base/test/node/processes/processes.integrationTest.ts @@ -59,6 +59,34 @@ suite('Processes', () => { }); }); + test('console forwarding - shared references are not reported as circular', function (done: (err?: unknown) => void) { + if (process.env['VSCODE_PID']) { + return done(); // this test fails when run from within VS Code + } + + const child = fork('vs/base/test/node/processes/fixtures/fork_console'); + + child.on('message', msgFromChild => { + const msg = msgFromChild as { type?: string; arguments?: string }; + if (msg.type !== '__$console') { + return; + } + + child.kill(); + + try { + assert.deepStrictEqual(JSON.parse(msg.arguments!), [ + { value: 1 }, + { a: { value: 1 }, b: { value: 1 } }, + { name: 'circular', self: '[Circular]' } + ]); + done(); + } catch (error) { + done(error); + } + }); + }); + (!platform.isWindows || process.env['VSCODE_PID'] ? test.skip : test)('buffered sending - lots of data (potential deadlock on win32)', function (done: () => void) { // test is only relevant for Windows and seems to crash randomly on some Linux builds const child = fork('vs/base/test/node/processes/fixtures/fork_large'); const sender = processes.createQueuedSender(child); From 3fa8626aca08f9acb2480cfd60e322d8c29d53ee Mon Sep 17 00:00:00 2001 From: Leo Camus Date: Fri, 11 Sep 2026 01:28:15 +0200 Subject: [PATCH 2/2] Simplify comment on JSON serialization logic Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/bootstrap-fork.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bootstrap-fork.ts b/src/bootstrap-fork.ts index 55bc99f2649d71..73dd8665d546dc 100644 --- a/src/bootstrap-fork.ts +++ b/src/bootstrap-fork.ts @@ -52,9 +52,7 @@ function pipeLoggingToParent(): void { try { const res = JSON.stringify(argsArray, function (this: unknown, key, value: unknown) { - // Objects get special treatment to prevent circles. Only the current - // ancestor path is tracked, so a value that is shared across arguments - // or sibling properties is serialized in full rather than as circular. + // Track only current ancestors so shared references are serialized in full. if (typeof value === 'object' && value !== null) { // `this` is the object holding `key`, pop the subtrees that are already done while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {