11import WebSocket from "ws" ;
22import { cliStatus } from "../../utils/cliStatus.js" ;
33import { outputError } from "../../utils/output.js" ;
4- import { processUtils } from "../../utils/processUtils.js" ;
54import { waitForReady } from "../../utils/ssh.js" ;
65import {
76 getPtyBaseUrl ,
8- ptyControl ,
97 createPtySessionReleaser ,
108 resolvePtyWebSocketUrl ,
119 createPtyTunnel ,
1210 getPtyTunnelBaseUrl ,
1311 isLocalPtyOverride ,
14- buildWsHeaders ,
1512 settleAfterPtyTunnel ,
1613 refreshPtySessionAfterAttach ,
14+ startPtyIoSession ,
1715} from "../../lib/pty-client.js" ;
1816import { openPtyWebSocket } from "../../lib/pty-ws.js" ;
1917
@@ -41,7 +39,7 @@ function registerPtyInterruptHandlers(
4139 return dispose ;
4240}
4341
44- function writePtyStreamToStdout ( data : WebSocket . Data ) : void {
42+ function writePtyStreamToStdout ( data : WebSocket . RawData ) : void {
4543 if ( Buffer . isBuffer ( data ) ) {
4644 process . stdout . write ( data ) ;
4745 } else if ( data instanceof ArrayBuffer ) {
@@ -101,6 +99,11 @@ export async function ptyDevbox(devboxId: string, options: PtyOptions = {}) {
10199 }
102100}
103101
102+ const PTY_EXEC_TIMEOUT_MS = ( ( ) => {
103+ const v = parseInt ( process . env . RUNLOOP_PTY_EXEC_TIMEOUT_MS || "0" , 10 ) ;
104+ return isNaN ( v ) || v < 0 ? 0 : v ;
105+ } ) ( ) ;
106+
104107async function execCommand (
105108 baseUrl : string ,
106109 sessionName : string ,
@@ -112,41 +115,39 @@ async function execCommand(
112115 rows : 24 ,
113116 authToken,
114117 } ) ;
115- const ws = await openPtyWebSocket ( wsUrl , buildWsHeaders ( authToken ) ) ;
116- await refreshPtySessionAfterAttach (
117- ws ,
118- baseUrl ,
119- sessionName ,
120- 80 ,
121- 24 ,
122- authToken ,
123- ) ;
118+ const ws = await openPtyWebSocket ( wsUrl , authToken ) ;
119+ await refreshPtySessionAfterAttach ( ws , baseUrl , sessionName , 80 , 24 , authToken ) ;
124120 ws . send ( command + "\n" ) ;
125121
126122 return new Promise < void > ( ( resolve , reject ) => {
127- const releaseOnce = createPtySessionReleaser (
128- baseUrl ,
129- sessionName ,
130- authToken ,
131- ) ;
132- const disposeInterruptSignals = registerPtyInterruptHandlers (
133- ws ,
134- releaseOnce ,
135- ) ;
136-
137- ws . on ( "message" , ( data : WebSocket . Data ) => {
123+ const releaseOnce = createPtySessionReleaser ( baseUrl , sessionName , authToken ) ;
124+ const disposeInterruptSignals = registerPtyInterruptHandlers ( ws , releaseOnce ) ;
125+
126+ let timeoutId : ReturnType < typeof setTimeout > | undefined ;
127+ if ( PTY_EXEC_TIMEOUT_MS > 0 ) {
128+ timeoutId = setTimeout ( ( ) => {
129+ releaseOnce ( ) ;
130+ ws . close ( ) ;
131+ } , PTY_EXEC_TIMEOUT_MS ) ;
132+ }
133+
134+ const finish = ( ) => {
135+ if ( timeoutId !== undefined ) clearTimeout ( timeoutId ) ;
136+ releaseOnce ( ) ;
137+ disposeInterruptSignals ( ) ;
138+ } ;
139+
140+ ws . on ( "message" , ( data : WebSocket . RawData ) => {
138141 writePtyStreamToStdout ( data ) ;
139142 } ) ;
140143
141144 ws . on ( "close" , ( ) => {
142- releaseOnce ( ) ;
143- disposeInterruptSignals ( ) ;
145+ finish ( ) ;
144146 resolve ( ) ;
145147 } ) ;
146148
147149 ws . on ( "error" , ( err : Error ) => {
148- releaseOnce ( ) ;
149- disposeInterruptSignals ( ) ;
150+ finish ( ) ;
150151 reject ( err ) ;
151152 } ) ;
152153 } ) ;
@@ -165,79 +166,21 @@ async function interactiveSession(
165166 rows,
166167 authToken,
167168 } ) ;
168- const ws = await openPtyWebSocket ( wsUrl , buildWsHeaders ( authToken ) ) ;
169- await refreshPtySessionAfterAttach (
170- ws ,
171- baseUrl ,
172- sessionName ,
173- cols ,
174- rows ,
175- authToken ,
176- ) ;
177-
178- return new Promise < void > ( ( resolve , reject ) => {
179- const releaseOnce = createPtySessionReleaser (
180- baseUrl ,
181- sessionName ,
182- authToken ,
183- ) ;
184- const disposeInterruptSignals = registerPtyInterruptHandlers (
185- ws ,
186- releaseOnce ,
187- ) ;
188-
189- const cleanup = ( ) => {
190- disposeInterruptSignals ( ) ;
191- process . stdin . removeListener ( "data" , onStdinData ) ;
192- process . removeListener ( "SIGWINCH" , onResize ) ;
193- if ( processUtils . stdin . isTTY && processUtils . stdin . setRawMode ) {
194- processUtils . stdin . setRawMode ( false ) ;
195- }
196- process . stdin . pause ( ) ;
197- } ;
198-
199- const onStdinData = ( data : Buffer ) => {
200- if ( ws . readyState === WebSocket . OPEN ) {
201- ws . send ( data ) ;
202- }
203- } ;
204-
205- const onResize = ( ) => {
206- const newCols = process . stdout . columns || 80 ;
207- const newRows = process . stdout . rows || 24 ;
208- ptyControl (
209- baseUrl ,
210- sessionName ,
211- {
212- action : "resize" ,
213- cols : newCols ,
214- rows : newRows ,
215- } ,
216- authToken ,
217- ) . catch ( ( ) => { } ) ;
218- } ;
169+ const ws = await openPtyWebSocket ( wsUrl , authToken ) ;
170+ await refreshPtySessionAfterAttach ( ws , baseUrl , sessionName , cols , rows , authToken ) ;
219171
220- if ( processUtils . stdin . isTTY && processUtils . stdin . setRawMode ) {
221- processUtils . stdin . setRawMode ( true ) ;
222- }
223- process . stdin . resume ( ) ;
224- process . stdin . on ( "data" , onStdinData ) ;
225- process . on ( "SIGWINCH" , onResize ) ;
172+ const releaseOnce = createPtySessionReleaser ( baseUrl , sessionName , authToken ) ;
173+ const { dispose, done } = startPtyIoSession ( ws , baseUrl , sessionName , authToken ) ;
174+ const disposeSignals = registerPtyInterruptHandlers ( ws , releaseOnce ) ;
226175
227- ws . on ( "message" , ( data : WebSocket . Data ) => {
228- writePtyStreamToStdout ( data ) ;
229- } ) ;
230-
231- ws . on ( "close" , ( ) => {
232- releaseOnce ( ) ;
233- cleanup ( ) ;
234- resolve ( ) ;
235- } ) ;
236-
237- ws . on ( "error" , ( err : Error ) => {
238- releaseOnce ( ) ;
239- cleanup ( ) ;
240- reject ( err ) ;
241- } ) ;
242- } ) ;
176+ try {
177+ await done ;
178+ releaseOnce ( ) ;
179+ } catch ( err ) {
180+ releaseOnce ( ) ;
181+ throw err ;
182+ } finally {
183+ dispose ( ) ;
184+ disposeSignals ( ) ;
185+ }
243186}
0 commit comments