1import spawnAsync from '@expo/spawn-async'; 2import { execFileSync } from 'child_process'; 3 4import * as Log from '../../../log'; 5import { AbortCommandError } from '../../../utils/errors'; 6import { installExitHooks } from '../../../utils/exit'; 7import { assertSdkRoot } from './AndroidSdk'; 8 9const BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: '; 10 11// This is a tricky class since it controls a system state (side-effects). 12// A more ideal solution would be to implement ADB in JS. 13// The main reason this is a class is to control the flow of testing. 14 15export class ADBServer { 16 isRunning: boolean = false; 17 removeExitHook: () => void = () => {}; 18 19 /** Returns the command line reference to ADB. */ 20 getAdbExecutablePath(): string { 21 try { 22 const sdkRoot = assertSdkRoot(); 23 if (sdkRoot) { 24 return `${sdkRoot}/platform-tools/adb`; 25 } 26 } catch (error: any) { 27 Log.warn(error.message); 28 } 29 30 Log.debug('Failed to resolve the Android SDK path, falling back to global adb executable'); 31 return 'adb'; 32 } 33 34 /** Start the ADB server. */ 35 async startAsync(): Promise<boolean> { 36 if (this.isRunning) { 37 return false; 38 } 39 // clean up 40 this.removeExitHook = installExitHooks(() => { 41 if (this.isRunning) { 42 this.stopAsync(); 43 } 44 }); 45 const adb = this.getAdbExecutablePath(); 46 const result = await this.resolveAdbPromise(spawnAsync(adb, ['start-server'])); 47 const lines = result.stderr.trim().split(/\r?\n/); 48 const isStarted = lines.includes('* daemon started successfully'); 49 this.isRunning = isStarted; 50 return isStarted; 51 } 52 53 /** Kill the ADB server. */ 54 async stopAsync(): Promise<boolean> { 55 Log.debug('Stopping ADB server'); 56 57 if (!this.isRunning) { 58 Log.debug('ADB server is not running'); 59 return false; 60 } 61 this.removeExitHook(); 62 try { 63 await this.runAsync(['kill-server']); 64 return true; 65 } catch (error: any) { 66 Log.error('Failed to stop ADB server: ' + error.message); 67 return false; 68 } finally { 69 Log.debug('Stopped ADB server'); 70 this.isRunning = false; 71 } 72 } 73 74 /** Execute an ADB command with given args. */ 75 async runAsync(args: string[]): Promise<string> { 76 // TODO: Add a global package that installs adb to the path. 77 const adb = this.getAdbExecutablePath(); 78 79 await this.startAsync(); 80 81 Log.debug([adb, ...args].join(' ')); 82 const result = await this.resolveAdbPromise(spawnAsync(adb, args)); 83 return result.output.join('\n'); 84 } 85 86 /** Get ADB file output. Useful for reading device state/settings. */ 87 async getFileOutputAsync(args: string[]): Promise<string> { 88 // TODO: Add a global package that installs adb to the path. 89 const adb = this.getAdbExecutablePath(); 90 91 await this.startAsync(); 92 93 const results = await this.resolveAdbPromise( 94 execFileSync(adb, args, { 95 encoding: 'latin1', 96 stdio: 'pipe', 97 }) 98 ); 99 Log.debug('[ADB] File output:\n', results); 100 return results; 101 } 102 103 /** Formats error info. */ 104 async resolveAdbPromise<T>(promise: T | Promise<T>): Promise<T> { 105 try { 106 return await promise; 107 } catch (error: any) { 108 // User pressed ctrl+c to cancel the process... 109 if (error.signal === 'SIGINT') { 110 throw new AbortCommandError(); 111 } 112 // TODO: Support heap corruption for adb 29 (process exits with code -1073740940) (windows and linux) 113 let errorMessage = (error.stderr || error.stdout || error.message).trim(); 114 if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) { 115 errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length); 116 } 117 error.message = errorMessage; 118 throw error; 119 } 120 } 121} 122