format import EventEmitter from 'events' ; import ServerController from './comms/ServerController' ; import { CertificateExchangeMedium } from './utils/CertificateProvider' ; import { AndroidDeviceManager } from './devices/android/androidDeviceManager' ; import { IOSDeviceManager , launchSimulator , } from './devices/ios/iOSDeviceManager' ; import metroDevice from './devices/metro/metroDeviceManager' ; import desktopDevice from './devices/desktop/desktopDeviceManager' ; import { FlipperServerEvents , FlipperServerState , FlipperServerCommands , FlipperServer , UninitializedClient , FlipperServerConfig , Logger , } from 'flipper-common' ; import { ServerDevice } from './devices/ServerDevice' ; import { Base64 } from 'js-base64' ; import MetroDevice from './devices/metro/MetroDevice' ; import { launchEmulator } from './devices/android/AndroidDevice' ; import { setFlipperServerConfig } from './FlipperServerConfig' ; import { saveSettings } from './utils/settings' ; import { saveLauncherSettings } from './utils/launcherSettings' ; import { KeytarManager , KeytarModule } from './utils/keytar' ; import { PluginManager } from './plugins/PluginManager' ; import { runHealthcheck , getHealthChecks } from './utils/runHealthchecks' ; import { openFile } from './utils/openFile' ; import { getChangelog } from './utils/pathUtils' ; import { sendScribeLogs } from './fb-stubs/sendScribeLogs' ; import { internGraphGETAPIRequest , internGraphPOSTAPIRequest , } from './fb-stubs/internRequests' ; import { commandNodeApiExec } from './commands/NodeApiExec' ; import { commandDownloadFileStartFactory } from './commands/DownloadFile' ; import { promises } from 'fs' ; import rm from 'rimraf' ; import assert from 'assert' ; const { access , copyFile , mkdir , unlink , stat , readlink , readFile , writeFile } = promises ; export const SERVICE_FLIPPER = 'flipper.oAuthToken' ; /**
 * FlipperServer takes care of all incoming device & client connections.
 * It will set up managers per device type, and create the incoming
 * RSocket/WebSocket server to handle incoming client connections.
 *
 * The server should be largely treated as event emitter, by listening to the relevant events
 * using '.on'. All events are strongly typed.
 */ export class FlipperServerImpl implements FlipperServer { private readonly events = new EventEmitter ( ) ; readonly server : ServerController ; readonly disposers : ( ( ( ) => void ) | void ) [ ] = [ ] ; private readonly devices = new Map < string , ServerDevice > ( ) ; state : FlipperServerState = 'pending' ; stateError : string | undefined = undefined ; android : AndroidDeviceManager ; ios : IOSDeviceManager ; keytarManager : KeytarManager ; pluginManager : PluginManager ; constructor ( public config , public logger , keytarModule ? ) { setFlipperServerConfig ( config ) ; console . log ( 'Loaded flipper config, paths: ' + JSON . stringify ( config . paths , null , 2 ) ) ; const server = ( this . server = new ServerController ( this ) ) ; this . android = new AndroidDeviceManager ( this ) ; this . ios = new IOSDeviceManager ( this ) ; this . keytarManager = new KeytarManager ( keytarModule ) ; this . pluginManager = new PluginManager ( ) ; server . addListener ( 'error' , ( err ) => { this . emit ( 'server-error' , err ) ; } ) ; server . addListener ( 'start-client-setup' , ( client ) => { this . emit ( 'client-setup' , client ) ; } ) ; server . addListener ( 'client-setup-error' , ( { client , error } ) => { this . emit ( 'notification' , { title : `Connection to '${ client . appName }' on '${ client . deviceName }' failed` , description : `Failed to start client connection: ${ error }` , type : 'error' , } ) ; } ) ; server . addListener ( 'client-unresponsive-error' , ( { client , medium , } ) => { this . emit ( 'notification' , { type : 'error' , title : `Timed out establishing connection with "${ client . appName }" on "${ client . deviceName }".` , description : medium === 'WWW' ? `Verify that both your computer and mobile device are on Lighthouse/VPN that you are logged in to Facebook Intern so that certificates can be exhanged. See: https://fburl.com/flippervpn` : 'Verify that your client is connected to Flipper and that there is no error related to idb or adb.' , } ) ; } ) ; } setServerState ( state , error ? ) { this . state = state ; this . stateError = '' + error ; this . emit ( 'server-state' , { state , error : this . stateError } ) ; } /**
     * Starts listening to parts and watching for devices.
     * Connect never throws directly, but communicates
     * through server-state events
     */ async connect ( ) { if ( this . state !== 'pending' ) { throw new Error ( 'Server already started' ) ; } this . setServerState ( 'starting' ) ; try { await this . server . init ( ) ; await this . pluginManager . start ( ) ; await this . startDeviceListeners ( ) ; this . setServerState ( 'started' ) ; } catch ( e ) { console . error ( 'Failed to start FlipperServer' , e ) ; this . setServerState ( 'error' , e ) ; } } async startDeviceListeners ( ) { this . disposers . push ( await this . android . watchAndroidDevices ( ) , await this . ios . watchIOSDevices ( ) , metroDevice ( this ) , desktopDevice ( this ) ) ; } on < Event extends keyof FlipperServerEvents > ( event , callback ) { this . events . on ( event , callback ) ; } off < Event extends keyof FlipperServerEvents > ( event , callback ) { this . events . off ( event , callback ) ; } onAny ( callback ) { this . events . on ( '*' , callback ) ; } offAny ( callback ) { this . events . off ( '*' , callback ) ; } internal emit < Event extends keyof FlipperServerEvents > ( event , payload ) { this . events . emit ( event , payload ) ; this . events . emit ( '*' , event , payload ) ; } exec < Event extends keyof FlipperServerCommands > ( event , ... args ) ; async exec < Event extends keyof FlipperServerCommands > ( event , ... args ) { try { const handler = this . commandHandler [ event ] ; if ( ! handler ) { throw new Error ( `Unimplemented server command: ${ event }` ) ; } const result = await handler ( ... args ) ; console . debug ( `[FlipperServer] command '${ event }' - OK` ) ; return result ; } catch ( e ) { console . debug ( `[FlipperServer] command '${ event }' - ERROR: ${ e } ` ) ; throw e ; } } private commandHandler : FlipperServerCommands = { 'get-server-state' : async ( ) => ( { state : this . state , error : this . stateError , } ) , 'node-api-exec' : commandNodeApiExec , 'node-api-fs-access' : access , 'node-api-fs-pathExists' : async ( path , mode ) => { try { await access ( path , mode ) ; return true ; } catch { return false ; } } , 'node-api-fs-unlink' : unlink , 'node-api-fs-mkdir' : mkdir , 'node-api-fs-rm' : async ( path , { maxRetries } = { } ) => new Promise < void > ( ( resolve , reject ) => rm ( path , { disableGlob : true , maxBusyTries : maxRetries } , ( err ) => err ? reject ( err ) : resolve ( ) ) ) , 'node-api-fs-copyFile' : copyFile , 'node-api-fs-stat' : async ( path ) => { const stats = await stat ( path ) ; const { atimeMs , birthtimeMs , ctimeMs , gid , mode , mtimeMs , size , uid } = stats ; return { atimeMs , birthtimeMs , ctimeMs , gid , mode , mtimeMs , size , uid , isDirectory : stats . isDirectory ( ) , isFile : stats . isFile ( ) , isSymbolicLink : stats . isSymbolicLink ( ) , } ; } , 'node-api-fs-readlink' : readlink , 'node-api-fs-readfile' : async ( path , options ) => { const contents = await readFile ( path , options ?? 'utf8' ) ; assert ( typeof contents === 'string' , `File ${ path } was not read with a string encoding` ) ; return contents ; } , 'node-api-fs-readfile-binary' : async ( path ) => { const contents = await readFile ( path ) ; return Base64 . fromUint8Array ( contents ) ; } , 'node-api-fs-writefile' : ( path , contents , options ) => writeFile ( path , contents , options ?? 'utf8' ) , 'node-api-fs-writefile-binary' : ( path , base64contents ) => writeFile ( path , Base64 . toUint8Array ( base64contents ) , 'binary' ) , 'download-file-start' : commandDownloadFileStartFactory ( this . emit . bind ( this ) ) , 'get-config' : async ( ) => this . config , 'get-changelog' : getChangelog , 'device-list' : async ( ) => { return Array . from ( this . devices . values ( ) ) . map ( ( d ) => d . info ) ; } , 'device-supports-screenshot' : async ( serial ) => this . getDevice ( serial ) . screenshotAvailable ( ) , 'device-supports-screencapture' : async ( serial ) => this . getDevice ( serial ) . screenCaptureAvailable ( ) , 'device-take-screenshot' : async ( serial ) => Base64 . fromUint8Array ( await this . getDevice ( serial ) . screenshot ( ) ) , 'device-start-screencapture' : async ( serial , destination ) => this . getDevice ( serial ) . startScreenCapture ( destination ) , 'device-stop-screencapture' : async ( serial ) => this . getDevice ( serial ) . stopScreenCapture ( ) , 'device-shell-exec' : async ( serial , command ) => this . getDevice ( serial ) . executeShell ( command ) , 'device-forward-port' : async ( serial , local , remote ) => this . getDevice ( serial ) . forwardPort ( local , remote ) , 'device-clear-logs' : async ( serial ) => this . getDevice ( serial ) . clearLogs ( ) , 'device-navigate' : async ( serial , loc ) => this . getDevice ( serial ) . navigateToLocation ( loc ) , 'metro-command' : async ( serial , command ) => { const device = this . getDevice ( serial ) ; if ( ! ( device instanceof MetroDevice ) ) { throw new Error ( 'Not a Metro device: ' + serial ) ; } device . sendCommand ( command ) ; } , 'client-list' : async ( ) => { return Array . from ( this . server . connections . values ( ) ) . map ( ( c ) => c . client ) ; } , 'client-request' : async ( clientId , payload ) => { this . server . connections . get ( clientId ) ?. connection ?. send ( payload ) ; } , 'client-request-response' : async ( clientId , payload ) => { const client = this . server . connections . get ( clientId ) ; if ( client && client . connection ) { return await client . connection . sendExpectResponse ( payload ) ; } return { length : 0 , error : { message : `Client '${ clientId } is no longer connected, failed to deliver: ${ JSON . stringify ( payload ) }` , name : 'CLIENT_DISCONNECTED' , stacktrace : '' , } , } ; } , 'android-get-emulators' : async ( ) => this . android . getAndroidEmulators ( ) , 'android-launch-emulator' : async ( name , coldBoot ) => launchEmulator ( this . config . settings . androidHome , name , coldBoot ) , 'ios-get-simulators' : async ( bootedOnly ) => this . ios . getSimulators ( bootedOnly ) , 'ios-launch-simulator' : async ( udid ) => launchSimulator ( udid ) , 'persist-settings' : async ( settings ) => saveSettings ( settings ) , 'persist-launcher-settings' : async ( settings ) => saveLauncherSettings ( settings ) , 'keychain-read' : ( service ) => this . keytarManager . retrieveToken ( service ) , 'keychain-write' : ( service , password ) => this . keytarManager . writeKeychain ( service , password ) , 'keychain-unset' : ( service ) => this . keytarManager . unsetKeychain ( service ) , 'plugins-load-dynamic-plugins' : ( ) => this . pluginManager . loadDynamicPlugins ( ) , 'plugins-get-bundled-plugins' : ( ) => this . pluginManager . getBundledPlugins ( ) , 'plugins-get-installed-plugins' : ( ) => this . pluginManager . getInstalledPlugins ( ) , 'plugins-remove-plugins' : ( plugins ) => this . pluginManager . removePlugins ( plugins ) , 'plugin-start-download' : ( details ) => this . pluginManager . downloadPlugin ( details ) , 'plugins-get-updatable-plugins' : ( query ) => this . pluginManager . getUpdatablePlugins ( query ) , 'plugins-install-from-file' : ( path ) => this . pluginManager . installPluginFromFile ( path ) , 'plugins-install-from-npm' : ( name ) => this . pluginManager . installPluginFromNpm ( name ) , 'plugin-source' : ( path ) => this . pluginManager . loadSource ( path ) , 'doctor-get-healthchecks' : getHealthChecks , 'doctor-run-healthcheck' : runHealthcheck , 'open-file' : openFile , 'intern-graph-post' : async ( endpoint , formfields , filefields , options ) => { const token = await this . keytarManager . retrieveToken ( SERVICE_FLIPPER ) ; return internGraphPOSTAPIRequest ( endpoint , formfields , filefields , options , token ) ; } , 'intern-graph-get' : async ( endpoint , params , options ) => { const token = await this . keytarManager . retrieveToken ( SERVICE_FLIPPER ) ; return internGraphGETAPIRequest ( endpoint , params , options , token ) ; } , 'intern-upload-scribe-logs' : sendScribeLogs , } ; registerDevice ( device ) { const { serial } = device . info ; const existing = this . devices . get ( serial ) ; if ( existing ) { if ( Object . getPrototypeOf ( existing ) !== Object . getPrototypeOf ( device ) ) { throw new Error ( `Tried to register a new device type for existing serial '${ serial }': Trying to replace existing '${ Object . getPrototypeOf ( existing ) . constructor . name }' with a new '${ Object . getPrototypeOf ( device ) . constructor . name }` ) ; } existing . disconnect ( ) ; } this . devices . set ( device . info . serial , device ) ; this . emit ( 'device-connected' , device . info ) ; } unregisterDevice ( serial ) { const device = this . devices . get ( serial ) ; if ( ! device ) { return ; } this . devices . delete ( serial ) ; device . disconnect ( ) ; this . emit ( 'device-disconnected' , device . info ) ; } getDevice ( serial ) { const device = this . devices . get ( serial ) ; if ( ! device ) { throw new Error ( 'No device with serial: ' + serial ) ; } return device ; } getDeviceSerials ( ) { return Array . from ( this . devices . keys ( ) ) ; } getDevices ( ) { return Array . from ( this . devices . values ( ) ) ; } public async close ( ) { this . server . close ( ) ; for ( const device of this . devices . values ( ) ) { device . disconnect ( ) ; } this . disposers . forEach ( ( f ) => f ?. ( ) ) ; this . setServerState ( 'closed' ) ; } }