Make client -> device connection synchronous

Summary:
devices not always being readily available is causes a lot of complication in the api,
figured to resolve devices first before construction clients,
since clients not attached to a device are shown uncategorized anyway, making them practically un-interactable.
For more background info, see following chat.

{F344388883}

This diff will make it possible to only expose a synchronous api in Sandy

n.b. didn't update Navigation plugin, as that is done in a next diff

Reviewed By: jknoxville

Differential Revision: D24858332

fbshipit-source-id: 8339f831fbbc9c219add56a199364fde67adafc7
This commit is contained in:
Michel Weststrate
2020-11-11 07:57:14 -08:00
committed by Facebook GitHub Bot
parent fd8065eb7a
commit 9b4e7e873c
9 changed files with 124 additions and 102 deletions

View File

@@ -21,7 +21,7 @@ import {setPluginState} from './reducers/pluginStates';
import {Payload, ConnectionStatus} from 'rsocket-types'; import {Payload, ConnectionStatus} from 'rsocket-types';
import {Flowable, Single} from 'rsocket-flowable'; import {Flowable, Single} from 'rsocket-flowable';
import {performance} from 'perf_hooks'; import {performance} from 'perf_hooks';
import {reportPlatformFailures, reportPluginFailures} from './utils/metrics'; import {reportPluginFailures} from './utils/metrics';
import {notNull} from './utils/typeUtils'; import {notNull} from './utils/typeUtils';
import {default as isProduction} from './utils/isProduction'; import {default as isProduction} from './utils/isProduction';
import {registerPlugins} from './reducers/plugins'; import {registerPlugins} from './reducers/plugins';
@@ -33,7 +33,6 @@ import {
defaultEnabledBackgroundPlugins, defaultEnabledBackgroundPlugins,
} from './utils/pluginUtils'; } from './utils/pluginUtils';
import {processMessagesLater} from './utils/messageQueue'; import {processMessagesLater} from './utils/messageQueue';
import {sideEffect} from './utils/sideEffect';
import {emitBytesReceived} from './dispatcher/tracking'; import {emitBytesReceived} from './dispatcher/tracking';
import {debounce} from 'lodash'; import {debounce} from 'lodash';
import {batch} from 'react-redux'; import {batch} from 'react-redux';
@@ -134,11 +133,14 @@ export default class Client extends EventEmitter {
connection: FlipperClientConnection<any, any> | null | undefined; connection: FlipperClientConnection<any, any> | null | undefined;
store: Store; store: Store;
activePlugins: Set<string>; activePlugins: Set<string>;
/**
* @deprecated
* use plugin.deviceSync instead
*/
device: Promise<BaseDevice>; device: Promise<BaseDevice>;
_deviceResolve: (device: BaseDevice) => void = (_) => {}; deviceSync: BaseDevice;
_deviceResolved: BaseDevice | undefined;
logger: Logger; logger: Logger;
lastSeenDeviceList: Array<BaseDevice>;
broadcastCallbacks: Map<string, Map<string, Set<Function>>>; broadcastCallbacks: Map<string, Map<string, Set<Function>>>;
messageBuffer: Record< messageBuffer: Record<
string /*pluginKey*/, string /*pluginKey*/,
@@ -168,8 +170,8 @@ export default class Client extends EventEmitter {
conn: FlipperClientConnection<any, any> | null | undefined, conn: FlipperClientConnection<any, any> | null | undefined,
logger: Logger, logger: Logger,
store: Store, store: Store,
plugins?: Plugins | null | undefined, plugins: Plugins | null | undefined,
device?: BaseDevice, device: BaseDevice,
) { ) {
super(); super();
this.connected = true; this.connected = true;
@@ -185,17 +187,9 @@ export default class Client extends EventEmitter {
this.broadcastCallbacks = new Map(); this.broadcastCallbacks = new Map();
this.requestCallbacks = new Map(); this.requestCallbacks = new Map();
this.activePlugins = new Set(); this.activePlugins = new Set();
this.lastSeenDeviceList = [];
this.device = device this.device = Promise.resolve(device);
? Promise.resolve(device) this.deviceSync = device;
: new Promise((resolve, _reject) => {
this._deviceResolve = resolve;
});
if (device != null) {
this._deviceResolved = device;
}
const client = this; const client = this;
if (conn) { if (conn) {
@@ -215,58 +209,6 @@ export default class Client extends EventEmitter {
} }
} }
/* All clients should have a corresponding Device in the store.
However, clients can connect before a device is registered, so wait a
while for the device to be registered if it isn't already. */
async setMatchingDevice(): Promise<void> {
return reportPlatformFailures(
new Promise<BaseDevice>((resolve, reject) => {
let unsubscribe: () => void = () => {};
const device = this.store
.getState()
.connections.devices.find(
(device) => device.serial === this.query.device_id,
);
if (device) {
this._deviceResolved = device;
resolve(device);
return;
}
const timeout = setTimeout(() => {
unsubscribe();
const error = `Timed out waiting for device for client ${this.id}`;
console.error(error);
reject(error);
}, 5000);
unsubscribe = sideEffect(
this.store,
{name: 'waitForDevice', throttleMs: 100},
(state) => state.connections.devices,
(newDeviceList) => {
if (newDeviceList === this.lastSeenDeviceList) {
return;
}
this.lastSeenDeviceList = newDeviceList;
const matchingDevice = newDeviceList.find(
(device) => device.serial === this.query.device_id,
);
if (matchingDevice) {
clearTimeout(timeout);
resolve(matchingDevice);
unsubscribe();
}
},
);
}),
'client-setMatchingDevice',
).then((device) => {
this._deviceResolved = device;
this._deviceResolve(device);
});
}
supportsPlugin(pluginId: string): boolean { supportsPlugin(pluginId: string): boolean {
return this.plugins.includes(pluginId); return this.plugins.includes(pluginId);
} }
@@ -289,7 +231,6 @@ export default class Client extends EventEmitter {
} }
async init() { async init() {
this.setMatchingDevice();
await this.loadPlugins(); await this.loadPlugins();
// this starts all sandy enabled plugins // this starts all sandy enabled plugins
this.plugins.forEach((pluginId) => this.plugins.forEach((pluginId) =>
@@ -434,20 +375,12 @@ export default class Client extends EventEmitter {
this.emit('plugins-change'); this.emit('plugins-change');
} }
/**
* @deprecated
* use deviceSync.serial
*/
async deviceSerial(): Promise<string> { async deviceSerial(): Promise<string> {
try { return this.deviceSync.serial;
const device = await this.device;
if (!device) {
console.error('Using "" for deviceId device is not ready');
return '';
}
return device.serial;
} catch (e) {
console.error(
'Using "" for deviceId because client has no matching device',
);
return '';
}
} }
onMessage(msg: string) { onMessage(msg: string) {
@@ -478,7 +411,7 @@ export default class Client extends EventEmitter {
flipperMessagesClientPlugin.isConnected() flipperMessagesClientPlugin.isConnected()
) { ) {
flipperMessagesClientPlugin.newMessage({ flipperMessagesClientPlugin.newMessage({
device: this._deviceResolved?.displayTitle(), device: this.deviceSync?.displayTitle(),
app: this.query.app, app: this.query.app,
flipperInternalMethod: method, flipperInternalMethod: method,
plugin: data.params?.api, plugin: data.params?.api,
@@ -497,7 +430,7 @@ export default class Client extends EventEmitter {
}: ${error.message} + \nDevice Stack Trace: ${error.stacktrace}`, }: ${error.message} + \nDevice Stack Trace: ${error.stacktrace}`,
'deviceError', 'deviceError',
); );
this.device.then((device) => handleError(this.store, device, error)); handleError(this.store, this.deviceSync, error);
} else if (method === 'refreshPlugins') { } else if (method === 'refreshPlugins') {
this.refreshPlugins(); this.refreshPlugins();
} else if (method === 'execute') { } else if (method === 'execute') {
@@ -576,7 +509,7 @@ export default class Client extends EventEmitter {
reject(data.error); reject(data.error);
const {error} = data; const {error} = data;
if (error) { if (error) {
this.device.then((device) => handleError(this.store, device, error)); handleError(this.store, this.deviceSync, error);
} }
} else { } else {
// ??? // ???
@@ -670,7 +603,7 @@ export default class Client extends EventEmitter {
if (flipperMessagesClientPlugin.isConnected()) { if (flipperMessagesClientPlugin.isConnected()) {
flipperMessagesClientPlugin.newMessage({ flipperMessagesClientPlugin.newMessage({
device: this._deviceResolved?.displayTitle(), device: this.deviceSync?.displayTitle(),
app: this.query.app, app: this.query.app,
flipperInternalMethod: method, flipperInternalMethod: method,
payload: response, payload: response,
@@ -692,7 +625,7 @@ export default class Client extends EventEmitter {
if (flipperMessagesClientPlugin.isConnected()) { if (flipperMessagesClientPlugin.isConnected()) {
flipperMessagesClientPlugin.newMessage({ flipperMessagesClientPlugin.newMessage({
device: this._deviceResolved?.displayTitle(), device: this.deviceSync?.displayTitle(),
app: this.query.app, app: this.query.app,
flipperInternalMethod: method, flipperInternalMethod: method,
plugin: params?.api, plugin: params?.api,
@@ -776,7 +709,7 @@ export default class Client extends EventEmitter {
if (flipperMessagesClientPlugin.isConnected()) { if (flipperMessagesClientPlugin.isConnected()) {
flipperMessagesClientPlugin.newMessage({ flipperMessagesClientPlugin.newMessage({
device: this._deviceResolved?.displayTitle(), device: this.deviceSync?.displayTitle(),
app: this.query.app, app: this.query.app,
flipperInternalMethod: method, flipperInternalMethod: method,
payload: params, payload: params,

View File

@@ -83,6 +83,7 @@ function getStore(selectedPlugins: Array<string>) {
// @ts-ignore // @ts-ignore
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
selectedDevice,
); );
mockStore.dispatch({ mockStore.dispatch({
type: 'NEW_CLIENT', type: 'NEW_CLIENT',

View File

@@ -285,10 +285,17 @@ export class FlipperPlugin<
client: PluginClient; client: PluginClient;
realClient: Client; realClient: Client;
/**
* @deprecated use .device instead
*/
getDevice(): Promise<BaseDevice> { getDevice(): Promise<BaseDevice> {
return this.realClient.device; return this.realClient.device;
} }
get device() {
return this.realClient.deviceSync;
}
_teardown() { _teardown() {
// automatically unsubscribe subscriptions // automatically unsubscribe subscriptions
const pluginId = this.constructor.id; const pluginId = this.constructor.id;

View File

@@ -378,8 +378,8 @@ export function findBestDevice(
return selected; return selected;
} }
// if there is an active app, use device owning the app // if there is an active app, use device owning the app
if (client && client._deviceResolved) { if (client) {
return client._deviceResolved; return client.deviceSync;
} }
// if no active app, use the preferred device // if no active app, use the preferred device
if (userPreferredDevice) { if (userPreferredDevice) {

View File

@@ -13,7 +13,7 @@ import {
} from './utils/CertificateProvider'; } from './utils/CertificateProvider';
import {Logger} from './fb-interfaces/Logger'; import {Logger} from './fb-interfaces/Logger';
import {ClientQuery} from './Client'; import {ClientQuery} from './Client';
import {Store} from './reducers/index'; import {Store, State} from './reducers/index';
import CertificateProvider from './utils/CertificateProvider'; import CertificateProvider from './utils/CertificateProvider';
import {RSocketServer} from 'rsocket-core'; import {RSocketServer} from 'rsocket-core';
import RSocketTCPServer from 'rsocket-tcp-server'; import RSocketTCPServer from 'rsocket-tcp-server';
@@ -38,6 +38,8 @@ import {IncomingMessage} from 'http';
import ws from 'ws'; import ws from 'ws';
import {initSelfInpector} from './utils/self-inspection/selfInspectionUtils'; import {initSelfInpector} from './utils/self-inspection/selfInspectionUtils';
import ClientDevice from './devices/ClientDevice'; import ClientDevice from './devices/ClientDevice';
import BaseDevice from './devices/BaseDevice';
import {sideEffect} from './utils/sideEffect';
type ClientInfo = { type ClientInfo = {
connection: FlipperClientConnection<any, any> | null | undefined; connection: FlipperClientConnection<any, any> | null | undefined;
@@ -529,7 +531,7 @@ class Server extends EventEmitter {
); );
}) })
: Promise.resolve(query.device_id) : Promise.resolve(query.device_id)
).then((csrId) => { ).then(async (csrId) => {
query.device_id = csrId; query.device_id = csrId;
query.app = appNameWithUpdateHint(query); query.app = appNameWithUpdateHint(query);
@@ -541,7 +543,18 @@ class Server extends EventEmitter {
}); });
console.debug(`Device connected: ${id}`, 'server'); console.debug(`Device connected: ${id}`, 'server');
const client = new Client(id, query, conn, this.logger, this.store); const device =
getDeviceBySerial(this.store.getState(), query.device_id) ??
(await findDeviceForConnection(this.store, query.app, query.device_id));
const client = new Client(
id,
query,
conn,
this.logger,
this.store,
undefined,
device,
);
const info = { const info = {
client, client,
@@ -627,4 +640,54 @@ class ConnectionTracker {
} }
} }
function getDeviceBySerial(
state: State,
serial: string,
): BaseDevice | undefined {
return state.connections.devices.find((device) => device.serial === serial);
}
async function findDeviceForConnection(
store: Store,
clientId: string,
serial: string,
): Promise<BaseDevice> {
let lastSeenDeviceList: BaseDevice[] = [];
/* All clients should have a corresponding Device in the store.
However, clients can connect before a device is registered, so wait a
while for the device to be registered if it isn't already. */
return reportPlatformFailures(
new Promise<BaseDevice>((resolve, reject) => {
let unsubscribe: () => void = () => {};
const timeout = setTimeout(() => {
unsubscribe();
const error = `Timed out waiting for device ${serial} for client ${clientId}`;
console.error(error);
reject(error);
}, 5000);
unsubscribe = sideEffect(
store,
{name: 'waitForDevice', throttleMs: 100},
(state) => state.connections.devices,
(newDeviceList) => {
if (newDeviceList === lastSeenDeviceList) {
return;
}
lastSeenDeviceList = newDeviceList;
const matchingDevice = newDeviceList.find(
(device) => device.serial === serial,
);
if (matchingDevice) {
clearTimeout(timeout);
resolve(matchingDevice);
unsubscribe();
}
},
);
}),
'client-setMatchingDevice',
);
}
export default Server; export default Server;

View File

@@ -726,6 +726,7 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
device1,
); );
const client2 = new Client( const client2 = new Client(
generateClientIdentifier(device1, 'app2'), generateClientIdentifier(device1, 'app2'),
@@ -739,6 +740,7 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
logger, logger,
mockStore, mockStore,
['TestDevicePlugin'], ['TestDevicePlugin'],
device1,
); );
const client3 = new Client( const client3 = new Client(
generateClientIdentifier(device1, 'app3'), generateClientIdentifier(device1, 'app3'),
@@ -752,6 +754,7 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
device1,
); );
const plugins: PluginsState = { const plugins: PluginsState = {
clientPlugins: new Map([ clientPlugins: new Map([
@@ -802,6 +805,7 @@ test('test determinePluginsToProcess for no selected plugin present in any clien
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
device1,
); );
const client2 = new Client( const client2 = new Client(
generateClientIdentifier(device1, 'app2'), generateClientIdentifier(device1, 'app2'),
@@ -815,6 +819,7 @@ test('test determinePluginsToProcess for no selected plugin present in any clien
logger, logger,
mockStore, mockStore,
['TestDevicePlugin'], ['TestDevicePlugin'],
device1,
); );
const plugins: PluginsState = { const plugins: PluginsState = {
clientPlugins: new Map([ clientPlugins: new Map([
@@ -846,6 +851,7 @@ test('test determinePluginsToProcess for multiple clients on same device', async
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
device1,
); );
const client2 = new Client( const client2 = new Client(
generateClientIdentifier(device1, 'app2'), generateClientIdentifier(device1, 'app2'),
@@ -859,6 +865,7 @@ test('test determinePluginsToProcess for multiple clients on same device', async
logger, logger,
mockStore, mockStore,
['TestDevicePlugin'], ['TestDevicePlugin'],
device1,
); );
const plugins: PluginsState = { const plugins: PluginsState = {
clientPlugins: new Map([['TestPlugin', TestPlugin]]), clientPlugins: new Map([['TestPlugin', TestPlugin]]),
@@ -897,6 +904,7 @@ test('test determinePluginsToProcess for multiple clients on different device',
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
device1,
); );
const client2Device1 = new Client( const client2Device1 = new Client(
generateClientIdentifier(device1, 'app2'), generateClientIdentifier(device1, 'app2'),
@@ -910,6 +918,7 @@ test('test determinePluginsToProcess for multiple clients on different device',
logger, logger,
mockStore, mockStore,
['TestDevicePlugin'], ['TestDevicePlugin'],
device1,
); );
const client1Device2 = new Client( const client1Device2 = new Client(
generateClientIdentifier(device2, 'app'), generateClientIdentifier(device2, 'app'),
@@ -923,6 +932,7 @@ test('test determinePluginsToProcess for multiple clients on different device',
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
device1,
); );
const client2Device2 = new Client( const client2Device2 = new Client(
generateClientIdentifier(device2, 'app2'), generateClientIdentifier(device2, 'app2'),
@@ -936,6 +946,7 @@ test('test determinePluginsToProcess for multiple clients on different device',
logger, logger,
mockStore, mockStore,
['TestDevicePlugin'], ['TestDevicePlugin'],
device1,
); );
const plugins: PluginsState = { const plugins: PluginsState = {
clientPlugins: new Map([['TestPlugin', TestPlugin]]), clientPlugins: new Map([['TestPlugin', TestPlugin]]),
@@ -999,6 +1010,7 @@ test('test determinePluginsToProcess to ignore archived clients', async () => {
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
archivedDevice,
); );
const archivedClient = new Client( const archivedClient = new Client(
generateClientIdentifier(archivedDevice, 'app'), generateClientIdentifier(archivedDevice, 'app'),
@@ -1012,6 +1024,7 @@ test('test determinePluginsToProcess to ignore archived clients', async () => {
logger, logger,
mockStore, mockStore,
['TestPlugin', 'TestDevicePlugin'], ['TestPlugin', 'TestDevicePlugin'],
archivedDevice,
); );
const plugins: PluginsState = { const plugins: PluginsState = {
clientPlugins: new Map([['TestPlugin', TestPlugin]]), clientPlugins: new Map([['TestPlugin', TestPlugin]]),

View File

@@ -44,9 +44,10 @@ export function initJsEmulatorIPC(
(_event: IpcRendererEvent, message: any) => { (_event: IpcRendererEvent, message: any) => {
const {windowId} = message; const {windowId} = message;
const {plugins, appName} = message.payload; const {plugins, appName} = message.payload;
const device = new JSDevice(jsDeviceId(windowId), 'jsEmulator', windowId);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: new JSDevice(jsDeviceId(windowId), 'jsEmulator', windowId), payload: device,
}); });
const connection = new JSClientFlipperConnection(windowId); const connection = new JSClientFlipperConnection(windowId);
@@ -69,6 +70,7 @@ export function initJsEmulatorIPC(
logger, logger,
store, store,
plugins, plugins,
device,
); );
flipperConnections.set(clientId, { flipperConnections.set(clientId, {

View File

@@ -32,14 +32,15 @@ export function initSelfInpector(
) { ) {
const appName = 'Flipper'; const appName = 'Flipper';
const device_id = 'FlipperSelfInspectionDevice'; const device_id = 'FlipperSelfInspectionDevice';
const device = new FlipperSelfInspectionDevice(
device_id,
'emulator',
appName,
'JSWebApp',
);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: new FlipperSelfInspectionDevice( payload: device,
device_id,
'emulator',
appName,
'JSWebApp',
),
}); });
selfInspectionClient.addPlugin(flipperMessagesClientPlugin); selfInspectionClient.addPlugin(flipperMessagesClientPlugin);
@@ -59,6 +60,8 @@ export function initSelfInpector(
selfInspectionClient, selfInspectionClient,
logger, logger,
store, store,
undefined,
device,
); );
flipperConnections.set(clientId, { flipperConnections.set(clientId, {

View File

@@ -240,7 +240,7 @@ export default class LayoutPlugin extends FlipperPlugin<
} }
if (this.props.isArchivedDevice) { if (this.props.isArchivedDevice) {
this.getDevice() Promise.resolve(this.device)
.then((d) => { .then((d) => {
const handle = (d as ArchivedDevice).getArchivedScreenshotHandle(); const handle = (d as ArchivedDevice).getArchivedScreenshotHandle();
if (!handle) { if (!handle) {