diff --git a/desktop/app/src/Client.tsx b/desktop/app/src/Client.tsx index 7e8c492e0..68a9108aa 100644 --- a/desktop/app/src/Client.tsx +++ b/desktop/app/src/Client.tsx @@ -268,8 +268,8 @@ export default class Client extends EventEmitter { }); } - supportsPlugin(Plugin: ClientPluginDefinition): boolean { - return this.plugins.includes(Plugin.id); + supportsPlugin(pluginId: string): boolean { + return this.plugins.includes(pluginId); } isBackgroundPlugin(pluginId: string) { diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx index ed8686d1b..04bdc3a63 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -13,7 +13,7 @@ import dispatcher, { getDynamicPlugins, checkDisabled, checkGK, - requirePlugin, + createRequirePluginFunction, filterNewestVersionOfEachPlugin, } from '../plugins'; import {PluginDetails} from 'flipper-plugin-lib'; @@ -136,7 +136,7 @@ test('checkGK for failing plugin', () => { }); test('requirePlugin returns null for invalid requires', () => { - const requireFn = requirePlugin([], {}, require); + const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ ...samplePluginDetails, name: 'pluginID', @@ -149,7 +149,7 @@ test('requirePlugin returns null for invalid requires', () => { test('requirePlugin loads plugin', () => { const name = 'pluginID'; - const requireFn = requirePlugin([], {}, require); + const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ ...samplePluginDetails, name, @@ -236,7 +236,7 @@ test('bundled versions are used when env var FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE test('requirePlugin loads valid Sandy plugin', () => { const name = 'pluginID'; - const requireFn = requirePlugin([], {}, require); + const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ ...samplePluginDetails, name, @@ -270,7 +270,7 @@ test('requirePlugin loads valid Sandy plugin', () => { test('requirePlugin errors on invalid Sandy plugin', () => { const name = 'pluginID'; const failedPlugins: any[] = []; - const requireFn = requirePlugin(failedPlugins, {}, require); + const requireFn = createRequirePluginFunction(failedPlugins, require); requireFn({ ...samplePluginDetails, name, @@ -286,7 +286,7 @@ test('requirePlugin errors on invalid Sandy plugin', () => { test('requirePlugin loads valid Sandy Device plugin', () => { const name = 'pluginID'; - const requireFn = requirePlugin([], {}, require); + const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ ...samplePluginDetails, name, diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index 2bd94f936..304381664 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -37,7 +37,9 @@ import loadDynamicPlugins from '../utils/loadDynamicPlugins'; import Immer from 'immer'; // eslint-disable-next-line import/no-unresolved -import getPluginIndex from '../utils/getDefaultPluginsIndex'; +import getDefaultPluginsIndex from '../utils/getDefaultPluginsIndex'; + +let defaultPluginsIndex: any = null; export default async (store: Store, logger: Logger) => { // expose Flipper and exact globally for dynamically loaded plugins @@ -53,7 +55,7 @@ export default async (store: Store, logger: Logger) => { const disabledPlugins: Array = []; const failedPlugins: Array<[PluginDetails, string]> = []; - const defaultPluginsIndex = getPluginIndex(); + defaultPluginsIndex = getDefaultPluginsIndex(); const initialPlugins: PluginDefinition[] = filterNewestVersionOfEachPlugin( getBundledPlugins(), @@ -62,7 +64,7 @@ export default async (store: Store, logger: Logger) => { .map(reportVersion) .filter(checkDisabled(disabledPlugins)) .filter(checkGK(gatekeepedPlugins)) - .map(requirePlugin(failedPlugins, defaultPluginsIndex)) + .map(createRequirePluginFunction(failedPlugins)) .filter(notNull); store.dispatch(addGatekeepedPlugins(gatekeepedPlugins)); @@ -173,18 +175,13 @@ export const checkDisabled = (disabledPlugins: Array) => ( return !disabledList.has(plugin.name); }; -export const requirePlugin = ( +export const createRequirePluginFunction = ( failedPlugins: Array<[PluginDetails, string]>, - defaultPluginsIndex: any, reqFn: Function = global.electronRequire, ) => { return (pluginDetails: PluginDetails): PluginDefinition | null => { try { - return tryCatchReportPluginFailures( - () => requirePluginInternal(pluginDetails, defaultPluginsIndex, reqFn), - 'plugin:load', - pluginDetails.id, - ); + return requirePlugin(pluginDetails, reqFn); } catch (e) { failedPlugins.push([pluginDetails, e.message]); console.error(`Plugin ${pluginDetails.id} failed to load`, e); @@ -193,15 +190,24 @@ export const requirePlugin = ( }; }; +export const requirePlugin = ( + pluginDetails: PluginDetails, + reqFn: Function = global.electronRequire, +): PluginDefinition => { + return tryCatchReportPluginFailures( + () => requirePluginInternal(pluginDetails, reqFn), + 'plugin:load', + pluginDetails.id, + ); +}; + const requirePluginInternal = ( pluginDetails: PluginDetails, - defaultPluginsIndex: any, reqFn: Function = global.electronRequire, -) => { +): PluginDefinition => { let plugin = pluginDetails.isDefault ? defaultPluginsIndex[pluginDetails.name] : reqFn(pluginDetails.entry); - if (pluginDetails.flipperSDKVersion) { // Sandy plugin return new SandyPluginDefinition(pluginDetails, plugin); diff --git a/desktop/app/src/reducers/__tests__/notifications.node.tsx b/desktop/app/src/reducers/__tests__/notifications.node.tsx index 2c100e02c..f118d6039 100644 --- a/desktop/app/src/reducers/__tests__/notifications.node.tsx +++ b/desktop/app/src/reducers/__tests__/notifications.node.tsx @@ -7,7 +7,7 @@ * @format */ -import {State, addNotification} from '../notifications'; +import {State, addNotification, removeNotification} from '../notifications'; import { default as reducer, @@ -108,6 +108,56 @@ test('reduce setActiveNotifications', () => { }); test('addNotification removes duplicates', () => { + let res = reducer( + getInitialState(), + addNotification({ + pluginId: 'test', + client: null, + notification, + }), + ); + res = reducer( + res, + addNotification({ + pluginId: 'test', + client: null, + notification: { + ...notification, + id: 'otherId', + }, + }), + ); + res = reducer( + res, + removeNotification({ + pluginId: 'test', + client: null, + notificationId: 'id', + }), + ); + expect(res).toMatchInlineSnapshot(` + Object { + "activeNotifications": Array [ + Object { + "client": null, + "notification": Object { + "id": "otherId", + "message": "message", + "severity": "warning", + "title": "title", + }, + "pluginId": "test", + }, + ], + "blacklistedCategories": Array [], + "blacklistedPlugins": Array [], + "clearedNotifications": Set {}, + "invalidatedNotifications": Array [], + } + `); +}); + +test('reduce removeNotification', () => { let res = reducer( getInitialState(), addNotification({ diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index 5baed1a24..9a6800d57 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -125,7 +125,12 @@ export type Action = type: 'SELECT_CLIENT'; payload: string; } - | RegisterPluginAction; + | RegisterPluginAction + | { + // Implemented by rootReducer in `store.tsx` + type: 'UPDATE_PLUGIN'; + payload: PluginDefinition; + }; const DEFAULT_PLUGIN = 'DeviceLogs'; const DEFAULT_DEVICE_BLACKLIST = [MacDevice]; @@ -341,7 +346,6 @@ export default (state: State = INITAL_STATE, action: Actions): State => { }); return state; } - default: return state; } @@ -395,6 +399,11 @@ export const selectClient = (clientId: string): Action => ({ payload: clientId, }); +export const registerPluginUpdate = (payload: PluginDefinition): Action => ({ + type: 'UPDATE_PLUGIN', + payload, +}); + export function getAvailableClients( device: null | undefined | BaseDevice, clients: Client[], diff --git a/desktop/app/src/reducers/notifications.tsx b/desktop/app/src/reducers/notifications.tsx index 671b14922..fa3192d52 100644 --- a/desktop/app/src/reducers/notifications.tsx +++ b/desktop/app/src/reducers/notifications.tsx @@ -16,6 +16,12 @@ export type PluginNotification = { client: null | string; }; +export type PluginNotificationReference = { + notificationId: string; + pluginId: string; + client: null | string; +}; + export type State = { activeNotifications: Array; invalidatedNotifications: Array; @@ -56,6 +62,10 @@ export type Action = | { type: 'ADD_NOTIFICATION'; payload: PluginNotification; + } + | { + type: 'REMOVE_NOTIFICATION'; + payload: PluginNotificationReference; }; const INITIAL_STATE: State = { @@ -113,6 +123,18 @@ export default function reducer( action.payload, ], }; + case 'REMOVE_NOTIFICATION': + return { + ...state, + activeNotifications: [ + ...state.activeNotifications.filter( + (notif) => + notif.client !== action.payload.client || + notif.pluginId !== action.payload.pluginId || + notif.notification.id !== action.payload.notificationId, + ), + ], + }; default: return state; } @@ -166,6 +188,15 @@ export function addNotification(payload: PluginNotification): Action { }; } +export function removeNotification( + payload: PluginNotificationReference, +): Action { + return { + type: 'REMOVE_NOTIFICATION', + payload, + }; +} + export function addErrorNotification( title: string, message: string, diff --git a/desktop/app/src/store.tsx b/desktop/app/src/store.tsx index ebaae244d..63ec8a9c6 100644 --- a/desktop/app/src/store.tsx +++ b/desktop/app/src/store.tsx @@ -15,7 +15,11 @@ import produce from 'immer'; import { defaultEnabledBackgroundPlugins, getPluginKey, + isDevicePluginDefinition, } from './utils/pluginUtils'; +import Client from './Client'; +import {PluginDefinition} from './plugin'; +import {deconstructPluginKey} from './utils/clientUtils'; export const store: Store = createStore( rootReducer, @@ -48,35 +52,59 @@ export function rootReducer( plugins.push(selectedPlugin); // enabling a plugin on one device enables it on all... clients.forEach((client) => { - // sandy plugin? initialize it - client.startPluginIfNeeded(plugin, true); - // background plugin? connect it needed - if ( - !defaultEnabledBackgroundPlugins.includes(selectedPlugin) && - client?.isBackgroundPlugin(selectedPlugin) - ) { - client.initPlugin(selectedPlugin); - } + startPlugin(client, plugin); }); } else { plugins.splice(idx, 1); // enabling a plugin on one device disables it on all... clients.forEach((client) => { - // disconnect background plugins - if ( - !defaultEnabledBackgroundPlugins.includes(selectedPlugin) && - client?.isBackgroundPlugin(selectedPlugin) - ) { - client.deinitPlugin(selectedPlugin); - } - // stop sandy plugins - client.stopPluginIfNeeded(plugin.id); - delete draft.pluginMessageQueue[ - getPluginKey(client.id, {serial: client.query.device_id}, plugin.id) - ]; + stopPlugin(client, plugin.id); + const pluginKey = getPluginKey( + client.id, + {serial: client.query.device_id}, + plugin.id, + ); + delete draft.pluginMessageQueue[pluginKey]; }); } }); + } else if (action.type === 'UPDATE_PLUGIN' && state) { + const plugin: PluginDefinition = action.payload; + const clients = state.connections.clients; + return produce(state, (draft) => { + const clientsWithEnabledPlugin = clients.filter((c) => { + return ( + c.supportsPlugin(plugin.id) && + state.connections.userStarredPlugins[c.query.app]?.includes(plugin.id) + ); + }); + // stop plugin for each client where it is enabled + clientsWithEnabledPlugin.forEach((client) => { + stopPlugin(client, plugin.id, true); + delete draft.pluginMessageQueue[ + getPluginKey(client.id, {serial: client.query.device_id}, plugin.id) + ]; + }); + // cleanup classic plugin state + Object.keys(draft.pluginStates).forEach((pluginKey) => { + const pluginKeyParts = deconstructPluginKey(pluginKey); + if (pluginKeyParts.pluginName === plugin.id) { + delete draft.pluginStates[pluginKey]; + } + }); + // update plugin definition + const {devicePlugins, clientPlugins} = draft.plugins; + const p = action.payload; + if (isDevicePluginDefinition(p)) { + devicePlugins.set(p.id, p); + } else { + clientPlugins.set(p.id, p); + } + // start plugin for each client + clientsWithEnabledPlugin.forEach((client) => { + startPlugin(client, plugin, true); + }); + }); } // otherwise @@ -88,3 +116,36 @@ if (!isProduction()) { // @ts-ignore window.flipperStore = store; } + +function stopPlugin( + client: Client, + pluginId: string, + forceInitBackgroundPlugin: boolean = false, +): boolean { + if ( + (forceInitBackgroundPlugin || + !defaultEnabledBackgroundPlugins.includes(pluginId)) && + client?.isBackgroundPlugin(pluginId) + ) { + client.deinitPlugin(pluginId); + } + // stop sandy plugins + client.stopPluginIfNeeded(pluginId); + return true; +} + +function startPlugin( + client: Client, + plugin: PluginDefinition, + forceInitBackgroundPlugin: boolean = false, +) { + client.startPluginIfNeeded(plugin, true); + // background plugin? connect it needed + if ( + (forceInitBackgroundPlugin || + !defaultEnabledBackgroundPlugins.includes(plugin.id)) && + client?.isBackgroundPlugin(plugin.id) + ) { + client.initPlugin(plugin.id); + } +} diff --git a/desktop/plugin-lib/src/getNpmHostedPlugins.ts b/desktop/plugin-lib/src/getNpmHostedPlugins.ts index 200403498..a986105d5 100644 --- a/desktop/plugin-lib/src/getNpmHostedPlugins.ts +++ b/desktop/plugin-lib/src/getNpmHostedPlugins.ts @@ -28,7 +28,7 @@ export type NpmHostedPluginsSearchArgs = { export async function getNpmHostedPlugins( args: NpmHostedPluginsSearchArgs = {}, -): Promise { +): Promise { const index = provideSearchIndex(); args = Object.assign( { diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index 7f9d69fbc..bfbeea6dc 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -93,7 +93,7 @@ async function installPluginFromTempDir( } throw err; } - return pluginDetails; + return await getPluginDetails(destinationDir); } async function getPluginRootDir(dir: string) {