@@ -591,11 +591,43 @@ export default class TerminalComponent {
591591 // Poll by hitting the actual HTTP endpoint, not just checking PID liveness.
592592 // isAxsRunning() only does kill -0 on the PID file, which can return true
593593 // while the HTTP server inside proot is still booting.
594+ const fetchWithTimeout = async ( url , options = { } , timeoutMs = 2000 ) => {
595+ const hasAbortSignalTimeout =
596+ typeof AbortSignal !== "undefined" &&
597+ typeof AbortSignal . timeout === "function" ;
598+
599+ if ( hasAbortSignalTimeout ) {
600+ return fetch ( url , {
601+ ...options ,
602+ signal : AbortSignal . timeout ( timeoutMs ) ,
603+ } ) ;
604+ }
605+
606+ if ( typeof AbortController === "undefined" ) {
607+ return fetch ( url , options ) ;
608+ }
609+
610+ const controller = new AbortController ( ) ;
611+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeoutMs ) ;
612+ try {
613+ return await fetch ( url , {
614+ ...options ,
615+ signal : controller . signal ,
616+ } ) ;
617+ } finally {
618+ clearTimeout ( timeoutId ) ;
619+ }
620+ } ;
621+
594622 const pollAxs = async ( maxRetries = 30 , intervalMs = 1000 ) => {
595623 for ( let i = 0 ; i < maxRetries ; i ++ ) {
596624 await new Promise ( ( r ) => setTimeout ( r , intervalMs ) ) ;
597625 try {
598- const resp = await fetch ( `http://localhost:${ this . options . port } /` , { method : 'GET' , signal : AbortSignal . timeout ( 2000 ) } ) ;
626+ const resp = await fetchWithTimeout (
627+ `http://localhost:${ this . options . port } /` ,
628+ { method : "GET" } ,
629+ 2000 ,
630+ ) ;
599631 if ( resp . ok || resp . status < 500 ) return true ;
600632 } catch ( _ ) {
601633 // HTTP not yet reachable
@@ -615,18 +647,30 @@ export default class TerminalComponent {
615647 // AXS failed to start — attempt auto-repair
616648 toast ( "Repairing terminal environment..." ) ;
617649
618- try { await Terminal . stopAxs ( ) ; } catch ( _ ) { /* ignore */ }
650+ try {
651+ await Terminal . stopAxs ( ) ;
652+ } catch ( _ ) {
653+ /* ignore */
654+ }
619655
620656 // Re-run installing flow to repair packages / config
621- const repairOk = await Terminal . startAxs ( true , console . log , console . error ) ;
657+ const repairOk = await Terminal . startAxs (
658+ true ,
659+ console . log ,
660+ console . error ,
661+ ) ;
622662 if ( repairOk ) {
623663 // Start AXS again after repair
624664 await Terminal . startAxs ( false , ( ) => { } , console . error ) ;
625665 }
626666
627667 if ( ! ( await pollAxs ( 30 ) ) ) {
628668 // Still broken — clear .configured so next open re-triggers install
629- try { await Terminal . resetConfigured ( ) ; } catch ( _ ) { /* ignore */ }
669+ try {
670+ await Terminal . resetConfigured ( ) ;
671+ } catch ( _ ) {
672+ /* ignore */
673+ }
630674 throw new Error ( "Failed to start AXS server after repair attempt" ) ;
631675 }
632676 }
@@ -655,17 +699,23 @@ export default class TerminalComponent {
655699 const data = await response . text ( ) ;
656700
657701 // Detect PTY errors from axs server (e.g. incompatible binary)
658- if ( data . includes ( '"error"' ) && data . includes ( ' Failed to open PTY' ) ) {
702+ if ( data . includes ( '"error"' ) && data . includes ( " Failed to open PTY" ) ) {
659703 const refreshed = await Terminal . refreshAxsBinary ( ) ;
660704 if ( refreshed ) {
661705 // Kill old axs, restart with fresh binary, and retry once
662- try { await Terminal . stopAxs ( ) ; } catch ( _ ) { }
706+ try {
707+ await Terminal . stopAxs ( ) ;
708+ } catch ( _ ) { }
663709 await Terminal . startAxs ( false , ( ) => { } , console . error ) ;
664710 const pollResult = await pollAxs ( 30 ) ;
665711 if ( pollResult ) {
666712 const retryResp = await fetch (
667713 `http://localhost:${ this . options . port } /terminals` ,
668- { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( requestBody ) } ,
714+ {
715+ method : "POST" ,
716+ headers : { "Content-Type" : "application/json" } ,
717+ body : JSON . stringify ( requestBody ) ,
718+ } ,
669719 ) ;
670720 if ( retryResp . ok ) {
671721 const retryData = await retryResp . text ( ) ;
@@ -676,7 +726,7 @@ export default class TerminalComponent {
676726 }
677727 }
678728 }
679- throw new Error ( ' Failed to open PTY even after refreshing AXS binary' ) ;
729+ throw new Error ( " Failed to open PTY even after refreshing AXS binary" ) ;
680730 }
681731
682732 this . pid = data . trim ( ) ;
@@ -703,6 +753,11 @@ export default class TerminalComponent {
703753 }
704754
705755 this . pid = pid ;
756+ this . _relocationSniffDisabled = false ;
757+ clearTimeout ( this . _relocationSniffTimer ) ;
758+ this . _relocationSniffTimer = setTimeout ( ( ) => {
759+ this . _relocationSniffDisabled = true ;
760+ } , 15000 ) ;
706761
707762 const wsUrl = `ws://localhost:${ this . options . port } /terminals/${ pid } ` ;
708763
@@ -716,7 +771,9 @@ export default class TerminalComponent {
716771 // Reassigning this.attachAddon does NOT auto-clean listeners bound by the old instance,
717772 // which can cause duplicate socket handlers and leaks after reconnects.
718773 if ( this . attachAddon ) {
719- try { this . attachAddon . dispose ( ) ; } catch ( _ ) { }
774+ try {
775+ this . attachAddon . dispose ( ) ;
776+ } catch ( _ ) { }
720777 this . attachAddon = null ;
721778 }
722779
@@ -748,19 +805,44 @@ export default class TerminalComponent {
748805
749806 // Also sniff the data to detect critical Alpine container corruption (e.g. bash/readline broken)
750807 this . websocket . addEventListener ( "message" , async ( event ) => {
808+ if ( this . _relocationSniffDisabled ) {
809+ return ;
810+ }
811+
812+ const MAX_SNIFF_BYTES = 4096 ;
813+
751814 try {
752815 let text = "" ;
753816 if ( typeof event . data === "string" ) {
754- text = event . data ;
755- } else if ( event . data instanceof ArrayBuffer || event . data instanceof Blob ) {
756- text = await new Response ( event . data ) . text ( ) ;
817+ text = event . data . slice ( 0 , MAX_SNIFF_BYTES ) ;
818+ } else if ( event . data instanceof ArrayBuffer ) {
819+ const byteLength = Math . min ( event . data . byteLength , MAX_SNIFF_BYTES ) ;
820+ const view = new Uint8Array ( event . data , 0 , byteLength ) ;
821+ text = new TextDecoder ( "utf-8" , { fatal : false } ) . decode ( view ) ;
822+ } else if ( event . data instanceof Blob ) {
823+ const slice =
824+ event . data . size > MAX_SNIFF_BYTES
825+ ? event . data . slice ( 0 , MAX_SNIFF_BYTES )
826+ : event . data ;
827+ text = await new Response ( slice ) . text ( ) ;
828+ }
829+
830+ if ( ! text ) {
831+ return ;
757832 }
758-
759- if ( text . includes ( "Error relocating" ) && text . includes ( "symbol not found" ) ) {
760- console . error ( "Detected critical Alpine libc corruption! Terminating and triggering reinstall." ) ;
833+
834+ if (
835+ text . includes ( "Error relocating" ) &&
836+ text . includes ( "symbol not found" )
837+ ) {
838+ console . error (
839+ "Detected critical Alpine libc corruption! Terminating and triggering reinstall." ,
840+ ) ;
761841 if ( this . onCrashData ) {
762842 this . onCrashData ( "relocation_error" ) ;
763843 }
844+ this . _relocationSniffDisabled = true ;
845+ clearTimeout ( this . _relocationSniffTimer ) ;
764846 }
765847 } catch ( err ) { }
766848 } ) ;
@@ -998,7 +1080,9 @@ export default class TerminalComponent {
9981080 */
9991081 async loadTerminalFont ( ) {
10001082 // Use original name without quotes for Acode fonts.get
1001- const fontFamily = this . options . fontFamily . replace ( / ^ " | " $ / g, '' ) . replace ( / " , \s * m o n o s p a c e $ / , '' ) ;
1083+ const fontFamily = this . options . fontFamily
1084+ . replace ( / ^ " | " $ / g, "" )
1085+ . replace ( / " , \s * m o n o s p a c e $ / , "" ) ;
10021086 if ( fontFamily && fonts . get ( fontFamily ) ) {
10031087 try {
10041088 await fonts . loadFont ( fontFamily ) ;
@@ -1007,7 +1091,9 @@ export default class TerminalComponent {
10071091 if ( this . terminal ) {
10081092 this . terminal . options . fontFamily = `"${ fontFamily } ", monospace` ;
10091093 if ( this . webglAddon ) {
1010- try { this . webglAddon . clearTextureAtlas ( ) ; } catch ( e ) { }
1094+ try {
1095+ this . webglAddon . clearTextureAtlas ( ) ;
1096+ } catch ( e ) { }
10111097 }
10121098 // Ensure terminal dimensions are updated after font load changes char size
10131099 setTimeout ( ( ) => this . fit ( ) , 100 ) ;
@@ -1072,6 +1158,9 @@ export default class TerminalComponent {
10721158 * Terminate terminal session
10731159 */
10741160 async terminate ( ) {
1161+ clearTimeout ( this . _relocationSniffTimer ) ;
1162+ this . _relocationSniffDisabled = true ;
1163+
10751164 if ( this . websocket ) {
10761165 this . websocket . close ( ) ;
10771166 }
0 commit comments