@@ -128,6 +128,8 @@ const SHOW_CURSOR = "\u001B[?25h";
128128const CHILD_TERMINATION_GRACE_MS = 1_000 ;
129129
130130type Writable = Pick < NodeJS . WriteStream , "write" > & {
131+ on ?( event : "error" , listener : ( error : Error ) => void ) : unknown ;
132+ off ?( event : "error" , listener : ( error : Error ) = > void ) : unknown ;
131133 readonly isTTY ? : boolean ;
132134 readonly fd ? : number ;
133135 readonly columns ? : number ;
@@ -2646,6 +2648,39 @@ async function runScan(
26462648 errorOutput : Writable ,
26472649 dependencies : CliDependencies ,
26482650 interactive = true ,
2651+ ) : Promise < ScanOutcome > {
2652+ const observeTerminalErrors =
2653+ typeof errorOutput . on === "function" &&
2654+ typeof errorOutput . off === "function" ;
2655+ const ignoreTerminalError = ( ) : void => { } ;
2656+ if ( observeTerminalErrors ) {
2657+ errorOutput . on ?. ( "error" , ignoreTerminalError ) ;
2658+ }
2659+ try {
2660+ return await executeScan (
2661+ arguments_ ,
2662+ errorOutput ,
2663+ dependencies ,
2664+ interactive ,
2665+ ) ;
2666+ } finally {
2667+ if ( observeTerminalErrors ) {
2668+ try {
2669+ errorOutput . write ( "" , ( ) => {
2670+ queueMicrotask ( ( ) => errorOutput . off ?. ( "error" , ignoreTerminalError ) ) ;
2671+ } ) ;
2672+ } catch {
2673+ errorOutput . off ?. ( "error" , ignoreTerminalError ) ;
2674+ }
2675+ }
2676+ }
2677+ }
2678+
2679+ async function executeScan (
2680+ arguments_ : ScanArguments ,
2681+ errorOutput : Writable ,
2682+ dependencies : CliDependencies ,
2683+ interactive = true ,
26492684) : Promise < ScanOutcome > {
26502685 let scanDir : string | null = null ;
26512686 let requestedSignal : SignalName | null = null ;
@@ -2691,6 +2726,14 @@ async function runScan(
26912726 } ) ;
26922727 } ;
26932728 const preparationAbortController = new AbortController ( ) ;
2729+ const stopPresentation = ( ) : void => {
2730+ try {
2731+ dashboard ?. stop ( ) ;
2732+ } catch { }
2733+ try {
2734+ progress ?. stopTimer ( ) ;
2735+ } catch { }
2736+ } ;
26942737 const signalListener = ( signal : SignalName ) = > ( ) => {
26952738 if ( requestedSignal !== null ) {
26962739 // Launchers and terminals can deliver the same initial signal twice.
@@ -2702,8 +2745,7 @@ async function runScan(
27022745 return ;
27032746 }
27042747 requestedSignal = signal ;
2705- dashboard ?. stop ( ) ;
2706- progress ?. stopTimer ( ) ;
2748+ stopPresentation ( ) ;
27072749 if ( progress ?. interactive === true ) {
27082750 try {
27092751 dependencies . writeSynchronously ( errorOutput , SHOW_CURSOR ) ;
@@ -3150,8 +3192,7 @@ async function runScan(
31503192 failed = true ;
31513193 failure = error ;
31523194 } finally {
3153- dashboard?. stop ( ) ;
3154- progress ?. stopTimer ( ) ;
3195+ stopPresentation ( ) ;
31553196 if ( security !== null ) {
31563197 diagnostic ( "runtime.cleanup.started" ) ;
31573198 await security . close ( ) . then (
@@ -3229,6 +3270,7 @@ async function runScan(
32293270 : undefined ,
32303271 verified : effectivePreflight . authentication . verified ,
32313272 } ) ;
3273+ progress ?. stopTimer ( ) ;
32323274 return { exitCode : 0 , data : { dryRun : true , ...effectivePreflight } } ;
32333275 }
32343276 if ( result === null ) {
@@ -3277,6 +3319,7 @@ async function runScan(
32773319 errorOutput . write (
32783320 "codex-security: Scan target changed during execution; results do not represent the current checkout.\n" ,
32793321 ) ;
3322+ progress ?. stopTimer ( ) ;
32803323 return { exitCode : 2 , data : scanData } ;
32813324 }
32823325 if ( incomplete ) {
@@ -3285,8 +3328,10 @@ async function runScan(
32853328 ? `codex-security: Scan coverage is ${ result . coverage . completeness } ; results may be incomplete.\n`
32863329 : `codex-security: Cannot evaluate the failure policy: coverage is ${ result . coverage . completeness } .\n` ,
32873330 ) ;
3331+ progress ?. stopTimer ( ) ;
32883332 return { exitCode : 2 , data : scanData } ;
32893333 }
3334+ progress ?. stopTimer ( ) ;
32903335 return { exitCode : blockingCount > 0 ? 1 : 0 , data : scanData } ;
32913336}
32923337
@@ -3713,6 +3758,10 @@ export class Progress {
37133758 #timerMessage: string | null = null;
37143759 #timerLineActive = false;
37153760 #cursorHidden = false;
3761+ #observingStreamErrors = false;
3762+ #streamErrorsActive = false;
3763+ #streamErrorGeneration = 0;
3764+ readonly #onStreamError = (): void => {};
37163765
37173766 public constructor(
37183767 stream: Writable = process.stderr,
@@ -3740,37 +3789,64 @@ export class Progress {
37403789 }
37413790
37423791 public stage(message: string): void {
3792+ this.#observeStreamErrors();
37433793 this.#stream.write(` $ { this . #line( message ) } \n`);
37443794 }
37453795
37463796 public startTimer(message: string): void {
3797+ this.#observeStreamErrors();
37473798 if (!this.interactive) {
37483799 this.stage(message);
37493800 return;
37503801 }
37513802 this.#stream.write(HIDE_CURSOR);
37523803 this.#cursorHidden = true;
37533804 this.#renderTimer(message);
3754- this.#timer = this.#dependencies.setInterval(
3755- () => this.#renderTimer(message),
3756- PROGRESS_REFRESH_MILLISECONDS,
3757- );
3805+ this.#timer = this.#dependencies.setInterval(() => {
3806+ try {
3807+ this.#renderTimer(message);
3808+ } catch {}
3809+ }, PROGRESS_REFRESH_MILLISECONDS);
37583810 this.#timerMessage = message;
37593811 }
37603812
37613813 public stopTimer(): void {
3762- if (this.#timer !== null) {
3763- this.#dependencies.clearInterval(this.#timer);
3764- this.#timer = null;
3765- }
3766- this.#timerMessage = null;
3767- if (this.#timerLineActive) {
3768- this.#stream.write("\n");
3769- this.#timerLineActive = false;
3770- }
3771- if (this.#cursorHidden) {
3772- this.#stream.write(SHOW_CURSOR);
3773- this.#cursorHidden = false;
3814+ try {
3815+ if (this.#timer !== null) {
3816+ this.#dependencies.clearInterval(this.#timer);
3817+ this.#timer = null;
3818+ }
3819+ this.#timerMessage = null;
3820+ if (this.#timerLineActive) {
3821+ this.#stream.write("\n");
3822+ this.#timerLineActive = false;
3823+ }
3824+ if (this.#cursorHidden) {
3825+ this.#stream.write(SHOW_CURSOR);
3826+ this.#cursorHidden = false;
3827+ }
3828+ } finally {
3829+ if (this.#observingStreamErrors) {
3830+ this.#streamErrorsActive = false;
3831+ const generation = this.#streamErrorGeneration;
3832+ try {
3833+ this.#stream.write("", () => {
3834+ queueMicrotask(() => {
3835+ if (
3836+ generation === this.#streamErrorGeneration &&
3837+ !this.#streamErrorsActive &&
3838+ this.#observingStreamErrors
3839+ ) {
3840+ this.#stream.off?.("error", this.#onStreamError);
3841+ this.#observingStreamErrors = false;
3842+ }
3843+ });
3844+ });
3845+ } catch {
3846+ this.#stream.off?.("error", this.#onStreamError);
3847+ this.#observingStreamErrors = false;
3848+ }
3849+ }
37743850 }
37753851 }
37763852
@@ -3795,6 +3871,15 @@ export class Progress {
37953871 return ` [ $ { String ( minutes ) . padStart ( 2 , "0" ) } :${String ( seconds ) . padStart ( 2 , "0" ) } ] $ { message } `;
37963872 }
37973873
3874+ #observeStreamErrors(): void {
3875+ this.#streamErrorsActive = true;
3876+ this.#streamErrorGeneration += 1;
3877+ if (!this.#observingStreamErrors && this.#stream.on !== undefined) {
3878+ this.#stream.on("error", this.#onStreamError);
3879+ this.#observingStreamErrors = true;
3880+ }
3881+ }
3882+
37983883 #renderTimer(message: string): void {
37993884 this.#stream.write(
38003885 ` $ { this . #timerLineActive ? "\r" : "" } ${this . #line( message ) } `,
0 commit comments