Skip to content

Commit b7215d8

Browse files
author
刘威
committed
feat: add adb terminal and tunnel status panels
1 parent 0a734fe commit b7215d8

5 files changed

Lines changed: 914 additions & 67 deletions

File tree

src/main/index.js

Lines changed: 199 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ console.log(`[StartupDiag] index.js first line reached: ${new Date(_procStartMs)
99
const { app, BrowserWindow, ipcMain, session, dialog } = require('electron');
1010
console.log(`[StartupDiag] +${Math.round(Number(process.hrtime.bigint() - _procStart) / 1e6)}ms after electron require`);
1111
const net = require('net');
12+
const { spawn } = require('child_process');
1213
const path = require('path');
1314
const 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
172173
class 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

Comments
 (0)