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