diff --git a/desktop/app/src/dispatcher/pluginManager.tsx b/desktop/app/src/dispatcher/pluginManager.tsx index f990ae66e..81b45b1c5 100644 --- a/desktop/app/src/dispatcher/pluginManager.tsx +++ b/desktop/app/src/dispatcher/pluginManager.tsx @@ -9,11 +9,17 @@ import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; -import {registerInstalledPlugins} from '../reducers/pluginManager'; +import { + pluginFilesRemoved, + registerInstalledPlugins, +} from '../reducers/pluginManager'; import { getInstalledPlugins, cleanupOldInstalledPluginVersions, + removePlugin, } from 'flipper-plugin-lib'; +import {sideEffect} from '../utils/sideEffect'; +import pMap from 'p-map'; const maxInstalledPluginVersionsToKeep = 2; @@ -28,4 +34,26 @@ export default (store: Store, _logger: Logger) => { window.requestIdleCallback(() => { refreshInstalledPlugins(store); }); + + sideEffect( + store, + { + name: 'removeUninstalledPluginFiles', + throttleMs: 1000, + fireImmediately: true, + }, + (state) => state.pluginManager.removedPlugins, + (removedPlugins) => { + pMap(removedPlugins, (p) => { + removePlugin(p.name) + .then(() => pluginFilesRemoved(p)) + .catch((e) => + console.error( + `Error while removing files of uninstalled plugin ${p.title}`, + e, + ), + ); + }).then(() => refreshInstalledPlugins(store)); + }, + ); }; diff --git a/desktop/app/src/plugins/TableNativePlugin.tsx b/desktop/app/src/plugins/TableNativePlugin.tsx index c05cbf851..1ac9d0d73 100644 --- a/desktop/app/src/plugins/TableNativePlugin.tsx +++ b/desktop/app/src/plugins/TableNativePlugin.tsx @@ -268,7 +268,7 @@ export default function createTableNativePlugin(id: string, title: string) { source: '', main: '', entry: '', - isDefault: false, + isDefault: true, }; static defaultPersistedState: PersistedState = { diff --git a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx index 3548a5171..4b0d3e950 100644 --- a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx @@ -12,7 +12,7 @@ import {PluginDetails} from 'flipper-plugin-lib'; test('reduce empty registerInstalledPlugins', () => { const result = reducer(undefined, registerInstalledPlugins([])); - expect(result).toEqual({installedPlugins: []}); + expect(result).toEqual({installedPlugins: [], removedPlugins: []}); }); const EXAMPLE_PLUGIN = { @@ -33,8 +33,9 @@ test('reduce registerInstalledPlugins, clear again', () => { const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN])); expect(result).toEqual({ installedPlugins: [EXAMPLE_PLUGIN], + removedPlugins: [], }); const result2 = reducer(result, registerInstalledPlugins([])); - expect(result2).toEqual({installedPlugins: []}); + expect(result2).toEqual({installedPlugins: [], removedPlugins: []}); }); diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index b755e0a8f..d46ceb85b 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -9,18 +9,33 @@ import {Actions} from './'; import {PluginDetails} from 'flipper-plugin-lib'; +import {produce} from 'immer'; +import {PluginDefinition} from '../plugin'; export type State = { installedPlugins: PluginDetails[]; + removedPlugins: PluginDetails[]; }; -export type Action = { - type: 'REGISTER_INSTALLED_PLUGINS'; - payload: PluginDetails[]; -}; +export type Action = + | { + type: 'REGISTER_INSTALLED_PLUGINS'; + payload: PluginDetails[]; + } + | { + type: 'PLUGIN_FILES_REMOVED'; + payload: PluginDetails; + } + | { + // Implemented by rootReducer in `store.tsx` + type: 'UNINSTALL_PLUGIN'; + payload: PluginDefinition; + }; const INITIAL_STATE: State = { installedPlugins: [], + // plugins which were uninstalled recently and require file cleanup + removedPlugins: [], }; export default function reducer( @@ -32,6 +47,13 @@ export default function reducer( ...state, installedPlugins: action.payload, }; + } else if (action.type === 'PLUGIN_FILES_REMOVED') { + const plugin = action.payload; + return produce(state, (draft) => { + draft.removedPlugins = draft.removedPlugins.filter( + (p) => p.id === plugin.id, + ); + }); } else { return state; } @@ -41,3 +63,13 @@ export const registerInstalledPlugins = (payload: PluginDetails[]): Action => ({ type: 'REGISTER_INSTALLED_PLUGINS', payload, }); + +export const pluginFilesRemoved = (payload: PluginDetails): Action => ({ + type: 'PLUGIN_FILES_REMOVED', + payload, +}); + +export const uninstallPlugin = (payload: PluginDefinition): Action => ({ + type: 'UNINSTALL_PLUGIN', + payload, +}); diff --git a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx index 532ddb00b..69927bd1c 100644 --- a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx @@ -10,7 +10,7 @@ import React, {memo, useCallback, useEffect, useRef, useState} from 'react'; import {Badge, Button, Menu, Tooltip, Typography} from 'antd'; import {InfoIcon, SidebarTitle} from '../LeftSidebar'; -import {PlusOutlined, MinusOutlined} from '@ant-design/icons'; +import {PlusOutlined, MinusOutlined, DeleteOutlined} from '@ant-design/icons'; import {Glyph, Layout, styled} from '../../ui'; import {theme, NUX, Tracked} from 'flipper-plugin'; import {useDispatch, useStore} from '../../utils/useStore'; @@ -26,6 +26,7 @@ import {useMemoize} from '../../utils/useMemoize'; import MetroDevice from '../../devices/MetroDevice'; import {DownloadablePluginDetails} from 'plugin-lib/lib'; import {startPluginDownload} from '../../reducers/pluginDownloads'; +import {uninstallPlugin} from '../../reducers/pluginManager'; const {SubMenu} = Menu; const {Text} = Typography; @@ -108,6 +109,13 @@ export const PluginList = memo(function PluginList({ }, [uninstalledPlugins, dispatch], ); + const handleUninstallPlugin = useCallback( + (id: string) => { + const plugin = disabledPlugins.find((p) => p.id === id)!; + dispatch(uninstallPlugin(plugin)); + }, + [disabledPlugins, dispatch], + ); return ( Plugins @@ -194,12 +202,29 @@ export const PluginList = memo(function PluginList({ scrollTo={plugin.id === connections.selectedPlugin} tooltip={getPluginTooltip(plugin.details)} actions={ - } - /> + <> + {!plugin.details.isDefault && ( + + } + /> + )} + + } + /> + } disabled /> diff --git a/desktop/app/src/store.tsx b/desktop/app/src/store.tsx index cf1f67e35..542d66de5 100644 --- a/desktop/app/src/store.tsx +++ b/desktop/app/src/store.tsx @@ -82,6 +82,9 @@ export function rootReducer( } else { return updateClientPlugin(state, plugin, enablePlugin); } + } else if (action.type === 'UNINSTALL_PLUGIN' && state) { + const plugin = action.payload; + return uninstallPlugin(state, plugin); } // otherwise @@ -173,6 +176,24 @@ function updateClientPlugin( }); } +function uninstallPlugin(state: StoreState, plugin: PluginDefinition) { + const clients = state.connections.clients; + return produce(state, (draft) => { + clients.forEach((client) => { + stopPlugin(client, plugin.id); + const pluginKey = getPluginKey( + client.id, + {serial: client.query.device_id}, + plugin.id, + ); + delete draft.pluginMessageQueue[pluginKey]; + }); + cleanupPluginStates(draft.pluginStates, plugin.id); + draft.plugins.clientPlugins.delete(plugin.id); + draft.pluginManager.removedPlugins.push(plugin.details); + }); +} + function updateDevicePlugin(state: StoreState, plugin: DevicePluginDefinition) { const devices = state.connections.devices; return produce(state, (draft) => { diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index 955db347d..31d76f0b1 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -140,8 +140,12 @@ export async function getInstalledPlugins(): Promise { versionDirs .filter(([_, versionDirs]) => versionDirs.length > 0) .map(([_, versionDirs]) => versionDirs[0]), - (latestVersionDir) => getPluginDetailsFromDir(latestVersionDir), - ); + (latestVersionDir) => + getPluginDetailsFromDir(latestVersionDir).catch((err) => { + console.error(`Failed to load plugin from ${latestVersionDir}`, err); + return null; + }), + ).then((plugins) => plugins.filter(notNull)); } export async function cleanupOldInstalledPluginVersions( @@ -172,11 +176,20 @@ export async function moveInstalledPluginsFromLegacyDir() { fs .lstat(dir) .then((lstat) => lstat.isDirectory()) - .catch(() => Promise.resolve(false)), + .catch(() => false), ), ) .then((dirs) => - pmap(dirs, (dir) => getPluginDetailsFromDir(dir).catch(() => null)), + pmap(dirs, (dir) => + getPluginDetailsFromDir(dir).catch(async (err) => { + console.error( + `Failed to load plugin from ${dir} on moving legacy plugins. Removing it.`, + err, + ); + fs.remove(dir); + return null; + }), + ), ) .then((plugins) => pmap(plugins.filter(notNull), (plugin) =>