18d307f52SEvan Baconimport spawnAsync from '@expo/spawn-async'; 28d307f52SEvan Baconimport { execFileSync } from 'child_process'; 38d307f52SEvan Bacon 4*8a424bebSJames Ideimport { assertSdkRoot } from './AndroidSdk'; 5474a7a4bSEvan Baconimport { Log } from '../../../log'; 68d307f52SEvan Baconimport { AbortCommandError } from '../../../utils/errors'; 78d307f52SEvan Baconimport { installExitHooks } from '../../../utils/exit'; 88d307f52SEvan Bacon 9474a7a4bSEvan Baconconst debug = require('debug')('expo:start:platforms:android:adbServer') as typeof console.log; 10474a7a4bSEvan Bacon 118d307f52SEvan Baconconst BEGINNING_OF_ADB_ERROR_MESSAGE = 'error: '; 128d307f52SEvan Bacon 138d307f52SEvan Bacon// This is a tricky class since it controls a system state (side-effects). 148d307f52SEvan Bacon// A more ideal solution would be to implement ADB in JS. 158d307f52SEvan Bacon// The main reason this is a class is to control the flow of testing. 168d307f52SEvan Bacon 178d307f52SEvan Baconexport class ADBServer { 188d307f52SEvan Bacon isRunning: boolean = false; 198d307f52SEvan Bacon removeExitHook: () => void = () => {}; 208d307f52SEvan Bacon 218d307f52SEvan Bacon /** Returns the command line reference to ADB. */ 228d307f52SEvan Bacon getAdbExecutablePath(): string { 23964fdd0aSCedric van Putten try { 24964fdd0aSCedric van Putten const sdkRoot = assertSdkRoot(); 25964fdd0aSCedric van Putten if (sdkRoot) { 26964fdd0aSCedric van Putten return `${sdkRoot}/platform-tools/adb`; 278d307f52SEvan Bacon } 28964fdd0aSCedric van Putten } catch (error: any) { 29964fdd0aSCedric van Putten Log.warn(error.message); 30964fdd0aSCedric van Putten } 31964fdd0aSCedric van Putten 32964fdd0aSCedric van Putten Log.debug('Failed to resolve the Android SDK path, falling back to global adb executable'); 338d307f52SEvan Bacon return 'adb'; 348d307f52SEvan Bacon } 358d307f52SEvan Bacon 368d307f52SEvan Bacon /** Start the ADB server. */ 378d307f52SEvan Bacon async startAsync(): Promise<boolean> { 388d307f52SEvan Bacon if (this.isRunning) { 398d307f52SEvan Bacon return false; 408d307f52SEvan Bacon } 418d307f52SEvan Bacon // clean up 428d307f52SEvan Bacon this.removeExitHook = installExitHooks(() => { 438d307f52SEvan Bacon if (this.isRunning) { 448d307f52SEvan Bacon this.stopAsync(); 458d307f52SEvan Bacon } 468d307f52SEvan Bacon }); 478d307f52SEvan Bacon const adb = this.getAdbExecutablePath(); 488d307f52SEvan Bacon const result = await this.resolveAdbPromise(spawnAsync(adb, ['start-server'])); 498d307f52SEvan Bacon const lines = result.stderr.trim().split(/\r?\n/); 508d307f52SEvan Bacon const isStarted = lines.includes('* daemon started successfully'); 518d307f52SEvan Bacon this.isRunning = isStarted; 528d307f52SEvan Bacon return isStarted; 538d307f52SEvan Bacon } 548d307f52SEvan Bacon 558d307f52SEvan Bacon /** Kill the ADB server. */ 568d307f52SEvan Bacon async stopAsync(): Promise<boolean> { 57474a7a4bSEvan Bacon debug('Stopping ADB server'); 58d04463cbSEvan Bacon 598d307f52SEvan Bacon if (!this.isRunning) { 60474a7a4bSEvan Bacon debug('ADB server is not running'); 618d307f52SEvan Bacon return false; 628d307f52SEvan Bacon } 638d307f52SEvan Bacon this.removeExitHook(); 648d307f52SEvan Bacon try { 658d307f52SEvan Bacon await this.runAsync(['kill-server']); 668d307f52SEvan Bacon return true; 6729975bfdSEvan Bacon } catch (error: any) { 6829975bfdSEvan Bacon Log.error('Failed to stop ADB server: ' + error.message); 698d307f52SEvan Bacon return false; 708d307f52SEvan Bacon } finally { 71474a7a4bSEvan Bacon debug('Stopped ADB server'); 728d307f52SEvan Bacon this.isRunning = false; 738d307f52SEvan Bacon } 748d307f52SEvan Bacon } 758d307f52SEvan Bacon 768d307f52SEvan Bacon /** Execute an ADB command with given args. */ 778d307f52SEvan Bacon async runAsync(args: string[]): Promise<string> { 788d307f52SEvan Bacon // TODO: Add a global package that installs adb to the path. 798d307f52SEvan Bacon const adb = this.getAdbExecutablePath(); 808d307f52SEvan Bacon 818d307f52SEvan Bacon await this.startAsync(); 828d307f52SEvan Bacon 83474a7a4bSEvan Bacon debug([adb, ...args].join(' ')); 848d307f52SEvan Bacon const result = await this.resolveAdbPromise(spawnAsync(adb, args)); 858d307f52SEvan Bacon return result.output.join('\n'); 868d307f52SEvan Bacon } 878d307f52SEvan Bacon 888d307f52SEvan Bacon /** Get ADB file output. Useful for reading device state/settings. */ 898d307f52SEvan Bacon async getFileOutputAsync(args: string[]): Promise<string> { 908d307f52SEvan Bacon // TODO: Add a global package that installs adb to the path. 918d307f52SEvan Bacon const adb = this.getAdbExecutablePath(); 928d307f52SEvan Bacon 938d307f52SEvan Bacon await this.startAsync(); 948d307f52SEvan Bacon 953d6e487dSEvan Bacon const results = await this.resolveAdbPromise( 968d307f52SEvan Bacon execFileSync(adb, args, { 978d307f52SEvan Bacon encoding: 'latin1', 988d307f52SEvan Bacon stdio: 'pipe', 998d307f52SEvan Bacon }) 1008d307f52SEvan Bacon ); 101474a7a4bSEvan Bacon debug('[ADB] File output:\n', results); 1023d6e487dSEvan Bacon return results; 1038d307f52SEvan Bacon } 1048d307f52SEvan Bacon 1058d307f52SEvan Bacon /** Formats error info. */ 1068d307f52SEvan Bacon async resolveAdbPromise<T>(promise: T | Promise<T>): Promise<T> { 1078d307f52SEvan Bacon try { 1088d307f52SEvan Bacon return await promise; 10929975bfdSEvan Bacon } catch (error: any) { 1108d307f52SEvan Bacon // User pressed ctrl+c to cancel the process... 11129975bfdSEvan Bacon if (error.signal === 'SIGINT') { 1128d307f52SEvan Bacon throw new AbortCommandError(); 1138d307f52SEvan Bacon } 1148d307f52SEvan Bacon // TODO: Support heap corruption for adb 29 (process exits with code -1073740940) (windows and linux) 11529975bfdSEvan Bacon let errorMessage = (error.stderr || error.stdout || error.message).trim(); 1168d307f52SEvan Bacon if (errorMessage.startsWith(BEGINNING_OF_ADB_ERROR_MESSAGE)) { 1178d307f52SEvan Bacon errorMessage = errorMessage.substring(BEGINNING_OF_ADB_ERROR_MESSAGE.length); 1188d307f52SEvan Bacon } 11929975bfdSEvan Bacon error.message = errorMessage; 12029975bfdSEvan Bacon throw error; 1218d307f52SEvan Bacon } 1228d307f52SEvan Bacon } 1238d307f52SEvan Bacon} 124