From 03f2f95a3171a7bdf849d8d12ae7e155c1bb5f04 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 17 Aug 2021 04:43:18 -0700 Subject: [PATCH] Decouple android device management from Flipper core/store Summary: See earlier diffs in the stack. This diff decouple android device management from the Redux store, replacing it with specific events. Reviewed By: timur-valiev Differential Revision: D30286345 fbshipit-source-id: 42f52056bf123b862e2fc087f2e7130c02bdd742 --- .../createMockFlipperWithPlugin.node.tsx.snap | 2 +- desktop/app/src/dispatcher/flipperServer.tsx | 31 +- desktop/app/src/reducers/connections.tsx | 35 +- .../appinspect/LaunchEmulator.tsx | 45 ++- .../__tests__/LaunchEmulator.spec.tsx | 36 +- desktop/app/src/server/FlipperServer.tsx | 27 +- .../devices/android/androidDeviceManager.tsx | 363 ++++++++---------- 7 files changed, 293 insertions(+), 246 deletions(-) diff --git a/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap b/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap index 938ee03a8..952f76355 100644 --- a/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap +++ b/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap @@ -2,7 +2,6 @@ exports[`can create a Fake flipper with legacy wrapper 1`] = ` Object { - "androidEmulators": Array [], "clients": Array [ Object { "id": "TestApp#Android#MockAndroidDevice#serial", @@ -36,6 +35,7 @@ Object { "TestPlugin", ], }, + "flipperServer": undefined, "selectedApp": "TestApp#Android#MockAndroidDevice#serial", "selectedAppPluginListRevision": 0, "selectedDevice": Object { diff --git a/desktop/app/src/dispatcher/flipperServer.tsx b/desktop/app/src/dispatcher/flipperServer.tsx index b1b98295d..e6b13aa66 100644 --- a/desktop/app/src/dispatcher/flipperServer.tsx +++ b/desktop/app/src/dispatcher/flipperServer.tsx @@ -16,15 +16,30 @@ import Client from '../Client'; import {notification} from 'antd'; export default async (store: Store, logger: Logger) => { - const {enableAndroid} = store.getState().settingsState; + const {enableAndroid, androidHome} = store.getState().settingsState; const server = await startFlipperServer( { enableAndroid, + androidHome, + serverPorts: store.getState().application.serverPorts, }, store, logger, ); + store.dispatch({ + type: 'SET_FLIPPER_SERVER', + payload: server, + }); + + server.on('notification', (notif) => { + notification.open({ + message: notif.title, + description: notif.description, + type: notif.type, + }); + }); + server.on('server-start-error', (err) => { notification.error({ message: 'Failed to start connection server', @@ -50,6 +65,12 @@ export default async (store: Store, logger: Logger) => { }); server.on('device-connected', (device) => { + logger.track('usage', 'register-device', { + os: 'Android', + name: device.title, + serial: device.serial, + }); + device.loadDevicePlugins( store.getState().plugins.devicePlugins, store.getState().connections.enabledDevicePlugins, @@ -61,6 +82,14 @@ export default async (store: Store, logger: Logger) => { }); }); + server.on('device-disconnected', (device) => { + logger.track('usage', 'unregister-device', { + os: device.os, + serial: device.serial, + }); + // N.B.: note that we don't remove the device, we keep it in offline mode! + }); + server.on('client-connected', (payload) => handleClientConnected(store, payload), ); diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index 2dc4766a9..f44efcf05 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -25,6 +25,7 @@ import {deconstructClientId} from '../utils/clientUtils'; import type {RegisterPluginAction} from './plugins'; import MetroDevice from '../server/devices/metro/MetroDevice'; import {Logger} from 'flipper-plugin'; +import {FlipperServer} from '../server/FlipperServer'; export type StaticViewProps = {logger: Logger}; @@ -63,7 +64,6 @@ export const persistMigrations = { type StateV2 = { devices: Array; - androidEmulators: Array; selectedDevice: null | BaseDevice; selectedPlugin: null | string; selectedApp: null | string; @@ -81,6 +81,7 @@ type StateV2 = { deepLinkPayload: unknown; staticView: StaticView; selectedAppPluginListRevision: number; + flipperServer: FlipperServer | undefined; }; type StateV1 = Omit & { @@ -102,10 +103,6 @@ export type Action = type: 'REGISTER_DEVICE'; payload: BaseDevice; } - | { - type: 'REGISTER_ANDROID_EMULATORS'; - payload: Array; - } | { type: 'SELECT_DEVICE'; payload: BaseDevice; @@ -182,13 +179,16 @@ export type Action = | { type: 'APP_PLUGIN_LIST_CHANGED'; } + | { + type: 'SET_FLIPPER_SERVER'; + payload: FlipperServer; + } | RegisterPluginAction; const DEFAULT_PLUGIN = 'DeviceLogs'; const DEFAULT_DEVICE_BLACKLIST = [MacDevice, MetroDevice]; const INITAL_STATE: State = { devices: [], - androidEmulators: [], selectedDevice: null, selectedApp: null, selectedPlugin: DEFAULT_PLUGIN, @@ -208,10 +208,18 @@ const INITAL_STATE: State = { deepLinkPayload: null, staticView: WelcomeScreenStaticView, selectedAppPluginListRevision: 0, + flipperServer: undefined, }; export default (state: State = INITAL_STATE, action: Actions): State => { switch (action.type) { + case 'SET_FLIPPER_SERVER': { + return { + ...state, + flipperServer: action.payload, + }; + } + case 'SET_STATIC_VIEW': { const {payload, deepLinkPayload} = action; const {selectedPlugin} = state; @@ -241,13 +249,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => { : state.userPreferredDevice, }); } - case 'REGISTER_ANDROID_EMULATORS': { - const {payload} = action; - return { - ...state, - androidEmulators: payload, - }; - } + case 'REGISTER_DEVICE': { const {payload} = action; @@ -256,9 +258,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => { (device) => device.serial === payload.serial, ); if (existing !== -1) { - console.warn( - `Got a new device instance for already existing serial ${payload.serial}`, - ); + newDevices[existing].destroy(); newDevices[existing] = payload; } else { newDevices.push(payload); @@ -270,6 +270,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => { }); } + // TODO: remove case 'UNREGISTER_DEVICES': { const deviceSerials = action.payload; @@ -290,6 +291,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => { }), ); } + case 'SELECT_PLUGIN': { const {payload} = action; const {selectedPlugin, selectedApp, deepLinkPayload} = payload; @@ -687,6 +689,7 @@ export function isPluginEnabled( return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1; } +// TODO: remove! export function destroyDevice(store: Store, logger: Logger, serial: string) { const device = store .getState() diff --git a/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx b/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx index 2690d9442..cbb5604c3 100644 --- a/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx @@ -33,31 +33,41 @@ const COLD_BOOT = 'cold-boot'; export function showEmulatorLauncher(store: Store) { renderReactRoot((unmount) => ( - + )); } +function LaunchEmulatorContainer({onClose}: {onClose: () => void}) { + const store = useStore(); + const flipperServer = useStore((state) => state.connections.flipperServer); + return ( + flipperServer!.android.getAndroidEmulators()} + /> + ); +} + type GetSimulators = typeof getSimulators; export const LaunchEmulatorDialog = withTrackingScope( function LaunchEmulatorDialog({ onClose, getSimulators, + getEmulators, }: { onClose: () => void; getSimulators: GetSimulators; + getEmulators: () => Promise; }) { const iosEnabled = useStore((state) => state.settingsState.enableIOS); - const androidEmulators = useStore((state) => - state.settingsState.enableAndroid - ? state.connections.androidEmulators - : [], + const androidEnabled = useStore( + (state) => state.settingsState.enableAndroid, ); const [iosEmulators, setIosEmulators] = useState([]); + const [androidEmulators, setAndroidEmulators] = useState([]); const store = useStore(); useEffect(() => { @@ -75,6 +85,19 @@ export const LaunchEmulatorDialog = withTrackingScope( }); }, [iosEnabled, getSimulators, store]); + useEffect(() => { + if (!androidEnabled) { + return; + } + getEmulators() + .then((emulators) => { + setAndroidEmulators(emulators); + }) + .catch((e) => { + console.warn('Failed to find emulatiors', e); + }); + }, [androidEnabled, getEmulators]); + const items = [ ...(androidEmulators.length > 0 ? [] @@ -82,11 +105,11 @@ export const LaunchEmulatorDialog = withTrackingScope( ...androidEmulators.map((name) => { const launch = (coldBoot: boolean) => { launchEmulator(name, coldBoot) + .then(onClose) .catch((e) => { - console.error(e); + console.error('Failed to start emulator: ', e); message.error('Failed to start emulator: ' + e); - }) - .then(onClose); + }); }; const menu = ( ({ launchEmulator: jest.fn(() => Promise.resolve([])), })); -import {launchEmulator} from '../../../server/devices/android/AndroidDevice'; - -test('Can render and launch android apps', async () => { +test('Can render and launch android apps - empty', async () => { const store = createStore(createRootReducer()); const onClose = jest.fn(); @@ -32,6 +30,7 @@ test('Can render and launch android apps', async () => { Promise.resolve([])} + getEmulators={() => Promise.resolve([])} /> , ); @@ -43,13 +42,32 @@ test('Can render and launch android apps', async () => { No emulators available `); +}); - act(() => { - store.dispatch({ - type: 'REGISTER_ANDROID_EMULATORS', - payload: ['emulator1', 'emulator2'], - }); +test('Can render and launch android apps', async () => { + const store = createStore(createRootReducer()); + store.dispatch({ + type: 'UPDATE_SETTINGS', + payload: { + ...store.getState().settingsState, + enableAndroid: true, + }, }); + const onClose = jest.fn(); + + let p: Promise | undefined = undefined; + + const renderer = render( + + Promise.resolve([])} + getEmulators={() => (p = Promise.resolve(['emulator1', 'emulator2']))} + /> + , + ); + + await p!; expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(` Array [ diff --git a/desktop/app/src/server/FlipperServer.tsx b/desktop/app/src/server/FlipperServer.tsx index 0d0190cbd..142cfd3b4 100644 --- a/desktop/app/src/server/FlipperServer.tsx +++ b/desktop/app/src/server/FlipperServer.tsx @@ -18,21 +18,33 @@ import {CertificateExchangeMedium} from '../server/utils/CertificateProvider'; import {isLoggedIn} from '../fb-stubs/user'; import React from 'react'; import {Typography} from 'antd'; -import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application'; -import androidDevice from './devices/android/androidDeviceManager'; +import { + ACTIVE_SHEET_SIGN_IN, + ServerPorts, + setActiveSheet, +} from '../reducers/application'; +import {AndroidDeviceManager} from './devices/android/androidDeviceManager'; import iOSDevice from './devices/ios/iOSDeviceManager'; import metroDevice from './devices/metro/metroDeviceManager'; import desktopDevice from './devices/desktop/desktopDeviceManager'; import BaseDevice from './devices/BaseDevice'; type FlipperServerEvents = { - 'device-connected': BaseDevice; - 'client-connected': Client; 'server-start-error': any; + notification: { + type: 'error'; + title: string; + description: string; + }; + 'device-connected': BaseDevice; + 'device-disconnected': BaseDevice; + 'client-connected': Client; }; export interface FlipperServerConfig { enableAndroid: boolean; + androidHome: string; + serverPorts: ServerPorts; } export async function startFlipperServer( @@ -59,6 +71,8 @@ export class FlipperServer { readonly server: ServerController; readonly disposers: ((() => void) | void)[] = []; + android: AndroidDeviceManager; + // TODO: remove store argument constructor( public config: FlipperServerConfig, @@ -67,6 +81,7 @@ export class FlipperServer { public logger: Logger, ) { this.server = new ServerController(logger, store); + this.android = new AndroidDeviceManager(this); } /** @private */ @@ -160,10 +175,8 @@ export class FlipperServer { } async startDeviceListeners() { - if (this.config.enableAndroid) { - this.disposers.push(await androidDevice(this.store, this.logger)); - } this.disposers.push( + await this.android.watchAndroidDevices(), iOSDevice(this.store, this.logger), metroDevice(this.store, this.logger), desktopDevice(this), diff --git a/desktop/app/src/server/devices/android/androidDeviceManager.tsx b/desktop/app/src/server/devices/android/androidDeviceManager.tsx index a18b9b154..82f628ca5 100644 --- a/desktop/app/src/server/devices/android/androidDeviceManager.tsx +++ b/desktop/app/src/server/devices/android/androidDeviceManager.tsx @@ -10,40 +10,41 @@ import AndroidDevice from './AndroidDevice'; import KaiOSDevice from './KaiOSDevice'; import child_process from 'child_process'; -import {Store} from '../../../reducers/index'; import BaseDevice from '../BaseDevice'; -import {Logger} from '../../../fb-interfaces/Logger'; import {getAdbClient} from './adbClient'; import which from 'which'; import {promisify} from 'util'; -import {ServerPorts} from '../../../reducers/application'; import {Client as ADBClient} from 'adbkit'; -import {addErrorNotification} from '../../../reducers/notifications'; -import {destroyDevice} from '../../../reducers/connections'; import {join} from 'path'; +import {FlipperServer} from '../../FlipperServer'; +import {notNull} from '../../utils/typeUtils'; -function createDevice( - adbClient: ADBClient, - device: any, - store: Store, - ports?: ServerPorts, -): Promise { - return new Promise((resolve, reject) => { - const type = - device.type !== 'device' || device.id.startsWith('emulator') - ? 'emulator' - : 'physical'; +export class AndroidDeviceManager { + // cache emulator path + private emulatorPath: string | undefined; + private devices: Map = new Map(); - adbClient - .getProperties(device.id) - .then(async (props) => { + constructor(public flipperServer: FlipperServer) {} + + createDevice( + adbClient: ADBClient, + device: any, + ): Promise { + return new Promise(async (resolve, reject) => { + const type = + device.type !== 'device' || device.id.startsWith('emulator') + ? 'emulator' + : 'physical'; + + try { + const props = await adbClient.getProperties(device.id); try { let name = props['ro.product.model']; const abiString = props['ro.product.cpu.abilist'] || ''; const sdkVersion = props['ro.build.version.sdk'] || ''; const abiList = abiString.length > 0 ? abiString.split(',') : []; if (type === 'emulator') { - name = (await getRunningEmulatorName(device.id)) || name; + name = (await this.getRunningEmulatorName(device.id)) || name; } const isKaiOSDevice = Object.keys(props).some( (name) => name.startsWith('kaios') || name.startsWith('ro.kaios'), @@ -51,9 +52,12 @@ function createDevice( const androidLikeDevice = new ( isKaiOSDevice ? KaiOSDevice : AndroidDevice )(device.id, type, name, adbClient, abiList, sdkVersion); - if (ports) { + if (this.flipperServer.config.serverPorts) { await androidLikeDevice - .reverse([ports.secure, ports.insecure]) + .reverse([ + this.flipperServer.config.serverPorts.secure, + this.flipperServer.config.serverPorts.insecure, + ]) // We may not be able to establish a reverse connection, e.g. for old Android SDKs. // This is *generally* fine, because we hard-code the ports on the SDK side. .catch((e) => { @@ -75,8 +79,7 @@ function createDevice( } catch (e) { reject(e); } - }) - .catch((e) => { + } catch (e) { if ( e && e.message && @@ -87,202 +90,160 @@ function createDevice( const isAuthorizationError = (e?.message as string)?.includes( 'device unauthorized', ); - store.dispatch( - addErrorNotification( - 'Could not connect to ' + device.id, - isAuthorizationError - ? 'Make sure to authorize debugging on the phone' - : 'Failed to setup connection', - e, - ), - ); + if (!isAuthorizationError) { + console.error('Failed to connect to android device', e); + } + this.flipperServer.emit('notification', { + type: 'error', + title: 'Could not connect to ' + device.id, + description: isAuthorizationError + ? 'Make sure to authorize debugging on the phone' + : 'Failed to setup connection: ' + e, + }); } resolve(undefined); // not ready yet, we will find it in the next tick - }); - }); -} + } + }); + } -export async function getActiveAndroidDevices( - store: Store, -): Promise> { - const client = await getAdbClient(store.getState().settingsState); - const androidDevices = await client.listDevices(); - const devices = await Promise.all( - androidDevices.map((device) => createDevice(client, device, store)), - ); - return devices.filter(Boolean) as any; -} - -function getRunningEmulatorName( - id: string, -): Promise { - return new Promise((resolve, reject) => { - const port = id.replace('emulator-', ''); - // The GNU version of netcat doesn't terminate after 1s when - // specifying `-w 1`, so we kill it after a timeout. Because - // of that, even in case of an error, there may still be - // relevant data for us to parse. - child_process.exec( - `echo "avd name" | nc -w 1 localhost ${port}`, - {timeout: 1000, encoding: 'utf-8'}, - (error: Error | null | undefined, data) => { - if (data != null && typeof data === 'string') { - const match = data.trim().match(/(.*)\r\nOK$/); - resolve(match != null && match.length > 0 ? match[1] : null); - } else { - reject(error); - } - }, + async getActiveAndroidDevices(): Promise> { + const client = await getAdbClient(this.flipperServer.config); + const androidDevices = await client.listDevices(); + const devices = await Promise.all( + androidDevices.map((device) => this.createDevice(client, device)), ); - }); -} + return devices.filter(Boolean) as any; + } -export default (store: Store, logger: Logger) => { - const watchAndroidDevices = () => { - // get emulators - promisify(which)('emulator') - .catch(() => - join( - process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || '', - 'tools', - 'emulator', - ), - ) - .then((emulatorPath) => { - child_process.execFile( - emulatorPath as string, - ['-list-avds'], - (error: Error | null, data: string | null) => { - if (error != null || data == null) { - console.warn('List AVD failed: ', error); - return; - } - const payload = data.split('\n').filter(Boolean); - store.dispatch({ - type: 'REGISTER_ANDROID_EMULATORS', - payload, - }); - }, - ); - }) - .catch((err) => { - console.warn('Failed to query AVDs:', err); - }); + async getEmulatorPath(): Promise { + if (this.emulatorPath) { + return this.emulatorPath; + } + // TODO: this doesn't respect the currently configured android_home in settings! + try { + this.emulatorPath = (await promisify(which)('emulator')) as string; + } catch (_e) { + this.emulatorPath = join( + process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || '', + 'tools', + 'emulator', + ); + } + return this.emulatorPath; + } - return getAdbClient(store.getState().settingsState) - .then((client) => { - client - .trackDevices() - .then((tracker) => { - tracker.on('error', (err) => { - if (err.message === 'Connection closed') { - // adb server has shutdown, remove all android devices - const {connections} = store.getState(); - const deviceIDsToRemove: Array = connections.devices - .filter( - (device: BaseDevice) => device instanceof AndroidDevice, - ) - .map((device: BaseDevice) => device.serial); + async getAndroidEmulators(): Promise { + const emulatorPath = await this.getEmulatorPath(); + return new Promise((resolve) => { + child_process.execFile( + emulatorPath as string, + ['-list-avds'], + (error: Error | null, data: string | null) => { + if (error != null || data == null) { + console.warn('List AVD failed: ', error); + resolve([]); + return; + } + const devices = data + .split('\n') + .filter(notNull) + .filter((l) => l !== ''); + resolve(devices); + }, + ); + }); + } - unregisterDevices(deviceIDsToRemove); - console.warn('adb server was shutdown'); - setTimeout(watchAndroidDevices, 500); - } else { - throw err; - } - }); + async getRunningEmulatorName(id: string): Promise { + return new Promise((resolve, reject) => { + const port = id.replace('emulator-', ''); + // The GNU version of netcat doesn't terminate after 1s when + // specifying `-w 1`, so we kill it after a timeout. Because + // of that, even in case of an error, there may still be + // relevant data for us to parse. + child_process.exec( + `echo "avd name" | nc -w 1 localhost ${port}`, + {timeout: 1000, encoding: 'utf-8'}, + (error: Error | null | undefined, data) => { + if (data != null && typeof data === 'string') { + const match = data.trim().match(/(.*)\r\nOK$/); + resolve(match != null && match.length > 0 ? match[1] : null); + } else { + reject(error); + } + }, + ); + }); + } - tracker.on('add', async (device) => { - if (device.type !== 'offline') { - registerDevice(client, device, store); - } - }); - - tracker.on('change', async (device) => { - if (device.type === 'offline') { - unregisterDevices([device.id]); - } else { - registerDevice(client, device, store); - } - }); - - tracker.on('remove', (device) => { - unregisterDevices([device.id]); - }); - }) - .catch((err: {code: string}) => { - if (err.code === 'ECONNREFUSED') { - console.warn('adb server not running'); + async watchAndroidDevices() { + try { + const client = await getAdbClient(this.flipperServer.config); + client + .trackDevices() + .then((tracker) => { + tracker.on('error', (err) => { + if (err.message === 'Connection closed') { + this.unregisterDevices(Array.from(this.devices.keys())); + console.warn('adb server was shutdown'); + setTimeout(() => { + this.watchAndroidDevices(); + }, 500); } else { throw err; } }); - }) - .catch((e) => { - console.warn(`Failed to watch for android devices: ${e.message}`); - }); - }; - async function registerDevice(adbClient: any, deviceData: any, store: Store) { - const androidDevice = await createDevice( - adbClient, - deviceData, - store, - store.getState().application.serverPorts, - ); + tracker.on('add', async (device) => { + if (device.type !== 'offline') { + this.registerDevice(client, device); + } + }); + + tracker.on('change', async (device) => { + if (device.type === 'offline') { + this.unregisterDevices([device.id]); + } else { + this.registerDevice(client, device); + } + }); + + tracker.on('remove', (device) => { + this.unregisterDevices([device.id]); + }); + }) + .catch((err: {code: string}) => { + if (err.code === 'ECONNREFUSED') { + console.warn('adb server not running'); + } else { + throw err; + } + }); + } catch (e) { + console.warn(`Failed to watch for android devices: ${e.message}`); + } + } + + async registerDevice(adbClient: ADBClient, deviceData: any) { + const androidDevice = await this.createDevice(adbClient, deviceData); if (!androidDevice) { return; } - logger.track('usage', 'register-device', { - os: 'Android', - name: androidDevice.title, - serial: androidDevice.serial, - }); // remove offline devices with same serial as the connected. - const reconnectedDevices = store - .getState() - .connections.devices.filter( - (device: BaseDevice) => - device.serial === androidDevice.serial && !device.connected.get(), - ) - .map((device) => device.serial); - - reconnectedDevices.forEach((serial) => { - destroyDevice(store, logger, serial); - }); - - androidDevice.loadDevicePlugins( - store.getState().plugins.devicePlugins, - store.getState().connections.enabledDevicePlugins, - ); - store.dispatch({ - type: 'REGISTER_DEVICE', - payload: androidDevice, - }); + this.devices.get(androidDevice.serial)?.destroy(); + // register new device + this.devices.set(androidDevice.serial, androidDevice); + this.flipperServer.emit('device-connected', androidDevice); } - async function unregisterDevices(deviceIds: Array) { - deviceIds.forEach((id) => - logger.track('usage', 'unregister-device', { - os: 'Android', - serial: id, - }), - ); - - deviceIds.forEach((id) => { - const device = store - .getState() - .connections.devices.find((device) => device.serial === id); - device?.disconnect(); + unregisterDevices(serials: Array) { + serials.forEach((serial) => { + const device = this.devices.get(serial); + if (device?.connected?.get()) { + device.disconnect(); + this.flipperServer.emit('device-disconnected', device); + } }); } - - watchAndroidDevices(); - - // cleanup method - return () => - getAdbClient(store.getState().settingsState).then((client) => { - client.kill(); - }); -}; +}