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