separate action dispatch from server

Summary: This diff moves the first small pieces of getting device detection up and running to `server/`, and the wiring between FlipperServer and flipper core / redux is setting up specific events and dispatch actions from there.

Reviewed By: timur-valiev

Differential Revision: D30276776

fbshipit-source-id: b30b996d03c27459815bebeb97b05b5fe5d24bec
This commit is contained in:
Michel Weststrate
2021-08-13 04:01:06 -07:00
committed by Facebook GitHub Bot
parent 4909296d86
commit 6175424d16
6 changed files with 318 additions and 189 deletions

View File

@@ -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)!;

View File

@@ -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.
<br />
<br />
Please try to kill the offending process by running{' '}
<code>kill $(lsof -ti:PORTNUMBER)</code> and restart flipper.
<br />
<br />
{'' + 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');
}

View File

@@ -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<void> {
}
const dispatchers: Array<Dispatcher> = [
application,
store.getState().settingsState.enableAndroid ? androidDevice : null,
iOSDevice,
metroDevice,
desktopDevice,
tracking,
server,
flipperServer,
notifications,
plugins,
user,

View File

@@ -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<FlipperServer> {
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{' '}
<Typography.Link
onClick={() =>
this.store.dispatch(
setActiveSheet(ACTIVE_SHEET_SIGN_IN),
)
}>
log in to Facebook Intern
</Typography.Link>
</>
)}{' '}
so they can exchange certificates.{' '}
<Typography.Link href="https://www.internalfb.com/intern/wiki/Ops/Network/Enterprise_Network_Engineering/ene_wlra/VPN_Help/Vpn/mobile/">
Check this link
</Typography.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 extends keyof FlipperServerEvents>(
event: Event,
callback: (payload: FlipperServerEvents[Event]) => void,
): void {
this.events.on(event, callback);
}
/**
* @internal
*/
emit<Event extends keyof FlipperServerEvents>(
event: Event,
payload: FlipperServerEvents[Event],
): void {
this.events.emit(event, payload);
}
public async close() {
this.server.close();
this.disposers.forEach((f) => f?.());
}
}

View File

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

View File

@@ -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.
<br />
<br />
Please try to kill the offending process by running{' '}
<code>kill $(lsof -ti:PORTNUMBER)</code> and restart flipper.
<br />
<br />
{'' + 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{' '}
<Typography.Link
onClick={() =>
store.dispatch(setActiveSheet(ACTIVE_SHEET_SIGN_IN))
}>
log in to Facebook Intern
</Typography.Link>
</>
)}{' '}
so they can exchange certificates.{' '}
<Typography.Link href="https://www.internalfb.com/intern/wiki/Ops/Network/Enterprise_Network_Engineering/ene_wlra/VPN_Help/Vpn/mobile/">
Check this link
</Typography.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));
}
}