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));
- }
-}