Decouple Metro device handling from Flipper core
Summary: Decoupled metro 'device' from Redux store. Extracting some commonalities with Android device management up into FlipperServer Reviewed By: timur-valiev Differential Revision: D30309256 fbshipit-source-id: 1a9ac01e3f21d2d08761554d3644a7ae8d00a93e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
03f2f95a31
commit
4ae7d9c42b
@@ -17,7 +17,7 @@ import {notification} from 'antd';
|
|||||||
|
|
||||||
export default async (store: Store, logger: Logger) => {
|
export default async (store: Store, logger: Logger) => {
|
||||||
const {enableAndroid, androidHome} = store.getState().settingsState;
|
const {enableAndroid, androidHome} = store.getState().settingsState;
|
||||||
const server = await startFlipperServer(
|
const server = startFlipperServer(
|
||||||
{
|
{
|
||||||
enableAndroid,
|
enableAndroid,
|
||||||
androidHome,
|
androidHome,
|
||||||
@@ -40,7 +40,7 @@ export default async (store: Store, logger: Logger) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('server-start-error', (err) => {
|
server.on('server-error', (err) => {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Failed to start connection server',
|
message: 'Failed to start connection server',
|
||||||
description:
|
description:
|
||||||
@@ -58,7 +58,7 @@ export default async (store: Store, logger: Logger) => {
|
|||||||
{'' + err}
|
{'' + err}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>Failed to start connection server: ${err.message}</>
|
<>Failed to start Flipper server: ${err.message}</>
|
||||||
),
|
),
|
||||||
duration: null,
|
duration: null,
|
||||||
});
|
});
|
||||||
@@ -87,7 +87,7 @@ export default async (store: Store, logger: Logger) => {
|
|||||||
os: device.os,
|
os: device.os,
|
||||||
serial: device.serial,
|
serial: device.serial,
|
||||||
});
|
});
|
||||||
// N.B.: note that we don't remove the device, we keep it in offline mode!
|
// N.B.: note that we don't remove the device, we keep it in offline
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('client-connected', (payload) =>
|
server.on('client-connected', (payload) =>
|
||||||
@@ -100,6 +100,21 @@ export default async (store: Store, logger: Logger) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server
|
||||||
|
.waitForServerStarted()
|
||||||
|
.then(() => {
|
||||||
|
console.log(
|
||||||
|
'Flipper server started and accepting device / client connections',
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Failed to start Flipper server', e);
|
||||||
|
notification.error({
|
||||||
|
message: 'Failed to start Flipper server',
|
||||||
|
description: 'error: ' + e,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
server.close();
|
server.close();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import BaseDevice from '../../../server/devices/BaseDevice';
|
|||||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||||
import {TestUtils} from 'flipper-plugin';
|
import {TestUtils} from 'flipper-plugin';
|
||||||
import {selectPlugin} from '../../../reducers/connections';
|
import {selectPlugin} from '../../../reducers/connections';
|
||||||
import {registerMetroDevice} from '../../../server/devices/metro/metroDeviceManager';
|
|
||||||
import {
|
import {
|
||||||
addGatekeepedPlugins,
|
addGatekeepedPlugins,
|
||||||
registerMarketplacePlugins,
|
registerMarketplacePlugins,
|
||||||
@@ -78,7 +77,10 @@ describe('basic getActiveDevice with metro present', () => {
|
|||||||
};
|
};
|
||||||
testDevice = flipper.device;
|
testDevice = flipper.device;
|
||||||
// flipper.store.dispatch(registerPlugins([LogsPlugin]))
|
// flipper.store.dispatch(registerPlugins([LogsPlugin]))
|
||||||
await registerMetroDevice(undefined, flipper.store, flipper.logger);
|
flipper.store.dispatch({
|
||||||
|
type: 'REGISTER_DEVICE',
|
||||||
|
payload: new MetroDevice('http://localhost:8081', undefined),
|
||||||
|
});
|
||||||
metro = getMetroDevice(flipper.store.getState())!;
|
metro = getMetroDevice(flipper.store.getState())!;
|
||||||
metro.supportsPlugin = (p) => {
|
metro.supportsPlugin = (p) => {
|
||||||
return p.id !== 'unsupportedDevicePlugin';
|
return p.id !== 'unsupportedDevicePlugin';
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import desktopDevice from './devices/desktop/desktopDeviceManager';
|
|||||||
import BaseDevice from './devices/BaseDevice';
|
import BaseDevice from './devices/BaseDevice';
|
||||||
|
|
||||||
type FlipperServerEvents = {
|
type FlipperServerEvents = {
|
||||||
'server-start-error': any;
|
'server-state': {state: ServerState; error?: Error};
|
||||||
|
'server-error': any;
|
||||||
notification: {
|
notification: {
|
||||||
type: 'error';
|
type: 'error';
|
||||||
title: string;
|
title: string;
|
||||||
@@ -47,17 +48,18 @@ export interface FlipperServerConfig {
|
|||||||
serverPorts: ServerPorts;
|
serverPorts: ServerPorts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startFlipperServer(
|
export function startFlipperServer(
|
||||||
config: FlipperServerConfig,
|
config: FlipperServerConfig,
|
||||||
store: Store,
|
store: Store,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<FlipperServer> {
|
): FlipperServer {
|
||||||
const server = new FlipperServer(config, store, logger);
|
const server = new FlipperServer(config, store, logger);
|
||||||
|
server.start();
|
||||||
await server.start();
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerState = 'pending' | 'starting' | 'started' | 'error' | 'closed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FlipperServer takes care of all incoming device & client connections.
|
* FlipperServer takes care of all incoming device & client connections.
|
||||||
* It will set up managers per device type, and create the incoming
|
* It will set up managers per device type, and create the incoming
|
||||||
@@ -70,7 +72,8 @@ export class FlipperServer {
|
|||||||
private readonly events = new EventEmitter();
|
private readonly events = new EventEmitter();
|
||||||
readonly server: ServerController;
|
readonly server: ServerController;
|
||||||
readonly disposers: ((() => void) | void)[] = [];
|
readonly disposers: ((() => void) | void)[] = [];
|
||||||
|
private readonly devices = new Map<string, BaseDevice>();
|
||||||
|
state: ServerState = 'pending';
|
||||||
android: AndroidDeviceManager;
|
android: AndroidDeviceManager;
|
||||||
|
|
||||||
// TODO: remove store argument
|
// TODO: remove store argument
|
||||||
@@ -84,8 +87,50 @@ export class FlipperServer {
|
|||||||
this.android = new AndroidDeviceManager(this);
|
this.android = new AndroidDeviceManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setServerState(state: ServerState, error?: Error) {
|
||||||
|
this.state = state;
|
||||||
|
this.emit('server-state', {state, error});
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForServerStarted() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
switch (this.state) {
|
||||||
|
case 'closed':
|
||||||
|
return reject(new Error('Server was closed already'));
|
||||||
|
case 'error':
|
||||||
|
return reject(new Error('Server has errored already'));
|
||||||
|
case 'started':
|
||||||
|
return resolve();
|
||||||
|
default: {
|
||||||
|
const listener = ({
|
||||||
|
state,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
state: ServerState;
|
||||||
|
error: Error;
|
||||||
|
}) => {
|
||||||
|
switch (state) {
|
||||||
|
case 'error':
|
||||||
|
return reject(error);
|
||||||
|
case 'started':
|
||||||
|
return resolve();
|
||||||
|
case 'closed':
|
||||||
|
return reject(new Error('Server closed'));
|
||||||
|
}
|
||||||
|
this.events.off('server-state', listener);
|
||||||
|
};
|
||||||
|
this.events.on('server-state', listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
async start() {
|
async start() {
|
||||||
|
if (this.state !== 'pending') {
|
||||||
|
throw new Error('Server already started');
|
||||||
|
}
|
||||||
|
this.setServerState('starting');
|
||||||
const server = this.server;
|
const server = this.server;
|
||||||
|
|
||||||
server.addListener('new-client', (client: Client) => {
|
server.addListener('new-client', (client: Client) => {
|
||||||
@@ -93,7 +138,7 @@ export class FlipperServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.addListener('error', (err) => {
|
server.addListener('error', (err) => {
|
||||||
this.emit('server-start-error', err);
|
this.emit('server-error', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.addListener('start-client-setup', (client: UninitializedClient) => {
|
server.addListener('start-client-setup', (client: UninitializedClient) => {
|
||||||
@@ -170,15 +215,21 @@ export class FlipperServer {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await server.init();
|
try {
|
||||||
await this.startDeviceListeners();
|
await server.init();
|
||||||
|
await this.startDeviceListeners();
|
||||||
|
this.setServerState('started');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to start FlipperServer', e);
|
||||||
|
this.setServerState('error', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startDeviceListeners() {
|
async startDeviceListeners() {
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
await this.android.watchAndroidDevices(),
|
await this.android.watchAndroidDevices(),
|
||||||
iOSDevice(this.store, this.logger),
|
iOSDevice(this.store, this.logger),
|
||||||
metroDevice(this.store, this.logger),
|
metroDevice(this),
|
||||||
desktopDevice(this),
|
desktopDevice(this),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -200,8 +251,60 @@ export class FlipperServer {
|
|||||||
this.events.emit(event, payload);
|
this.events.emit(event, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerDevice(device: BaseDevice) {
|
||||||
|
// destroy existing device
|
||||||
|
const existing = this.devices.get(device.serial);
|
||||||
|
if (existing) {
|
||||||
|
// assert different kind of devices aren't accidentally reusing the same serial
|
||||||
|
if (Object.getPrototypeOf(existing) !== Object.getPrototypeOf(device)) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to register a new device type for existing serial '${
|
||||||
|
device.serial
|
||||||
|
}': Trying to replace existing '${
|
||||||
|
Object.getPrototypeOf(existing).constructor.name
|
||||||
|
}' with a new '${Object.getPrototypeOf(device).constructor.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// devices should be recycled, unless they have lost connection
|
||||||
|
if (existing.connected.get()) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to replace still connected device '${device.serial}' with a new instance`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
existing.destroy();
|
||||||
|
}
|
||||||
|
// register new device
|
||||||
|
this.devices.set(device.serial, device);
|
||||||
|
this.emit('device-connected', device);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterDevice(serial: string) {
|
||||||
|
const device = this.devices.get(serial);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device.disconnect(); // we'll only destroy upon replacement
|
||||||
|
this.emit('device-disconnected', device);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDevice(serial: string): BaseDevice {
|
||||||
|
const device = this.devices.get(serial);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error('No device with serial: ' + serial);
|
||||||
|
}
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceSerials(): string[] {
|
||||||
|
return Array.from(this.devices.keys());
|
||||||
|
}
|
||||||
|
|
||||||
public async close() {
|
public async close() {
|
||||||
this.server.close();
|
this.server.close();
|
||||||
|
for (const device of this.devices.values()) {
|
||||||
|
device.destroy();
|
||||||
|
}
|
||||||
this.disposers.forEach((f) => f?.());
|
this.disposers.forEach((f) => f?.());
|
||||||
|
this.setServerState('closed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,20 +230,12 @@ export class AndroidDeviceManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove offline devices with same serial as the connected.
|
this.flipperServer.registerDevice(androidDevice);
|
||||||
this.devices.get(androidDevice.serial)?.destroy();
|
|
||||||
// register new device
|
|
||||||
this.devices.set(androidDevice.serial, androidDevice);
|
|
||||||
this.flipperServer.emit('device-connected', androidDevice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterDevices(serials: Array<string>) {
|
unregisterDevices(serials: Array<string>) {
|
||||||
serials.forEach((serial) => {
|
serials.forEach((serial) => {
|
||||||
const device = this.devices.get(serial);
|
this.flipperServer.unregisterDevice(serial);
|
||||||
if (device?.connected?.get()) {
|
|
||||||
device.disconnect();
|
|
||||||
this.flipperServer.emit('device-disconnected', device);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ export default (flipperServer: FlipperServer) => {
|
|||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flipperServer.emit('device-connected', device);
|
flipperServer.registerDevice(device);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,13 +7,10 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Store} from '../../../reducers/index';
|
|
||||||
import {Logger} from '../../../fb-interfaces/Logger';
|
|
||||||
import MetroDevice from './MetroDevice';
|
import MetroDevice from './MetroDevice';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import {addErrorNotification} from '../../../reducers/notifications';
|
|
||||||
import {destroyDevice} from '../../../reducers/connections';
|
|
||||||
import {parseEnvironmentVariableAsNumber} from '../../utils/environmentVariables';
|
import {parseEnvironmentVariableAsNumber} from '../../utils/environmentVariables';
|
||||||
|
import {FlipperServer} from '../../FlipperServer';
|
||||||
|
|
||||||
const METRO_HOST = 'localhost';
|
const METRO_HOST = 'localhost';
|
||||||
const METRO_PORT = parseEnvironmentVariableAsNumber('METRO_SERVER_PORT', 8081);
|
const METRO_PORT = parseEnvironmentVariableAsNumber('METRO_SERVER_PORT', 8081);
|
||||||
@@ -47,29 +44,15 @@ async function isMetroRunning(): Promise<boolean> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerMetroDevice(
|
async function registerMetroDevice(
|
||||||
ws: WebSocket | undefined,
|
ws: WebSocket | undefined,
|
||||||
store: Store,
|
flipperServer: FlipperServer,
|
||||||
logger: Logger,
|
|
||||||
) {
|
) {
|
||||||
const metroDevice = new MetroDevice(METRO_URL, ws);
|
const metroDevice = new MetroDevice(METRO_URL, ws);
|
||||||
logger.track('usage', 'register-device', {
|
flipperServer.registerDevice(metroDevice);
|
||||||
os: 'Metro',
|
|
||||||
name: metroDevice.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
metroDevice.loadDevicePlugins(
|
|
||||||
store.getState().plugins.devicePlugins,
|
|
||||||
store.getState().connections.enabledDevicePlugins,
|
|
||||||
);
|
|
||||||
store.dispatch({
|
|
||||||
type: 'REGISTER_DEVICE',
|
|
||||||
payload: metroDevice,
|
|
||||||
serial: METRO_URL,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (store: Store, logger: Logger) => {
|
export default (flipperServer: FlipperServer) => {
|
||||||
let timeoutHandle: NodeJS.Timeout;
|
let timeoutHandle: NodeJS.Timeout;
|
||||||
let ws: WebSocket | undefined;
|
let ws: WebSocket | undefined;
|
||||||
let unregistered = false;
|
let unregistered = false;
|
||||||
@@ -86,7 +69,7 @@ export default (store: Store, logger: Logger) => {
|
|||||||
_ws.onopen = () => {
|
_ws.onopen = () => {
|
||||||
clearTimeout(guard);
|
clearTimeout(guard);
|
||||||
ws = _ws;
|
ws = _ws;
|
||||||
registerMetroDevice(ws, store, logger);
|
registerMetroDevice(ws, flipperServer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_ws.onclose = _ws.onerror = function (event?: any) {
|
_ws.onclose = _ws.onerror = function (event?: any) {
|
||||||
@@ -100,20 +83,19 @@ export default (store: Store, logger: Logger) => {
|
|||||||
unregistered = true;
|
unregistered = true;
|
||||||
clearTimeout(guard);
|
clearTimeout(guard);
|
||||||
ws = undefined;
|
ws = undefined;
|
||||||
destroyDevice(store, logger, METRO_URL);
|
flipperServer.unregisterDevice(METRO_URL);
|
||||||
scheduleNext();
|
scheduleNext();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const guard = setTimeout(() => {
|
const guard = setTimeout(() => {
|
||||||
// Metro is running, but didn't respond to /events endpoint
|
// Metro is running, but didn't respond to /events endpoint
|
||||||
store.dispatch(
|
flipperServer.emit('notification', {
|
||||||
addErrorNotification(
|
type: 'error',
|
||||||
'Failed to connect to Metro',
|
title: 'Failed to connect to Metro',
|
||||||
`Flipper did find a running Metro instance, but couldn't connect to the logs. Probably your React Native version is too old to support Flipper. Cause: Failed to get a connection to ${METRO_LOGS_ENDPOINT} in a timely fashion`,
|
description: `Flipper did find a running Metro instance, but couldn't connect to the logs. Probably your React Native version is too old to support Flipper. Cause: Failed to get a connection to ${METRO_LOGS_ENDPOINT} in a timely fashion`,
|
||||||
),
|
});
|
||||||
);
|
registerMetroDevice(undefined, flipperServer);
|
||||||
registerMetroDevice(undefined, store, logger);
|
|
||||||
// Note: no scheduleNext, we won't retry until restart
|
// Note: no scheduleNext, we won't retry until restart
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user