@@ -9,6 +9,7 @@ console.log(`[StartupDiag] index.js first line reached: ${new Date(_procStartMs)
99const { app, BrowserWindow, ipcMain, session, dialog } = require ( 'electron' ) ;
1010console . log ( `[StartupDiag] +${ Math . round ( Number ( process . hrtime . bigint ( ) - _procStart ) / 1e6 ) } ms after electron require` ) ;
1111const net = require ( 'net' ) ;
12+ const { spawn } = require ( 'child_process' ) ;
1213const path = require ( 'path' ) ;
1314const fs = require ( 'fs' ) ;
1415// NOTE: adb/index.js and adb/device.js both do top-level require('adbkit') which
@@ -168,42 +169,112 @@ class ConnectionManager {
168169 }
169170}
170171
171- // Terminal Manager - SSH connection to Termux
172+ // Terminal Manager - SSH or ADB shell connection to phone
172173class TerminalManager {
173174 constructor ( adbManager ) {
174175 this . adbManager = adbManager ;
175176 this . sshConnection = null ;
176177 this . sshStream = null ;
178+ this . adbProcess = null ;
177179 this . sshLocalPort = 8022 ;
178180 this . connected = false ;
179181 this . credentials = null ;
182+ this . mode = 'adb' ;
180183 }
181184
182185 /**
183- * Connect to Termux via SSH
186+ * Connect to terminal backend
184187 */
185188 async connect ( options = { } ) {
189+ const mode = options . mode === 'ssh' ? 'ssh' : 'adb' ;
186190 console . log ( '[Terminal] connect() called with options:' , { ...options , password : '***' } ) ;
187191
188192 if ( this . connected ) {
189193 console . log ( '[Terminal] Already connected, disconnecting first' ) ;
190194 await this . disconnect ( ) ;
191195 }
192196
193- const { username, password, localPort = 8022 } = options ;
197+ this . mode = mode ;
198+ if ( mode === 'adb' ) {
199+ return this . connectAdbShell ( options ) ;
200+ }
201+ return this . connectSsh ( options ) ;
202+ }
194203
195- // Store credentials for reconnect
196- this . credentials = { username, password } ;
197- this . sshLocalPort = localPort ;
204+ _sendTerminalData ( data ) {
205+ if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
206+ mainWindow . webContents . send ( 'terminal:data' , data ) ;
207+ }
208+ }
198209
199- // Check for device
210+ _sendTerminalClose ( reason , mode ) {
211+ if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
212+ mainWindow . webContents . send ( 'terminal:close' , { reason, mode } ) ;
213+ }
214+ }
215+
216+ _createBufferedSender ( mode ) {
217+ let dataBuffer = '' ;
218+ let dataFlushTimeout = null ;
219+
220+ const flushData = ( ) => {
221+ if ( dataBuffer ) {
222+ this . _sendTerminalData ( dataBuffer ) ;
223+ dataBuffer = '' ;
224+ }
225+ dataFlushTimeout = null ;
226+ } ;
227+
228+ return {
229+ push : ( data ) => {
230+ dataBuffer += data . toString ( 'utf8' ) ;
231+ if ( ! dataFlushTimeout ) {
232+ dataFlushTimeout = setTimeout ( flushData , 16 ) ;
233+ }
234+ } ,
235+ flush : ( ) => {
236+ if ( dataFlushTimeout ) {
237+ clearTimeout ( dataFlushTimeout ) ;
238+ }
239+ flushData ( ) ;
240+ } ,
241+ close : ( reason ) => {
242+ if ( dataFlushTimeout ) {
243+ clearTimeout ( dataFlushTimeout ) ;
244+ }
245+ flushData ( ) ;
246+ this . connected = false ;
247+ if ( mode === 'ssh' ) {
248+ this . sshStream = null ;
249+ }
250+ this . _sendTerminalClose ( reason , mode ) ;
251+ }
252+ } ;
253+ }
254+
255+ _requireDevice ( ) {
200256 console . log ( '[Terminal] Checking for connected device...' ) ;
201257 const device = this . adbManager . getFirstDevice ( ) ;
202258 if ( ! device ) {
203259 console . error ( '[Terminal] No device connected' ) ;
204260 throw new Error ( 'No device connected. Please connect your phone and try again.' ) ;
205261 }
262+
206263 console . log ( '[Terminal] Device found:' , device . id ) ;
264+ return device ;
265+ }
266+
267+ /**
268+ * Connect to Termux via SSH
269+ */
270+ async connectSsh ( options = { } ) {
271+ const { username, password, localPort = 8022 } = options ;
272+
273+ // Store credentials for reconnect
274+ this . credentials = { username, password } ;
275+ this . sshLocalPort = localPort ;
276+
277+ const device = this . _requireDevice ( ) ;
207278
208279 // Create ADB forward for SSH
209280 // Termux sshd defaults to port 8022
@@ -280,41 +351,18 @@ class TerminalManager {
280351
281352 this . sshStream = stream ;
282353 this . connected = true ;
354+ this . mode = 'ssh' ;
283355 console . log ( '[Terminal] Shell created successfully - terminal ready' ) ;
284-
285- // Buffer for data throttling
286- let dataBuffer = '' ;
287- let dataFlushTimeout = null ;
288-
289- const flushData = ( ) => {
290- if ( dataBuffer && mainWindow && ! mainWindow . isDestroyed ( ) ) {
291- mainWindow . webContents . send ( 'terminal:data' , dataBuffer ) ;
292- dataBuffer = '' ;
293- }
294- dataFlushTimeout = null ;
295- } ;
356+ const bufferedSender = this . _createBufferedSender ( 'ssh' ) ;
296357
297358 // Handle stream events with buffering
298359 stream . on ( 'data' , ( data ) => {
299- dataBuffer += data . toString ( 'utf8' ) ;
300- // Throttle data sends to max 60fps
301- if ( ! dataFlushTimeout ) {
302- dataFlushTimeout = setTimeout ( flushData , 16 ) ;
303- }
360+ bufferedSender . push ( data ) ;
304361 } ) ;
305362
306363 stream . on ( 'close' , ( ) => {
307364 console . log ( '[Terminal] Stream closed' ) ;
308- // Flush any remaining data
309- if ( dataFlushTimeout ) {
310- clearTimeout ( dataFlushTimeout ) ;
311- flushData ( ) ;
312- }
313- this . connected = false ;
314- this . sshStream = null ;
315- if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
316- mainWindow . webContents . send ( 'terminal:close' , { reason : 'Stream closed' } ) ;
317- }
365+ bufferedSender . close ( 'SSH stream closed' ) ;
318366 } ) ;
319367
320368 stream . stderr . on ( 'data' , ( data ) => {
@@ -396,13 +444,94 @@ class TerminalManager {
396444 }
397445
398446 /**
399- * Write data to SSH stream
447+ * Connect to phone shell via adb shell
448+ */
449+ async connectAdbShell ( ) {
450+ const device = this . _requireDevice ( ) ;
451+ const adbPath = this . adbManager . deviceManager && this . adbManager . deviceManager . getAdbPath
452+ ? this . adbManager . deviceManager . getAdbPath ( )
453+ : 'adb' ;
454+
455+ console . log ( `[Terminal] Starting adb shell with: ${ adbPath } -s ${ device . id } shell` ) ;
456+
457+ return new Promise ( ( resolve , reject ) => {
458+ const proc = spawn ( adbPath , [ '-s' , device . id , 'shell' ] , {
459+ stdio : [ 'pipe' , 'pipe' , 'pipe' ]
460+ } ) ;
461+
462+ let resolved = false ;
463+ const bufferedSender = this . _createBufferedSender ( 'adb' ) ;
464+
465+ proc . stdout . on ( 'data' , ( data ) => {
466+ bufferedSender . push ( data ) ;
467+ } ) ;
468+
469+ proc . stderr . on ( 'data' , ( data ) => {
470+ bufferedSender . push ( data ) ;
471+ } ) ;
472+
473+ proc . on ( 'spawn' , ( ) => {
474+ this . adbProcess = proc ;
475+ this . connected = true ;
476+ this . mode = 'adb' ;
477+ console . log ( '[Terminal] ADB shell started' ) ;
478+ if ( ! resolved ) {
479+ resolved = true ;
480+ resolve ( { success : true , mode : 'adb' } ) ;
481+ }
482+ } ) ;
483+
484+ proc . on ( 'error' , ( err ) => {
485+ console . error ( '[Terminal] ADB shell error:' , err . message ) ;
486+ this . connected = false ;
487+ if ( this . adbProcess === proc ) {
488+ this . adbProcess = null ;
489+ }
490+ if ( ! resolved ) {
491+ resolved = true ;
492+ reject ( new Error ( `Failed to start adb shell: ${ err . message } ` ) ) ;
493+ return ;
494+ }
495+ this . _sendTerminalClose ( `ADB shell error: ${ err . message } ` , 'adb' ) ;
496+ } ) ;
497+
498+ proc . on ( 'close' , ( code , signal ) => {
499+ console . log ( '[Terminal] ADB shell closed:' , { code, signal } ) ;
500+ if ( this . adbProcess === proc ) {
501+ this . adbProcess = null ;
502+ }
503+ const reason = signal
504+ ? `ADB shell closed by signal ${ signal } `
505+ : `ADB shell exited${ typeof code === 'number' ? ` (${ code } )` : '' } ` ;
506+
507+ if ( ! resolved ) {
508+ resolved = true ;
509+ reject ( new Error ( reason ) ) ;
510+ return ;
511+ }
512+
513+ bufferedSender . close ( reason ) ;
514+ } ) ;
515+ } ) ;
516+ }
517+
518+ /**
519+ * Write data to current terminal stream
400520 */
401521 async write ( data ) {
402- if ( ! this . connected || ! this . sshStream ) {
522+ if ( ! this . connected ) {
403523 throw new Error ( 'Not connected' ) ;
404524 }
405525
526+ if ( this . mode === 'adb' && this . adbProcess && this . adbProcess . stdin ) {
527+ this . adbProcess . stdin . write ( data ) ;
528+ return true ;
529+ }
530+
531+ if ( ! this . sshStream ) {
532+ throw new Error ( 'SSH stream not available' ) ;
533+ }
534+
406535 this . sshStream . write ( data ) ;
407536 return true ;
408537 }
@@ -411,7 +540,15 @@ class TerminalManager {
411540 * Resize terminal
412541 */
413542 async resize ( cols , rows ) {
414- if ( ! this . connected || ! this . sshStream ) {
543+ if ( ! this . connected ) {
544+ return false ;
545+ }
546+
547+ if ( this . mode === 'adb' ) {
548+ return false ;
549+ }
550+
551+ if ( ! this . sshStream ) {
415552 return false ;
416553 }
417554
@@ -422,9 +559,26 @@ class TerminalManager {
422559 }
423560
424561 /**
425- * Disconnect SSH and remove ADB forward
562+ * Disconnect current terminal session
426563 */
427564 async disconnect ( ) {
565+ if ( this . adbProcess ) {
566+ const proc = this . adbProcess ;
567+ this . adbProcess = null ;
568+ try {
569+ if ( proc . stdin && ! proc . stdin . destroyed ) {
570+ proc . stdin . end ( ) ;
571+ }
572+ } catch ( err ) {
573+ console . warn ( '[Terminal] Failed to close adb shell stdin:' , err . message ) ;
574+ }
575+ try {
576+ proc . kill ( ) ;
577+ } catch ( err ) {
578+ console . warn ( '[Terminal] Failed to kill adb shell:' , err . message ) ;
579+ }
580+ }
581+
428582 if ( this . sshStream ) {
429583 this . sshStream . close ( ) ;
430584 this . sshStream = null ;
@@ -453,7 +607,8 @@ class TerminalManager {
453607 getStatus ( ) {
454608 return {
455609 connected : this . connected ,
456- localPort : this . sshLocalPort
610+ localPort : this . sshLocalPort ,
611+ mode : this . mode
457612 } ;
458613 }
459614}
@@ -863,9 +1018,11 @@ function setupIpc() {
8631018 throw new Error ( 'Terminal not initialized yet. Please wait.' ) ;
8641019 }
8651020 try {
1021+ const mode = options && options . mode === 'ssh' ? 'ssh' : 'adb' ;
1022+
8661023 // Prompt for credentials if not provided
867- if ( ! options . username || ! options . password ) {
868- const result = await dialog . showMessageBox ( mainWindow , {
1024+ if ( mode === 'ssh' && ( ! options . username || ! options . password ) ) {
1025+ await dialog . showMessageBox ( mainWindow , {
8691026 type : 'question' ,
8701027 buttons : [ 'Cancel' ] ,
8711028 title : 'SSH Credentials Required' ,
@@ -877,7 +1034,7 @@ function setupIpc() {
8771034 throw new Error ( 'SSH credentials required' ) ;
8781035 }
8791036
880- return await terminalManager . connect ( options ) ;
1037+ return await terminalManager . connect ( { ... options , mode } ) ;
8811038 } catch ( err ) {
8821039 console . error ( '[IPC] Terminal connect error:' , err . message ) ;
8831040 throw err ;
0 commit comments