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