Make sure Sandy Devices Plugins are loaded in Flipper devices
Summary: This diff makes sure that devices will actually instantiate applicable sandy device plugins. Similar to how client plugins are owned by Client, device plugins are directly owned by BaseDevice, which significantly simplifies life cycle management and doesn't dispatch updates to all Redux connect components whenever something irrelevant changes. Also made sure `device.teardown()` is called. That API already existed, but wasn't used or implemented before. Updated Flipper test utils to support testing device plugins as well (both Sandy and classic ones) Reviewed By: passy, nikoant Differential Revision: D22693929 fbshipit-source-id: 73b2b8666ef7a0e748ea89360db84734d37eb5be
This commit is contained in:
committed by
Facebook GitHub Bot
parent
1e956e1bf5
commit
489dd1521e
@@ -68,12 +68,16 @@ const handleError = (store: Store, device: BaseDevice, error: ErrorType) => {
|
|||||||
if (isProduction()) {
|
if (isProduction()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const crashReporterPlugin = store
|
const crashReporterPlugin: typeof FlipperDevicePlugin = store
|
||||||
.getState()
|
.getState()
|
||||||
.plugins.devicePlugins.get('CrashReporter');
|
.plugins.devicePlugins.get('CrashReporter') as any;
|
||||||
if (!crashReporterPlugin) {
|
if (!crashReporterPlugin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!crashReporterPlugin.persistedStateReducer) {
|
||||||
|
console.error('CrashReporterPlugin persistedStateReducer broken'); // Make sure we update this code if we ever convert it to Sandy
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pluginKey = getPluginKey(null, device, 'CrashReporter');
|
const pluginKey = getPluginKey(null, device, 'CrashReporter');
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import {FlipperDevicePlugin, DeviceLogListener} from 'flipper';
|
import {DeviceLogListener} from 'flipper';
|
||||||
import {sortPluginsByName} from '../utils/pluginUtils';
|
import {sortPluginsByName} from '../utils/pluginUtils';
|
||||||
import {DeviceLogEntry} from 'flipper-plugin';
|
import {
|
||||||
|
DeviceLogEntry,
|
||||||
|
SandyDevicePluginInstance,
|
||||||
|
SandyPluginDefinition,
|
||||||
|
} from 'flipper-plugin';
|
||||||
|
import {DevicePluginMap} from '../plugin';
|
||||||
|
|
||||||
export type DeviceShell = {
|
export type DeviceShell = {
|
||||||
stdout: stream.Readable;
|
stdout: stream.Readable;
|
||||||
@@ -64,7 +69,9 @@ export default class BaseDevice {
|
|||||||
source = '';
|
source = '';
|
||||||
|
|
||||||
// sorted list of supported device plugins
|
// sorted list of supported device plugins
|
||||||
devicePlugins!: string[];
|
devicePlugins: string[] = [];
|
||||||
|
|
||||||
|
sandyPluginStates = new Map<string, SandyDevicePluginInstance>();
|
||||||
|
|
||||||
supportsOS(os: OS) {
|
supportsOS(os: OS) {
|
||||||
return os.toLowerCase() === this.os.toLowerCase();
|
return os.toLowerCase() === this.os.toLowerCase();
|
||||||
@@ -84,7 +91,11 @@ export default class BaseDevice {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown() {}
|
teardown() {
|
||||||
|
for (const instance of this.sandyPluginStates.values()) {
|
||||||
|
instance.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
supportedColumns(): Array<string> {
|
supportedColumns(): Array<string> {
|
||||||
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
|
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
|
||||||
@@ -158,10 +169,26 @@ export default class BaseDevice {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDevicePlugins(devicePlugins?: Map<string, typeof FlipperDevicePlugin>) {
|
loadDevicePlugins(devicePlugins?: DevicePluginMap) {
|
||||||
this.devicePlugins = Array.from(devicePlugins ? devicePlugins.values() : [])
|
if (!devicePlugins) {
|
||||||
.filter((plugin) => plugin.supportsDevice(this))
|
return;
|
||||||
.sort(sortPluginsByName)
|
}
|
||||||
.map((plugin) => plugin.id);
|
const plugins = Array.from(devicePlugins.values());
|
||||||
|
plugins.sort(sortPluginsByName);
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (plugin instanceof SandyPluginDefinition) {
|
||||||
|
if (plugin.asDevicePluginModule().supportsDevice(this as any)) {
|
||||||
|
this.devicePlugins.push(plugin.id);
|
||||||
|
this.sandyPluginStates.set(
|
||||||
|
plugin.id,
|
||||||
|
new SandyDevicePluginInstance(this, plugin),
|
||||||
|
); // TODO: pass initial state if applicable
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (plugin.supportsDevice(this)) {
|
||||||
|
this.devicePlugins.push(plugin.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue';
|
|||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
import {PluginDetails} from 'flipper-plugin-lib';
|
||||||
import {Settings} from './reducers/settings';
|
import {Settings} from './reducers/settings';
|
||||||
import {SandyPluginDefinition} from 'flipper-plugin';
|
import {SandyPluginDefinition} from 'flipper-plugin';
|
||||||
|
|
||||||
type Parameters = {[key: string]: any};
|
type Parameters = {[key: string]: any};
|
||||||
|
|
||||||
export type PluginDefinition = ClientPluginDefinition | DevicePluginDefinition;
|
export type PluginDefinition = ClientPluginDefinition | DevicePluginDefinition;
|
||||||
|
|
||||||
// TODO: T68738317 add SandyPluginDefinition
|
export type DevicePluginDefinition =
|
||||||
export type DevicePluginDefinition = typeof FlipperDevicePlugin;
|
| typeof FlipperDevicePlugin
|
||||||
|
| SandyPluginDefinition;
|
||||||
|
|
||||||
export type ClientPluginDefinition =
|
export type ClientPluginDefinition =
|
||||||
| typeof FlipperPlugin
|
| typeof FlipperPlugin
|
||||||
@@ -244,7 +246,7 @@ export class FlipperDevicePlugin<
|
|||||||
this.teardown();
|
this.teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
static supportsDevice(_device: BaseDevice) {
|
static supportsDevice(_device: BaseDevice): boolean {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'supportsDevice is unimplemented in FlipperDevicePlugin class',
|
'supportsDevice is unimplemented in FlipperDevicePlugin class',
|
||||||
);
|
);
|
||||||
|
|||||||
107
desktop/app/src/reducers/__tests__/sandydeviceplugins.node.tsx
Normal file
107
desktop/app/src/reducers/__tests__/sandydeviceplugins.node.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 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 {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
||||||
|
import {Store} from '../../';
|
||||||
|
import {selectPlugin} from '../../reducers/connections';
|
||||||
|
import {
|
||||||
|
SandyPluginDefinition,
|
||||||
|
SandyDevicePluginInstance,
|
||||||
|
DevicePluginClient,
|
||||||
|
TestUtils,
|
||||||
|
} from 'flipper-plugin';
|
||||||
|
|
||||||
|
interface PersistedState {
|
||||||
|
count: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginDetails = TestUtils.createMockPluginDetails();
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initialized = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function devicePlugin(client: DevicePluginClient) {
|
||||||
|
const activateStub = jest.fn();
|
||||||
|
const deactivateStub = jest.fn();
|
||||||
|
const destroyStub = jest.fn();
|
||||||
|
|
||||||
|
client.onActivate(activateStub);
|
||||||
|
client.onDeactivate(deactivateStub);
|
||||||
|
client.onDestroy(destroyStub);
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activateStub: activateStub,
|
||||||
|
deactivateStub: deactivateStub,
|
||||||
|
destroyStub,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const TestPlugin = new SandyPluginDefinition(pluginDetails, {
|
||||||
|
supportsDevice: jest.fn().mockImplementation(() => true),
|
||||||
|
devicePlugin: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(devicePlugin) as typeof devicePlugin,
|
||||||
|
Component: jest.fn().mockImplementation(() => null),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PluginApi = ReturnType<typeof devicePlugin>;
|
||||||
|
|
||||||
|
function selectTestPlugin(store: Store) {
|
||||||
|
store.dispatch(
|
||||||
|
selectPlugin({
|
||||||
|
selectedPlugin: TestPlugin.id,
|
||||||
|
selectedApp: null,
|
||||||
|
deepLinkPayload: null,
|
||||||
|
selectedDevice: store.getState().connections.selectedDevice!,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('it should initialize device sandy plugins', async () => {
|
||||||
|
const {device, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||||
|
|
||||||
|
// already started, so initialized immediately
|
||||||
|
expect(initialized).toBe(true);
|
||||||
|
expect(device.sandyPluginStates.get(TestPlugin.id)).toBeInstanceOf(
|
||||||
|
SandyDevicePluginInstance,
|
||||||
|
);
|
||||||
|
expect(TestPlugin.asDevicePluginModule().supportsDevice).toBeCalledTimes(1);
|
||||||
|
const instanceApi: PluginApi = device.sandyPluginStates.get(TestPlugin.id)!
|
||||||
|
.instanceApi;
|
||||||
|
|
||||||
|
expect(instanceApi.activateStub).toBeCalledTimes(0);
|
||||||
|
selectTestPlugin(store);
|
||||||
|
|
||||||
|
// without rendering, non-bg plugins won't connect automatically,
|
||||||
|
// so this isn't the best test, but PluginContainer tests do test that part of the lifecycle
|
||||||
|
device.sandyPluginStates.get(TestPlugin.id)!.activate();
|
||||||
|
expect(instanceApi.activateStub).toBeCalledTimes(1);
|
||||||
|
device.sandyPluginStates.get(TestPlugin.id)!.deactivate();
|
||||||
|
expect(instanceApi.deactivateStub).toBeCalledTimes(1);
|
||||||
|
expect(instanceApi.destroyStub).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should cleanup if device is removed', async () => {
|
||||||
|
const {device, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||||
|
const pluginInstance = device.sandyPluginStates.get(TestPlugin.id)!;
|
||||||
|
expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
// close device
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UNREGISTER_DEVICES',
|
||||||
|
payload: new Set([device.serial]),
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
(pluginInstance.instanceApi as PluginApi).destroyStub,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
@@ -233,5 +233,3 @@ test('it can send messages from sandy clients', async () => {
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: T68683449 state is persisted if a plugin connects and reconnects
|
|
||||||
|
|||||||
@@ -232,9 +232,14 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
|
|||||||
|
|
||||||
return updateSelection(
|
return updateSelection(
|
||||||
produce(state, (draft) => {
|
produce(state, (draft) => {
|
||||||
draft.devices = draft.devices.filter(
|
draft.devices = draft.devices.filter((device) => {
|
||||||
(device) => !deviceSerials.has(device.serial),
|
if (!deviceSerials.has(device.serial)) {
|
||||||
);
|
return true;
|
||||||
|
} else {
|
||||||
|
device.teardown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {Logger} from '../fb-interfaces/Logger';
|
|||||||
import {PluginDefinition} from '../plugin';
|
import {PluginDefinition} from '../plugin';
|
||||||
import {registerPlugins} from '../reducers/plugins';
|
import {registerPlugins} from '../reducers/plugins';
|
||||||
import PluginContainer from '../PluginContainer';
|
import PluginContainer from '../PluginContainer';
|
||||||
import {getPluginKey} from '../utils/pluginUtils';
|
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||||
import {getInstance} from '../fb-stubs/Logger';
|
import {getInstance} from '../fb-stubs/Logger';
|
||||||
|
|
||||||
type MockFlipperResult = {
|
type MockFlipperResult = {
|
||||||
@@ -75,6 +75,7 @@ export async function createMockFlipperWithPlugin(
|
|||||||
type: 'REGISTER_DEVICE',
|
type: 'REGISTER_DEVICE',
|
||||||
payload: device,
|
payload: device,
|
||||||
});
|
});
|
||||||
|
device.loadDevicePlugins(store.getState().plugins.devicePlugins);
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ export async function createMockFlipperWithPlugin(
|
|||||||
null, // create a stub connection to avoid this plugin to be archived?
|
null, // create a stub connection to avoid this plugin to be archived?
|
||||||
logger,
|
logger,
|
||||||
store,
|
store,
|
||||||
[pluginClazz.id],
|
isDevicePluginDefinition(pluginClazz) ? [] : [pluginClazz.id],
|
||||||
device,
|
device,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,10 +126,7 @@ export async function createMockFlipperWithPlugin(
|
|||||||
case 'getPlugins':
|
case 'getPlugins':
|
||||||
// assuming this plugin supports all plugins for now
|
// assuming this plugin supports all plugins for now
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [...store.getState().plugins.clientPlugins.keys()],
|
||||||
...store.getState().plugins.clientPlugins.keys(),
|
|
||||||
...store.getState().plugins.devicePlugins.keys(),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
case 'getBackgroundPlugins':
|
case 'getBackgroundPlugins':
|
||||||
return {plugins: []};
|
return {plugins: []};
|
||||||
@@ -142,6 +140,7 @@ export async function createMockFlipperWithPlugin(
|
|||||||
|
|
||||||
// enable the plugin
|
// enable the plugin
|
||||||
if (
|
if (
|
||||||
|
!isDevicePluginDefinition(pluginClazz) &&
|
||||||
!store
|
!store
|
||||||
.getState()
|
.getState()
|
||||||
.connections.userStarredPlugins[client.query.app]?.includes(
|
.connections.userStarredPlugins[client.query.app]?.includes(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {PluginNotification} from '../reducers/notifications';
|
|||||||
import Client, {ClientExport, ClientQuery} from '../Client';
|
import Client, {ClientExport, ClientQuery} from '../Client';
|
||||||
import {pluginKey} from '../reducers/pluginStates';
|
import {pluginKey} from '../reducers/pluginStates';
|
||||||
import {
|
import {
|
||||||
FlipperDevicePlugin,
|
|
||||||
callClient,
|
callClient,
|
||||||
supportsMethod,
|
supportsMethod,
|
||||||
PluginDefinition,
|
PluginDefinition,
|
||||||
@@ -82,7 +81,7 @@ type ProcessPluginStatesOptions = {
|
|||||||
clients: Array<ClientExport>;
|
clients: Array<ClientExport>;
|
||||||
serial: string;
|
serial: string;
|
||||||
allPluginStates: PluginStatesState;
|
allPluginStates: PluginStatesState;
|
||||||
devicePlugins: Map<string, typeof FlipperDevicePlugin>;
|
devicePlugins: DevicePluginMap;
|
||||||
selectedPlugins: Array<string>;
|
selectedPlugins: Array<string>;
|
||||||
statusUpdate?: (msg: string) => void;
|
statusUpdate?: (msg: string) => void;
|
||||||
};
|
};
|
||||||
@@ -91,7 +90,7 @@ type ProcessNotificationStatesOptions = {
|
|||||||
clients: Array<ClientExport>;
|
clients: Array<ClientExport>;
|
||||||
serial: string;
|
serial: string;
|
||||||
allActiveNotifications: Array<PluginNotification>;
|
allActiveNotifications: Array<PluginNotification>;
|
||||||
devicePlugins: Map<string, typeof FlipperDevicePlugin>;
|
devicePlugins: DevicePluginMap;
|
||||||
statusUpdate?: (msg: string) => void;
|
statusUpdate?: (msg: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {State as PluginStatesState} from '../reducers/pluginStates';
|
|||||||
import {State as PluginsState} from '../reducers/plugins';
|
import {State as PluginsState} from '../reducers/plugins';
|
||||||
import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue';
|
import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue';
|
||||||
import {deconstructPluginKey, deconstructClientId} from './clientUtils';
|
import {deconstructPluginKey, deconstructClientId} from './clientUtils';
|
||||||
|
import {SandyPluginDefinition} from 'flipper-plugin';
|
||||||
|
|
||||||
type Client = import('../Client').default;
|
type Client = import('../Client').default;
|
||||||
|
|
||||||
@@ -231,7 +232,8 @@ export function sortPluginsByName(
|
|||||||
export function isDevicePluginDefinition(
|
export function isDevicePluginDefinition(
|
||||||
definition: PluginDefinition,
|
definition: PluginDefinition,
|
||||||
): definition is DevicePluginDefinition {
|
): definition is DevicePluginDefinition {
|
||||||
// TODO: support Sandy device plugins T68738317
|
return (
|
||||||
// @ts-ignore
|
(definition as any).prototype instanceof FlipperDevicePlugin ||
|
||||||
return definition.prototype instanceof FlipperDevicePlugin;
|
(definition instanceof SandyPluginDefinition && definition.isDevicePlugin)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export {
|
|||||||
DeviceLogListener,
|
DeviceLogListener,
|
||||||
DevicePluginClient,
|
DevicePluginClient,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
SandyDevicePluginInstance,
|
||||||
} from './plugin/DevicePlugin';
|
} from './plugin/DevicePlugin';
|
||||||
export {SandyPluginDefinition} from './plugin/SandyPluginDefinition';
|
export {SandyPluginDefinition} from './plugin/SandyPluginDefinition';
|
||||||
export {SandyPluginRenderer} from './plugin/PluginRenderer';
|
export {SandyPluginRenderer} from './plugin/PluginRenderer';
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
ContextMenu,
|
ContextMenu,
|
||||||
clipboard,
|
clipboard,
|
||||||
Button,
|
Button,
|
||||||
FlipperPlugin,
|
|
||||||
getPluginKey,
|
getPluginKey,
|
||||||
getPersistedState,
|
getPersistedState,
|
||||||
BaseDevice,
|
BaseDevice,
|
||||||
@@ -239,10 +238,14 @@ export function parseCrashLogAndUpdateState(
|
|||||||
| typeof FlipperBasePlugin
|
| typeof FlipperBasePlugin
|
||||||
| undefined = store
|
| undefined = store
|
||||||
.getState()
|
.getState()
|
||||||
.plugins.devicePlugins.get(CrashReporterPlugin.id);
|
.plugins.devicePlugins.get(CrashReporterPlugin.id) as any;
|
||||||
if (!persistingPlugin) {
|
if (!persistingPlugin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!persistingPlugin.persistedStateReducer) {
|
||||||
|
console.error('CrashReporterPlugin is incompatible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pluginStates = store.getState().pluginStates;
|
const pluginStates = store.getState().pluginStates;
|
||||||
const persistedState = getPersistedState(
|
const persistedState = getPersistedState(
|
||||||
pluginKey,
|
pluginKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user