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 if (!this.isRunning) { 50 return false; 51 } 52 this.removeExitHook(); 53 try { 54 await this.runAsync(['kill-server']); 55 return true; 56 } catch (error: any) { 57 Log.error('Failed to stop ADB server: ' + error.message); 58 return false; 59 } finally { 60 this.isRunning = false; 61 } 62 } 63 64 /** Execute an ADB command with given args. */ 65 async runAsync(args: string[]): Promise<string> { 66 // TODO: Add a global package that installs adb to the path. 67 const adb = this.getAdbExecutablePath(); 68 69 await this.startAsync(); 70 71 Log.debug([adb, ...args].join(' ')); 72 const result = await this.resolveAdbPromise(spawnAsync(adb, args)); 73 return result.output.join('\n'); 74 } 75 76 /** Get ADB file output. Useful for reading device state/settings. */ 77 async getFileOutputAsync(args: string[]): Promise<string> { 78 // TODO: Add a global package that installs adb to the path. 79 const adb = this.getAdbExecutablePath(); 80 81 await this.startAsync(); 82 83 return await this.resolveAdbPromise( 84 execFileSync(adb, args, { 85 encoding: 'latin1', 86 stdio: 'pipe', 87 }) 88 ); 89 } 90 91 /** Formats error info. */ 92 async resolveAdbPromise<T>(promise: T | Promise<T>): Promise<T> { 93 try { 94 return await promise; 95 } catch (error: any) { 96 // User pressed ctrl+c to cancel the process... 97 if (error.signal === 'SIGINT') { 98 throw new AbortCommandError(); 99 } 100 // TODO: Support heap corruption for adb 29 (process exits with code -1073740940) (windows and linux) 101 let errorMessage = (error.stderr || error.stdout || error.message).trim(); 102 if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) { 103 errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length); 104 } 105 error.message = errorMessage; 106 throw error; 107 } 108 } 109} 110