Protect stdout and stderr so JavaScript code can't accidentally write to stdout corrupting ink rendering (#13247)

Bypassing rules as link checker failure is spurious.
This commit is contained in:
Jacob Richman
2025-11-20 10:44:02 -08:00
committed by GitHub
parent e20d282088
commit d1e35f8660
82 changed files with 1523 additions and 868 deletions
@@ -11,6 +11,7 @@ import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { debugState } from '../debug.js';
import { appEvents, AppEvent } from '../../utils/events.js';
import { debugLogger } from '@google/gemini-cli-core';
// Frames that render at least this far before or after an action are considered
// idle frames.
@@ -21,6 +22,7 @@ export const FRAME_TIMESTAMP_CAPACITY = 2048;
// Exported for testing purposes.
export const profiler = {
profilersActive: 0,
numFrames: 0,
totalIdleFrames: 0,
totalFlickerFrames: 0,
@@ -47,25 +49,25 @@ export const profiler = {
},
reportFrameRendered() {
if (this.profilersActive === 0) {
return;
}
const now = Date.now();
// Simple frame detection logic (a write after at least 16ms is a new frame)
if (now - this.lastFrameStartTime > 16) {
this.lastFrameStartTime = now;
this.numFrames++;
if (debugState.debugNumAnimatedComponents === 0) {
if (this.possiblyIdleFrameTimestamps.size >= FRAME_TIMESTAMP_CAPACITY) {
this.possiblyIdleFrameTimestamps.shift();
}
this.possiblyIdleFrameTimestamps.push(now);
} else {
// If a spinner is present, consider this an action that both prevents
// this frame from being idle and also should prevent a follow on frame
// from being considered idle.
if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
this.actionTimestamps.shift();
}
this.actionTimestamps.push(now);
this.lastFrameStartTime = now;
this.numFrames++;
if (debugState.debugNumAnimatedComponents === 0) {
if (this.possiblyIdleFrameTimestamps.size >= FRAME_TIMESTAMP_CAPACITY) {
this.possiblyIdleFrameTimestamps.shift();
}
this.possiblyIdleFrameTimestamps.push(now);
} else {
// If a spinner is present, consider this an action that both prevents
// this frame from being idle and also should prevent a follow on frame
// from being considered idle.
if (this.actionTimestamps.size >= ACTION_TIMESTAMP_CAPACITY) {
this.actionTimestamps.shift();
}
this.actionTimestamps.push(now);
}
},
@@ -108,8 +110,7 @@ export const profiler = {
this.openedDebugConsole = true;
appEvents.emit(AppEvent.OpenDebugConsole);
}
appEvents.emit(
AppEvent.LogError,
debugLogger.error(
`${idleInPastSecond} frames rendered while the app was ` +
`idle in the past second. This likely indicates severe infinite loop ` +
`React state management bugs.`,
@@ -130,8 +131,7 @@ export const profiler = {
if (!this.hasLoggedFirstFlicker) {
this.hasLoggedFirstFlicker = true;
appEvents.emit(
AppEvent.LogError,
debugLogger.error(
'A flicker frame was detected. This will cause UI instability. Type `/profile` for more info.',
);
}
@@ -149,6 +149,7 @@ export const DebugProfiler = () => {
// Effect for listening to stdin for keypresses and stdout for resize events.
useEffect(() => {
profiler.profilersActive++;
const stdin = process.stdin;
const stdout = process.stdout;
@@ -162,31 +163,7 @@ export const DebugProfiler = () => {
return () => {
stdin.off('data', handler);
stdout.off('resize', handler);
};
}, []);
// Effect for patching stdout to count frames and detect idle ones
useEffect(() => {
const originalWrite = process.stdout.write;
const boundOriginalWrite = originalWrite.bind(process.stdout);
process.stdout.write = (
chunk: Uint8Array | string,
encodingOrCb?:
| BufferEncoding
| ((err?: NodeJS.ErrnoException | null) => void),
cb?: (err?: NodeJS.ErrnoException | null) => void,
) => {
profiler.reportFrameRendered();
if (typeof encodingOrCb === 'function') {
return boundOriginalWrite(chunk, encodingOrCb);
}
return boundOriginalWrite(chunk, encodingOrCb, cb);
};
return () => {
process.stdout.write = originalWrite;
profiler.profilersActive--;
};
}, []);