diff --git a/desktop/app/src/dispatcher/pluginManager.tsx b/desktop/app/src/dispatcher/pluginManager.tsx index 4bcb95c06..3ad6d1c2c 100644 --- a/desktop/app/src/dispatcher/pluginManager.tsx +++ b/desktop/app/src/dispatcher/pluginManager.tsx @@ -8,12 +8,14 @@ */ import type {Store} from '../reducers/index'; -import type {Logger} from '../fb-interfaces/Logger'; import {clearPluginState} from '../reducers/pluginStates'; +import type {Logger} from '../fb-interfaces/Logger'; import { LoadPluginActionPayload, - pluginCommandsProcessed, + PluginCommand, UninstallPluginActionPayload, + UpdatePluginActionPayload, + pluginCommandsProcessed, } from '../reducers/pluginManager'; import { getInstalledPlugins, @@ -23,11 +25,23 @@ import { } from 'flipper-plugin-lib'; import {sideEffect} from '../utils/sideEffect'; import {requirePlugin} from './plugins'; -import {registerPluginUpdate} from '../reducers/connections'; import {showErrorNotification} from '../utils/notifications'; +import { + DevicePluginDefinition, + FlipperDevicePlugin, + FlipperPlugin, + PluginDefinition, +} from '../plugin'; import type Client from '../Client'; import {unloadModule} from '../utils/electronModuleCache'; -import {pluginUninstalled, registerInstalledPlugins} from '../reducers/plugins'; +import { + pluginLoaded, + pluginUninstalled, + registerInstalledPlugins, +} from '../reducers/plugins'; +import {_SandyPluginDefinition} from 'flipper-plugin'; +import type BaseDevice from '../devices/BaseDevice'; +import {pluginStarred} from '../reducers/connections'; import {defaultEnabledBackgroundPlugins} from '../utils/pluginUtils'; const maxInstalledPluginVersionsToKeep = 2; @@ -65,46 +79,49 @@ export default ( noTimeBudgetWarns: true, // These side effects are critical, so we're doing them with zero throttling and want to avoid unnecessary warns }, (state) => state.pluginManager.pluginCommandsQueue, - (queue, store) => { - for (const command of queue) { - switch (command.type) { - case 'LOAD_PLUGIN': - loadPlugin(store, command.payload); - break; - case 'UNINSTALL_PLUGIN': - uninstallPlugin(store, command.payload); - break; - default: - console.error('Unexpected plugin command', command); - break; - } - } - store.dispatch(pluginCommandsProcessed(queue.length)); - }, + processPluginCommandsQueue, ); return async () => { unsubscribeHandlePluginCommands(); }; }; +export function processPluginCommandsQueue( + queue: PluginCommand[], + store: Store, +) { + for (const command of queue) { + switch (command.type) { + case 'LOAD_PLUGIN': + loadPlugin(store, command.payload); + break; + case 'UNINSTALL_PLUGIN': + uninstallPlugin(store, command.payload); + break; + case 'UPDATE_PLUGIN': + updatePlugin(store, command.payload); + break; + default: + console.error('Unexpected plugin command', command); + break; + } + } + store.dispatch(pluginCommandsProcessed(queue.length)); +} + function loadPlugin(store: Store, payload: LoadPluginActionPayload) { try { const plugin = requirePlugin(payload.plugin); const enablePlugin = payload.enable; - store.dispatch( - registerPluginUpdate({ - plugin, - enablePlugin, - }), - ); + updatePlugin(store, {plugin, enablePlugin}); } catch (err) { console.error( - `Failed to activate plugin ${payload.plugin.title} v${payload.plugin.version}`, + `Failed to load plugin ${payload.plugin.title} v${payload.plugin.version}`, err, ); if (payload.notifyIfFailed) { showErrorNotification( - `Failed to activate plugin "${payload.plugin.title}" v${payload.plugin.version}`, + `Failed to load plugin "${payload.plugin.title}" v${payload.plugin.version}`, ); } } @@ -133,6 +150,83 @@ function uninstallPlugin(store: Store, {plugin}: UninstallPluginActionPayload) { } } +function updatePlugin(store: Store, payload: UpdatePluginActionPayload) { + const {plugin, enablePlugin} = payload; + if (isDevicePluginDefinition(plugin)) { + return updateDevicePlugin(store, plugin); + } else { + return updateClientPlugin(store, plugin, enablePlugin); + } +} + +function updateClientPlugin( + store: Store, + plugin: typeof FlipperPlugin, + enable: boolean, +) { + const clients = store.getState().connections.clients; + if (enable) { + store.dispatch(pluginStarred(plugin)); + } + const clientsWithEnabledPlugin = clients.filter((c) => { + return ( + c.supportsPlugin(plugin.id) && + store + .getState() + .connections.userStarredPlugins[c.query.app]?.includes(plugin.id) + ); + }); + const previousVersion = store.getState().plugins.clientPlugins.get(plugin.id); + clientsWithEnabledPlugin.forEach((client) => { + stopPlugin(client, plugin.id); + }); + store.dispatch(clearPluginState({pluginId: plugin.id})); + clientsWithEnabledPlugin.forEach((client) => { + startPlugin(client, plugin, true); + }); + store.dispatch(pluginLoaded(plugin)); + if (previousVersion) { + // unload previous version from Electron cache + unloadPluginModule(previousVersion.details); + } +} + +function updateDevicePlugin(store: Store, plugin: DevicePluginDefinition) { + const devices = store.getState().connections.devices; + const devicesWithEnabledPlugin = devices.filter((d) => + supportsDevice(plugin, d), + ); + devicesWithEnabledPlugin.forEach((d) => { + d.unloadDevicePlugin(plugin.id); + }); + store.dispatch(clearPluginState({pluginId: plugin.id})); + const previousVersion = store.getState().plugins.clientPlugins.get(plugin.id); + if (previousVersion) { + // unload previous version from Electron cache + unloadPluginModule(previousVersion.details); + } + store.dispatch(pluginLoaded(plugin)); + devicesWithEnabledPlugin.forEach((d) => { + d.loadDevicePlugin(plugin); + }); +} + +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); + } +} + function stopPlugin( client: Client, pluginId: string, @@ -157,3 +251,23 @@ function unloadPluginModule(plugin: ActivatablePluginDetails) { } unloadModule(plugin.entry); } + +export function isDevicePluginDefinition( + definition: PluginDefinition, +): definition is DevicePluginDefinition { + return ( + (definition as any).prototype instanceof FlipperDevicePlugin || + (definition instanceof _SandyPluginDefinition && definition.isDevicePlugin) + ); +} + +function supportsDevice(plugin: DevicePluginDefinition, device: BaseDevice) { + if (plugin instanceof _SandyPluginDefinition) { + return ( + plugin.isDevicePlugin && + plugin.asDevicePluginModule().supportsDevice(device as any) + ); + } else { + return plugin.supportsDevice(device); + } +} diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index 2ea1d1339..3d1bdf23f 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -116,19 +116,23 @@ export type Action = plugin: PluginDefinition; }; } + | { + type: 'PLUGIN_STARRED'; + payload: { + plugin: PluginDefinition; + }; + } + | { + type: 'PLUGIN_UNSTARRED'; + payload: { + plugin: PluginDefinition; + }; + } | { type: 'SELECT_CLIENT'; payload: string | null; } - | RegisterPluginAction - | { - // Implemented by rootReducer in `store.tsx` - type: 'UPDATE_PLUGIN'; - payload: { - plugin: PluginDefinition; - enablePlugin: boolean; - }; - }; + | RegisterPluginAction; const DEFAULT_PLUGIN = 'DeviceLogs'; const DEFAULT_DEVICE_BLACKLIST = [MacDevice, MetroDevice]; @@ -367,6 +371,44 @@ export default (state: State = INITAL_STATE, action: Actions): State => { }); return state; } + case 'PLUGIN_STARRED': { + const {plugin} = action.payload; + const selectedPlugin = plugin.id; + const selectedApp = state.selectedApp + ? deconstructClientId(state.selectedApp).app + : undefined; + if (!selectedApp) { + return state; + } + return produce(state, (draft) => { + if (!draft.userStarredPlugins[selectedApp]) { + draft.userStarredPlugins[selectedApp] = []; + } + const plugins = draft.userStarredPlugins[selectedApp]; + const idx = plugins.indexOf(selectedPlugin); + if (idx === -1) { + plugins.push(selectedPlugin); + } + }); + } + case 'PLUGIN_UNSTARRED': { + const {plugin} = action.payload; + const selectedPlugin = plugin.id; + const selectedApp = state.selectedApp; + if (!selectedApp) { + return state; + } + return produce(state, (draft) => { + if (!draft.userStarredPlugins[selectedApp]) { + draft.userStarredPlugins[selectedApp] = []; + } + const plugins = draft.userStarredPlugins[selectedApp]; + const idx = plugins.indexOf(selectedPlugin); + if (idx !== -1) { + plugins.splice(idx, 1); + } + }); + } default: return state; } @@ -420,12 +462,18 @@ export const selectClient = (clientId: string | null): Action => ({ payload: clientId, }); -export const registerPluginUpdate = (payload: { - plugin: PluginDefinition; - enablePlugin: boolean; -}): Action => ({ - type: 'UPDATE_PLUGIN', - payload, +export const pluginStarred = (plugin: PluginDefinition): Action => ({ + type: 'PLUGIN_STARRED', + payload: { + plugin, + }, +}); + +export const pluginUnstarred = (plugin: PluginDefinition): Action => ({ + type: 'PLUGIN_UNSTARRED', + payload: { + plugin, + }, }); export function getAvailableClients( diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index ee7409a96..8dfd8c80c 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -16,7 +16,10 @@ export type State = { pluginCommandsQueue: PluginCommand[]; }; -export type PluginCommand = LoadPluginAction | UninstallPluginAction; +export type PluginCommand = + | LoadPluginAction + | UninstallPluginAction + | UpdatePluginAction; export type LoadPluginActionPayload = { plugin: ActivatablePluginDetails; @@ -38,6 +41,16 @@ export type UninstallPluginAction = { payload: UninstallPluginActionPayload; }; +export type UpdatePluginActionPayload = { + plugin: PluginDefinition; + enablePlugin: boolean; +}; + +export type UpdatePluginAction = { + type: 'UPDATE_PLUGIN'; + payload: UpdatePluginActionPayload; +}; + export type Action = | { type: 'PLUGIN_COMMANDS_PROCESSED'; @@ -56,6 +69,7 @@ export default function reducer( switch (action.type) { case 'LOAD_PLUGIN': case 'UNINSTALL_PLUGIN': + case 'UPDATE_PLUGIN': return produce(state, (draft) => { draft.pluginCommandsQueue.push(action); }); @@ -84,3 +98,10 @@ export const pluginCommandsProcessed = (payload: number): Action => ({ type: 'PLUGIN_COMMANDS_PROCESSED', payload, }); + +export const registerPluginUpdate = ( + payload: UpdatePluginActionPayload, +): Action => ({ + type: 'UPDATE_PLUGIN', + payload, +}); diff --git a/desktop/app/src/reducers/plugins.tsx b/desktop/app/src/reducers/plugins.tsx index 769363953..e564d9890 100644 --- a/desktop/app/src/reducers/plugins.tsx +++ b/desktop/app/src/reducers/plugins.tsx @@ -83,6 +83,10 @@ export type Action = | { type: 'PLUGIN_UNINSTALLED'; payload: ActivatablePluginDetails; + } + | { + type: 'PLUGIN_LOADED'; + payload: PluginDefinition; }; const INITIAL_STATE: State = { @@ -178,6 +182,17 @@ export default function reducer( draft.loadedPlugins.delete(plugin.id); draft.uninstalledPlugins.add(plugin.name); }); + } else if (action.type === 'PLUGIN_LOADED') { + const plugin = action.payload; + return produce(state, (draft) => { + if (isDevicePluginDefinition(plugin)) { + draft.devicePlugins.set(plugin.id, plugin); + } else { + draft.clientPlugins.set(plugin.id, plugin); + } + draft.uninstalledPlugins.delete(plugin.id); + draft.loadedPlugins.set(plugin.id, plugin.details); + }); } else { return state; } @@ -253,3 +268,8 @@ export const pluginUninstalled = ( type: 'PLUGIN_UNINSTALLED', payload, }); + +export const pluginLoaded = (payload: PluginDefinition): Action => ({ + type: 'PLUGIN_LOADED', + payload, +}); diff --git a/desktop/app/src/store.tsx b/desktop/app/src/store.tsx index 16c09dc0a..3eac463dd 100644 --- a/desktop/app/src/store.tsx +++ b/desktop/app/src/store.tsx @@ -15,21 +15,10 @@ import produce from 'immer'; import { defaultEnabledBackgroundPlugins, getPluginKey, - isDevicePluginDefinition, } from './utils/pluginUtils'; import Client from './Client'; -import { - DevicePluginDefinition, - FlipperPlugin, - PluginDefinition, -} from './plugin'; -import {deconstructPluginKey} from './utils/clientUtils'; +import {PluginDefinition} from './plugin'; import {_SandyPluginDefinition} from 'flipper-plugin'; -import BaseDevice from './devices/BaseDevice'; -import {State as PluginStates} from './reducers/pluginStates'; -import {ActivatablePluginDetails} from 'flipper-plugin-lib'; -import {unloadModule} from './utils/electronModuleCache'; - export const store: Store = createStore( rootReducer, // @ts-ignore Type definition mismatch @@ -77,13 +66,6 @@ export function rootReducer( }); } }); - } else if (action.type === 'UPDATE_PLUGIN' && state) { - const {plugin, enablePlugin} = action.payload; - if (isDevicePluginDefinition(plugin)) { - return updateDevicePlugin(state, plugin); - } else { - return updateClientPlugin(state, plugin, enablePlugin); - } } // otherwise @@ -128,117 +110,3 @@ function startPlugin( client.initPlugin(plugin.id); } } - -function updateClientPlugin( - state: StoreState, - plugin: typeof FlipperPlugin, - enable: boolean, -) { - const clients = state.connections.clients; - return produce(state, (draft) => { - if (enable) { - clients.forEach((c) => { - let enabledPlugins = draft.connections.userStarredPlugins[c.query.app]; - if ( - c.supportsPlugin(plugin.id) && - !enabledPlugins?.includes(plugin.id) - ) { - if (!enabledPlugins) { - enabledPlugins = [plugin.id]; - draft.connections.userStarredPlugins[c.query.app] = enabledPlugins; - } else { - enabledPlugins.push(plugin.id); - } - } - }); - } - const clientsWithEnabledPlugin = clients.filter((c) => { - return ( - c.supportsPlugin(plugin.id) && - draft.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) - ]; - }); - cleanupPluginStates(draft.pluginStates, plugin.id); - const previousVersion = draft.plugins.clientPlugins.get(plugin.id); - if (previousVersion) { - // unload previous version from Electron cache - unloadPluginModule(previousVersion.details); - } - // update plugin definition - draft.plugins.clientPlugins.set(plugin.id, plugin); - // start plugin for each client - clientsWithEnabledPlugin.forEach((client) => { - startPlugin(client, plugin, true); - }); - registerLoadedPlugin(draft, plugin.details); - }); -} - -function updateDevicePlugin(state: StoreState, plugin: DevicePluginDefinition) { - const devices = state.connections.devices; - return produce(state, (draft) => { - const devicesWithEnabledPlugin = devices.filter((d) => - supportsDevice(plugin, d), - ); - devicesWithEnabledPlugin.forEach((d) => { - d.unloadDevicePlugin(plugin.id); - }); - cleanupPluginStates(draft.pluginStates, plugin.id); - const previousVersion = draft.plugins.devicePlugins.get(plugin.id); - if (previousVersion) { - // unload previous version from Electron cache - unloadPluginModule(previousVersion.details); - } - draft.plugins.devicePlugins.set(plugin.id, plugin); - devicesWithEnabledPlugin.forEach((d) => { - d.loadDevicePlugin(plugin); - }); - registerLoadedPlugin(draft, plugin.details); - }); -} - -function registerLoadedPlugin( - draft: { - pluginManager: StoreState['pluginManager']; - plugins: StoreState['plugins']; - }, - plugin: ActivatablePluginDetails, -) { - draft.plugins.uninstalledPlugins.delete(plugin.name); - draft.plugins.loadedPlugins.set(plugin.id, plugin); -} - -function supportsDevice(plugin: DevicePluginDefinition, device: BaseDevice) { - if (plugin instanceof _SandyPluginDefinition) { - return ( - plugin.isDevicePlugin && - plugin.asDevicePluginModule().supportsDevice(device as any) - ); - } else { - return plugin.supportsDevice(device); - } -} - -function cleanupPluginStates(pluginStates: PluginStates, pluginId: string) { - Object.keys(pluginStates).forEach((pluginKey) => { - const pluginKeyParts = deconstructPluginKey(pluginKey); - if (pluginKeyParts.pluginName === pluginId) { - delete pluginStates[pluginKey]; - } - }); -} - -function unloadPluginModule(plugin: ActivatablePluginDetails) { - if (plugin.isBundled) { - // We cannot unload bundled plugin. - return; - } - unloadModule(plugin.entry); -}