diff --git a/desktop/app/src/__tests__/disconnect.node.tsx b/desktop/app/src/__tests__/disconnect.node.tsx index a03b96cc7..c3a42bb88 100644 --- a/desktop/app/src/__tests__/disconnect.node.tsx +++ b/desktop/app/src/__tests__/disconnect.node.tsx @@ -16,7 +16,7 @@ import { DevicePluginClient, PluginClient, } from 'flipper-plugin'; -import {registerNewClient} from '../server/server'; +import {handleClientConnected} from '../dispatcher/flipperServer'; import {destroyDevice} from '../reducers/connections'; test('Devices can disconnect', async () => { @@ -225,7 +225,7 @@ test('new clients replace old ones', async () => { expect(instance.instanceApi.disconnect).toBeCalledTimes(0); const client2 = await createClient(device, 'AnotherApp', client.query, true); - registerNewClient(store, client2); + handleClientConnected(store, client2); expect(client2.connected.get()).toBe(true); const instance2 = client2.sandyPluginStates.get(plugin.id)!; diff --git a/desktop/app/src/dispatcher/flipperServer.tsx b/desktop/app/src/dispatcher/flipperServer.tsx new file mode 100644 index 000000000..b1b98295d --- /dev/null +++ b/desktop/app/src/dispatcher/flipperServer.tsx @@ -0,0 +1,117 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import React from 'react'; +import {Store} from '../reducers/index'; +import {Logger} from '../fb-interfaces/Logger'; +import {startFlipperServer} from '../server/FlipperServer'; +import {selectClient, selectDevice} from '../reducers/connections'; +import Client from '../Client'; +import {notification} from 'antd'; + +export default async (store: Store, logger: Logger) => { + const {enableAndroid} = store.getState().settingsState; + const server = await startFlipperServer( + { + enableAndroid, + }, + store, + logger, + ); + + server.on('server-start-error', (err) => { + notification.error({ + message: 'Failed to start connection server', + description: + err.code === 'EADDRINUSE' ? ( + <> + Couldn't start connection server. Looks like you have multiple + copies of Flipper running or another process is using the same + port(s). As a result devices will not be able to connect to Flipper. +
+
+ Please try to kill the offending process by running{' '} + kill $(lsof -ti:PORTNUMBER) and restart flipper. +
+
+ {'' + err} + + ) : ( + <>Failed to start connection server: ${err.message} + ), + duration: null, + }); + }); + + server.on('device-connected', (device) => { + device.loadDevicePlugins( + store.getState().plugins.devicePlugins, + store.getState().connections.enabledDevicePlugins, + ); + + store.dispatch({ + type: 'REGISTER_DEVICE', + payload: device, + }); + }); + + server.on('client-connected', (payload) => + handleClientConnected(store, payload), + ); + + if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', () => { + server.close(); + }); + } + + return () => { + server.close(); + }; +}; + +export async function handleClientConnected(store: Store, client: Client) { + const {connections} = store.getState(); + const existingClient = connections.clients.find((c) => c.id === client.id); + + if (existingClient) { + existingClient.destroy(); + store.dispatch({ + type: 'CLEAR_CLIENT_PLUGINS_STATE', + payload: { + clientId: client.id, + devicePlugins: new Set(), + }, + }); + store.dispatch({ + type: 'CLIENT_REMOVED', + payload: client.id, + }); + } + + console.debug( + `Device client initialized: ${client.id}. Supported plugins: ${Array.from( + client.plugins, + ).join(', ')}`, + 'server', + ); + + store.dispatch({ + type: 'NEW_CLIENT', + payload: client, + }); + + const device = client.deviceSync; + if (device) { + store.dispatch(selectDevice(device)); + store.dispatch(selectClient(client.id)); + } + + client.emit('plugins-change'); +} diff --git a/desktop/app/src/dispatcher/index.tsx b/desktop/app/src/dispatcher/index.tsx index 943c629fe..340f98040 100644 --- a/desktop/app/src/dispatcher/index.tsx +++ b/desktop/app/src/dispatcher/index.tsx @@ -8,13 +8,9 @@ */ import {remote} from 'electron'; -import androidDevice from '../server/androidDevice'; -import metroDevice from '../server/metroDevice'; -import iOSDevice from '../server/iOSDevice'; -import desktopDevice from './desktopDevice'; +import flipperServer from './flipperServer'; import application from './application'; import tracking from './tracking'; -import server from '../server/server'; import notifications from './notifications'; import plugins from './plugins'; import user from './fb-stubs/user'; @@ -38,12 +34,8 @@ export default function (store: Store, logger: Logger): () => Promise { } const dispatchers: Array = [ application, - store.getState().settingsState.enableAndroid ? androidDevice : null, - iOSDevice, - metroDevice, - desktopDevice, tracking, - server, + flipperServer, notifications, plugins, user, diff --git a/desktop/app/src/server/FlipperServer.tsx b/desktop/app/src/server/FlipperServer.tsx new file mode 100644 index 000000000..850aadb54 --- /dev/null +++ b/desktop/app/src/server/FlipperServer.tsx @@ -0,0 +1,194 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import EventEmitter from 'events'; +import Client from '../Client'; +import {Store} from '../reducers/index'; +import {Logger} from '../fb-interfaces/Logger'; +import ServerController from './comms/ServerController'; +import {UninitializedClient} from '../UninitializedClient'; +import {addErrorNotification} from '../reducers/notifications'; +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 './androidDevice'; +import iOSDevice from './iOSDevice'; +import metroDevice from './metroDevice'; +import desktopDevice from './desktopDevice'; +import BaseDevice from './devices/BaseDevice'; + +type FlipperServerEvents = { + 'device-connected': BaseDevice; + 'client-connected': Client; + 'server-start-error': any; +}; + +export interface FlipperServerConfig { + enableAndroid: boolean; +} + +export async function startFlipperServer( + config: FlipperServerConfig, + store: Store, + logger: Logger, +): Promise { + const server = new FlipperServer(config, store, logger); + + await server.start(); + return server; +} + +/** + * 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 FlipperServer { + private readonly events = new EventEmitter(); + readonly server: ServerController; + readonly disposers: ((() => void) | void)[] = []; + + // TODO: remove store argument + constructor( + public config: FlipperServerConfig, + /** @deprecated remove! */ + public store: Store, + public logger: Logger, + ) { + this.server = new ServerController(logger, store); + } + + /** @private */ + async start() { + const server = this.server; + + server.addListener('new-client', (client: Client) => { + this.emit('client-connected', client); + }); + + server.addListener('error', (err) => { + this.emit('server-start-error', err); + }); + + server.addListener('start-client-setup', (client: UninitializedClient) => { + this.store.dispatch({ + type: 'START_CLIENT_SETUP', + payload: client, + }); + }); + + server.addListener( + 'finish-client-setup', + (payload: {client: UninitializedClient; deviceId: string}) => { + this.store.dispatch({ + type: 'FINISH_CLIENT_SETUP', + payload: payload, + }); + }, + ); + + server.addListener( + 'client-setup-error', + ({client, error}: {client: UninitializedClient; error: Error}) => { + this.store.dispatch( + addErrorNotification( + `Connection to '${client.appName}' on '${client.deviceName}' failed`, + 'Failed to start client connection', + error, + ), + ); + }, + ); + + server.addListener( + 'client-unresponsive-error', + ({ + client, + medium, + }: { + client: UninitializedClient; + medium: CertificateExchangeMedium; + deviceID: string; + }) => { + this.store.dispatch( + addErrorNotification( + `Timed out establishing connection with "${client.appName}" on "${client.deviceName}".`, + medium === 'WWW' ? ( + <> + Verify that both your computer and mobile device are on + Lighthouse/VPN{' '} + {!isLoggedIn().get() && ( + <> + and{' '} + + this.store.dispatch( + setActiveSheet(ACTIVE_SHEET_SIGN_IN), + ) + }> + log in to Facebook Intern + + + )}{' '} + so they can exchange certificates.{' '} + + Check this link + {' '} + on how to enable VPN on mobile device. + + ) : ( + 'Verify that your client is connected to Flipper and that there is no error related to idb.' + ), + ), + ); + }, + ); + + await server.init(); + await this.startDeviceListeners(); + } + + async startDeviceListeners() { + if (this.config.enableAndroid) { + this.disposers.push(await androidDevice(this.store, this.logger)); + } + this.disposers.push( + iOSDevice(this.store, this.logger), + metroDevice(this.store, this.logger), + desktopDevice(this), + ); + } + + on( + event: Event, + callback: (payload: FlipperServerEvents[Event]) => void, + ): void { + this.events.on(event, callback); + } + + /** + * @internal + */ + emit( + event: Event, + payload: FlipperServerEvents[Event], + ): void { + this.events.emit(event, payload); + } + + public async close() { + this.server.close(); + this.disposers.forEach((f) => f?.()); + } +} diff --git a/desktop/app/src/dispatcher/desktopDevice.tsx b/desktop/app/src/server/desktopDevice.tsx similarity index 59% rename from desktop/app/src/dispatcher/desktopDevice.tsx rename to desktop/app/src/server/desktopDevice.tsx index dbbb2093f..cc1bc4e44 100644 --- a/desktop/app/src/dispatcher/desktopDevice.tsx +++ b/desktop/app/src/server/desktopDevice.tsx @@ -7,13 +7,11 @@ * @format */ -import {Store} from '../reducers/index'; -import {Logger} from '../fb-interfaces/Logger'; - import MacDevice from '../server/devices/MacDevice'; import WindowsDevice from '../server/devices/WindowsDevice'; +import {FlipperServer} from './FlipperServer'; -export default (store: Store, _logger: Logger) => { +export default (flipperServer: FlipperServer) => { let device; if (process.platform === 'darwin') { device = new MacDevice(); @@ -22,12 +20,5 @@ export default (store: Store, _logger: Logger) => { } else { return; } - device.loadDevicePlugins( - store.getState().plugins.devicePlugins, - store.getState().connections.enabledDevicePlugins, - ); - store.dispatch({ - type: 'REGISTER_DEVICE', - payload: device, - }); + flipperServer.emit('device-connected', device); }; diff --git a/desktop/app/src/server/server.tsx b/desktop/app/src/server/server.tsx deleted file mode 100644 index acc7ca101..000000000 --- a/desktop/app/src/server/server.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -import ServerController from '../server/comms/ServerController'; - -import {Store} from '../reducers/index'; -import {Logger} from '../fb-interfaces/Logger'; -import Client from '../Client'; -import {UninitializedClient} from '../UninitializedClient'; -import {addErrorNotification} from '../reducers/notifications'; -import {CertificateExchangeMedium} from '../server/utils/CertificateProvider'; -import {selectClient, selectDevice} from '../reducers/connections'; -import {isLoggedIn} from '../fb-stubs/user'; -import React from 'react'; -import {notification, Typography} from 'antd'; -import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application'; - -export default (store: Store, logger: Logger) => { - const server = new ServerController(logger, store); - server.init(); - - server.addListener('new-client', (client: Client) => { - registerNewClient(store, client); - }); - - server.addListener('error', (err) => { - notification.error({ - message: 'Failed to start connection server', - description: - err.code === 'EADDRINUSE' ? ( - <> - Couldn't start connection server. Looks like you have multiple - copies of Flipper running or another process is using the same - port(s). As a result devices will not be able to connect to Flipper. -
-
- Please try to kill the offending process by running{' '} - kill $(lsof -ti:PORTNUMBER) and restart flipper. -
-
- {'' + err} - - ) : ( - <>Failed to start connection server: ${err.message} - ), - duration: null, - }); - }); - - server.addListener('start-client-setup', (client: UninitializedClient) => { - store.dispatch({ - type: 'START_CLIENT_SETUP', - payload: client, - }); - }); - - server.addListener( - 'finish-client-setup', - (payload: {client: UninitializedClient; deviceId: string}) => { - store.dispatch({ - type: 'FINISH_CLIENT_SETUP', - payload: payload, - }); - }, - ); - - server.addListener( - 'client-setup-error', - ({client, error}: {client: UninitializedClient; error: Error}) => { - store.dispatch( - addErrorNotification( - `Connection to '${client.appName}' on '${client.deviceName}' failed`, - 'Failed to start client connection', - error, - ), - ); - }, - ); - - server.addListener( - 'client-unresponsive-error', - ({ - client, - medium, - }: { - client: UninitializedClient; - medium: CertificateExchangeMedium; - deviceID: string; - }) => { - store.dispatch( - addErrorNotification( - `Timed out establishing connection with "${client.appName}" on "${client.deviceName}".`, - medium === 'WWW' ? ( - <> - Verify that both your computer and mobile device are on - Lighthouse/VPN{' '} - {!isLoggedIn().get() && ( - <> - and{' '} - - store.dispatch(setActiveSheet(ACTIVE_SHEET_SIGN_IN)) - }> - log in to Facebook Intern - - - )}{' '} - so they can exchange certificates.{' '} - - Check this link - {' '} - on how to enable VPN on mobile device. - - ) : ( - 'Verify that your client is connected to Flipper and that there is no error related to idb.' - ), - ), - ); - }, - ); - - if (typeof window !== 'undefined') { - window.addEventListener('beforeunload', () => { - server.close(); - }); - } - return server.close; -}; - -export function registerNewClient(store: Store, client: Client) { - const {connections} = store.getState(); - const existingClient = connections.clients.find((c) => c.id === client.id); - - if (existingClient) { - existingClient.destroy(); - store.dispatch({ - type: 'CLEAR_CLIENT_PLUGINS_STATE', - payload: { - clientId: client.id, - devicePlugins: new Set(), - }, - }); - store.dispatch({ - type: 'CLIENT_REMOVED', - payload: client.id, - }); - } - - store.dispatch({ - type: 'NEW_CLIENT', - payload: client, - }); - - const device = client.deviceSync; - if (device) { - store.dispatch(selectDevice(device)); - store.dispatch(selectClient(client.id)); - } -}