Command processing (3/n): Uninstall plugin

Summary:
*Stack summary*: this stack refactors plugin management actions to perform them in a dispatcher rather than in the root reducer (store.tsx) as all of these actions has side effects. To do that, we store requested plugin management actions (install/update/uninstall, star/unstar) in a queue which is then handled by pluginManager dispatcher. This dispatcher then dispatches all required state updates.

*Diff summary*: refactored "uninstall plugin" operation to perform it in pluginManager dispatcher

Reviewed By: mweststrate

Differential Revision: D26166198

fbshipit-source-id: d74a1d690102d9036c6d3d8612d2428f5ecef4e6
This commit is contained in:
Anton Nikolaev
2021-02-16 10:46:11 -08:00
committed by Facebook GitHub Bot
parent 24aed8fd45
commit 01f02b2cab
19 changed files with 336 additions and 170 deletions

View File

@@ -556,8 +556,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
userStarredPlugins,
},
pluginStates,
plugins: {devicePlugins, clientPlugins},
pluginManager: {installedPlugins},
plugins: {devicePlugins, clientPlugins, installedPlugins},
pluginMessageQueue,
settingsState,
}) => {

View File

@@ -55,8 +55,10 @@ Object {
"disabledPlugins": Array [],
"failedPlugins": Array [],
"gatekeepedPlugins": Array [],
"installedPlugins": Map {},
"loadedPlugins": Map {},
"marketplacePlugins": Array [],
"selectedPlugins": Array [],
"uninstalledPlugins": Set {},
}
`;

View File

@@ -29,7 +29,7 @@ import React, {useCallback, useState, useEffect} from 'react';
import {List} from 'immutable';
import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
import reloadFlipper from '../../utils/reloadFlipper';
import {registerInstalledPlugins} from '../../reducers/pluginManager';
import {registerInstalledPlugins} from '../../reducers/plugins';
import {
UpdateResult,
getInstalledPlugins,
@@ -366,7 +366,7 @@ function useNPMSearch(
PluginInstaller.defaultProps = defaultProps;
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
({pluginManager: {installedPlugins}}) => ({
({plugins: {installedPlugins}}) => ({
installedPlugins,
}),
(dispatch: Dispatch<Action<any>>) => ({

View File

@@ -24,7 +24,7 @@ const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
function getStore(installedPlugins: PluginDetails[] = []): Store {
return configureStore([])({
application: {sessionId: 'mysession'},
pluginManager: {installedPlugins},
plugins: {installedPlugins},
}) as Store;
}

View File

@@ -9,7 +9,7 @@
jest.mock('../plugins');
jest.mock('../../utils/electronModuleCache');
import {loadPlugin} from '../../reducers/pluginManager';
import {loadPlugin, uninstallPlugin} from '../../reducers/pluginManager';
import {requirePlugin} from '../plugins';
import {mocked} from 'ts-jest/utils';
import {TestUtils} from 'flipper-plugin';
@@ -19,12 +19,14 @@ import MockFlipper from '../../test-utils/MockFlipper';
const pluginDetails1 = TestUtils.createMockPluginDetails({
id: 'plugin1',
name: 'flipper-plugin1',
version: '0.0.1',
});
const pluginDefinition1 = new SandyPluginDefinition(pluginDetails1, TestPlugin);
const pluginDetails1V2 = TestUtils.createMockPluginDetails({
id: 'plugin1',
name: 'flipper-plugin1',
version: '0.0.2',
});
const pluginDefinition1V2 = new SandyPluginDefinition(
@@ -32,7 +34,10 @@ const pluginDefinition1V2 = new SandyPluginDefinition(
TestPlugin,
);
const pluginDetails2 = TestUtils.createMockPluginDetails({id: 'plugin2'});
const pluginDetails2 = TestUtils.createMockPluginDetails({
id: 'plugin2',
name: 'flipper-plugin2',
});
const pluginDefinition2 = new SandyPluginDefinition(pluginDetails2, TestPlugin);
const mockedRequirePlugin = mocked(requirePlugin);
@@ -106,3 +111,48 @@ test('load and enable Sandy plugin', async () => {
);
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeTruthy();
});
test('uninstall plugin', async () => {
mockFlipper.dispatch(
loadPlugin({plugin: pluginDetails1, enable: true, notifyIfFailed: false}),
);
mockFlipper.dispatch(uninstallPlugin({plugin: pluginDefinition1}));
expect(
mockFlipper.getState().plugins.clientPlugins.has('plugin1'),
).toBeFalsy();
expect(
mockFlipper.getState().plugins.loadedPlugins.has('plugin1'),
).toBeFalsy();
expect(
mockFlipper.getState().plugins.uninstalledPlugins.has('flipper-plugin1'),
).toBeTruthy();
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeFalsy();
});
test('uninstall bundled plugin', async () => {
const pluginDetails = TestUtils.createMockBundledPluginDetails({
id: 'bundled-plugin',
name: 'flipper-bundled-plugin',
version: '0.43.0',
});
const pluginDefinition = new SandyPluginDefinition(pluginDetails, TestPlugin);
mockedRequirePlugin.mockReturnValue(pluginDefinition);
mockFlipper.dispatch(
loadPlugin({plugin: pluginDetails, enable: true, notifyIfFailed: false}),
);
mockFlipper.dispatch(uninstallPlugin({plugin: pluginDefinition}));
expect(
mockFlipper.getState().plugins.clientPlugins.has('bundled-plugin'),
).toBeFalsy();
expect(
mockFlipper.getState().plugins.loadedPlugins.has('bundled-plugin'),
).toBeFalsy();
expect(
mockFlipper
.getState()
.plugins.uninstalledPlugins.has('flipper-bundled-plugin'),
).toBeTruthy();
expect(
mockFlipper.clients[0].sandyPluginStates.has('bundled-plugin'),
).toBeFalsy();
});

View File

@@ -27,8 +27,9 @@ import path from 'path';
import tmp from 'tmp';
import {promisify} from 'util';
import {reportPlatformFailures, reportUsage} from '../utils/metrics';
import {loadPlugin, pluginInstalled} from '../reducers/pluginManager';
import {loadPlugin} from '../reducers/pluginManager';
import {showErrorNotification} from '../utils/notifications';
import {pluginInstalled} from '../reducers/plugins';
// Adapter which forces node.js implementation for axios instead of browser implementation
// used by default in Electron. Node.js implementation is better, because it

View File

@@ -7,27 +7,33 @@
* @format
*/
import {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger';
import type {Store} from '../reducers/index';
import type {Logger} from '../fb-interfaces/Logger';
import {clearPluginState} from '../reducers/pluginStates';
import {
LoadPluginActionPayload,
pluginCommandsProcessed,
registerInstalledPlugins,
UninstallPluginActionPayload,
} from '../reducers/pluginManager';
import {
getInstalledPlugins,
cleanupOldInstalledPluginVersions,
removePlugins,
ActivatablePluginDetails,
} from 'flipper-plugin-lib';
import {sideEffect} from '../utils/sideEffect';
import {requirePlugin} from './plugins';
import {registerPluginUpdate} from '../reducers/connections';
import {showErrorNotification} from '../utils/notifications';
import type Client from '../Client';
import {unloadModule} from '../utils/electronModuleCache';
import {pluginUninstalled, registerInstalledPlugins} from '../reducers/plugins';
import {defaultEnabledBackgroundPlugins} from '../utils/pluginUtils';
const maxInstalledPluginVersionsToKeep = 2;
function refreshInstalledPlugins(store: Store) {
removePlugins(store.getState().pluginManager.uninstalledPlugins.values())
removePlugins(store.getState().plugins.uninstalledPlugins.values())
.then(() =>
cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep),
)
@@ -65,6 +71,9 @@ export default (
case 'LOAD_PLUGIN':
loadPlugin(store, command.payload);
break;
case 'UNINSTALL_PLUGIN':
uninstallPlugin(store, command.payload);
break;
default:
console.error('Unexpected plugin command', command);
break;
@@ -95,8 +104,56 @@ function loadPlugin(store: Store, payload: LoadPluginActionPayload) {
);
if (payload.notifyIfFailed) {
showErrorNotification(
`Failed to load plugin "${payload.plugin.title}" v${payload.plugin.version}`,
`Failed to activate plugin "${payload.plugin.title}" v${payload.plugin.version}`,
);
}
}
}
function uninstallPlugin(store: Store, {plugin}: UninstallPluginActionPayload) {
try {
const state = store.getState();
const clients = state.connections.clients;
clients.forEach((client) => {
stopPlugin(client, plugin.id);
});
store.dispatch(clearPluginState({pluginId: plugin.id}));
if (!plugin.details.isBundled) {
unloadPluginModule(plugin.details);
}
store.dispatch(pluginUninstalled(plugin.details));
} catch (err) {
console.error(
`Failed to uninstall plugin ${plugin.title} v${plugin.version}`,
err,
);
showErrorNotification(
`Failed to uninstall plugin "${plugin.title}" v${plugin.version}`,
);
}
}
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 unloadPluginModule(plugin: ActivatablePluginDetails) {
if (plugin.isBundled) {
// We cannot unload bundled plugin.
return;
}
unloadModule(plugin.entry);
}

View File

@@ -70,7 +70,7 @@ export default async (store: Store, logger: Logger) => {
defaultPluginsIndex = getDefaultPluginsIndex();
const uninstalledPlugins = store.getState().pluginManager.uninstalledPlugins;
const uninstalledPlugins = store.getState().plugins.uninstalledPlugins;
const bundledPlugins = getBundledPlugins();

View File

@@ -1,40 +0,0 @@
/**
* 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 {default as reducer, registerInstalledPlugins} from '../pluginManager';
import {InstalledPluginDetails} from 'flipper-plugin-lib';
test('reduce empty registerInstalledPlugins', () => {
const result = reducer(undefined, registerInstalledPlugins([]));
expect(result.installedPlugins).toEqual(new Map());
});
const EXAMPLE_PLUGIN = {
name: 'test',
version: '0.1',
description: 'my test plugin',
dir: '/plugins/test',
specVersion: 2,
source: 'src/index.ts',
isBundled: false,
isActivatable: true,
main: 'lib/index.js',
title: 'test',
id: 'test',
entry: '/plugins/test/lib/index.js',
} as InstalledPluginDetails;
test('reduce registerInstalledPlugins, clear again', () => {
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
expect(result.installedPlugins).toEqual(
new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]),
);
const result2 = reducer(result, registerInstalledPlugins([]));
expect(result2.installedPlugins).toEqual(new Map());
});

View File

@@ -11,6 +11,7 @@ import {
default as reducer,
registerPlugins,
addGatekeepedPlugins,
registerInstalledPlugins,
} from '../plugins';
import {FlipperPlugin, FlipperDevicePlugin, BaseAction} from '../../plugin';
import {InstalledPluginDetails} from 'flipper-plugin-lib';
@@ -39,6 +40,8 @@ test('add clientPlugin', () => {
disabledPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
uninstalledPlugins: new Set(),
installedPlugins: new Map(),
},
registerPlugins([testPlugin]),
);
@@ -57,6 +60,8 @@ test('add devicePlugin', () => {
disabledPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
uninstalledPlugins: new Set(),
installedPlugins: new Map(),
},
registerPlugins([testDevicePlugin]),
);
@@ -75,6 +80,8 @@ test('do not add plugin twice', () => {
disabledPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
uninstalledPlugins: new Set(),
installedPlugins: new Map(),
},
registerPlugins([testPlugin, testPlugin]),
);
@@ -109,8 +116,39 @@ test('add gatekeeped plugin', () => {
disabledPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
installedPlugins: new Map(),
uninstalledPlugins: new Set(),
},
addGatekeepedPlugins(gatekeepedPlugins),
);
expect(res.gatekeepedPlugins).toEqual(gatekeepedPlugins);
});
test('reduce empty registerInstalledPlugins', () => {
const result = reducer(undefined, registerInstalledPlugins([]));
expect(result.installedPlugins).toEqual(new Map());
});
const EXAMPLE_PLUGIN = {
name: 'test',
version: '0.1',
description: 'my test plugin',
dir: '/plugins/test',
specVersion: 2,
source: 'src/index.ts',
isBundled: false,
isActivatable: true,
main: 'lib/index.js',
title: 'test',
id: 'test',
entry: '/plugins/test/lib/index.js',
} as InstalledPluginDetails;
test('reduce registerInstalledPlugins, clear again', () => {
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
expect(result.installedPlugins).toEqual(
new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]),
);
const result2 = reducer(result, registerInstalledPlugins([]));
expect(result2.installedPlugins).toEqual(new Map());
});

View File

@@ -102,7 +102,7 @@ export type State = {
settingsState: SettingsState & PersistPartial;
launcherSettingsState: LauncherSettingsState & PersistPartial;
supportForm: SupportFormState;
pluginManager: PluginManagerState & PersistPartial;
pluginManager: PluginManagerState;
healthchecks: HealthcheckState & PersistPartial;
usageTracking: TrackingState;
pluginDownloads: PluginDownloadsState;
@@ -159,20 +159,13 @@ export default combineReducers<State, Actions>({
{
key: 'plugins',
storage,
whitelist: ['marketplacePlugins'],
whitelist: ['marketplacePlugins', 'uninstalledPlugins'],
transforms: [setTransformer({whitelist: ['uninstalledPlugins']})],
},
plugins,
),
supportForm,
pluginManager: persistReducer<PluginManagerState, Actions>(
{
key: 'pluginManager',
storage,
whitelist: ['uninstalledPlugins'],
transforms: [setTransformer({whitelist: ['uninstalledPlugins']})],
},
pluginManager,
),
pluginManager,
user: persistReducer(
{
key: 'user',

View File

@@ -7,22 +7,16 @@
* @format
*/
import {Actions} from './';
import {
ActivatablePluginDetails,
InstalledPluginDetails,
} from 'flipper-plugin-lib';
import {PluginDefinition} from '../plugin';
import type {Actions} from './';
import type {ActivatablePluginDetails} from 'flipper-plugin-lib';
import type {PluginDefinition} from '../plugin';
import {produce} from 'immer';
import semver from 'semver';
export type State = {
installedPlugins: Map<string, InstalledPluginDetails>;
uninstalledPlugins: Set<string>;
pluginCommandsQueue: PluginCommand[];
};
export type PluginCommand = LoadPluginAction;
export type PluginCommand = LoadPluginAction | UninstallPluginAction;
export type LoadPluginActionPayload = {
plugin: ActivatablePluginDetails;
@@ -35,29 +29,23 @@ export type LoadPluginAction = {
payload: LoadPluginActionPayload;
};
export type UninstallPluginActionPayload = {
plugin: PluginDefinition;
};
export type UninstallPluginAction = {
type: 'UNINSTALL_PLUGIN';
payload: UninstallPluginActionPayload;
};
export type Action =
| {
type: 'REGISTER_INSTALLED_PLUGINS';
payload: InstalledPluginDetails[];
}
| {
// Implemented by rootReducer in `store.tsx`
type: 'UNINSTALL_PLUGIN';
payload: PluginDefinition;
}
| {
type: 'PLUGIN_INSTALLED';
payload: InstalledPluginDetails;
}
| {
type: 'PLUGIN_COMMANDS_PROCESSED';
payload: number;
}
| LoadPluginAction;
| PluginCommand;
const INITIAL_STATE: State = {
installedPlugins: new Map<string, InstalledPluginDetails>(),
uninstalledPlugins: new Set<string>(),
pluginCommandsQueue: [],
};
@@ -65,55 +53,28 @@ export default function reducer(
state: State = INITIAL_STATE,
action: Actions,
): State {
if (action.type === 'REGISTER_INSTALLED_PLUGINS') {
return produce(state, (draft) => {
draft.installedPlugins = new Map(
action.payload
.filter((p) => !state.uninstalledPlugins?.has(p.name))
.map((p) => [p.name, p]),
);
});
} else if (action.type === 'PLUGIN_INSTALLED') {
const plugin = action.payload;
return produce(state, (draft) => {
const existing = draft.installedPlugins.get(plugin.name);
if (!existing || semver.gt(plugin.version, existing.version)) {
draft.installedPlugins.set(plugin.name, plugin);
}
});
} else if (action.type === 'LOAD_PLUGIN') {
return produce(state, (draft) => {
draft.pluginCommandsQueue.push({
type: 'LOAD_PLUGIN',
payload: action.payload,
switch (action.type) {
case 'LOAD_PLUGIN':
case 'UNINSTALL_PLUGIN':
return produce(state, (draft) => {
draft.pluginCommandsQueue.push(action);
});
});
} else if (action.type === 'PLUGIN_COMMANDS_PROCESSED') {
return produce(state, (draft) => {
draft.pluginCommandsQueue.splice(0, action.payload);
});
} else {
return {...state};
case 'PLUGIN_COMMANDS_PROCESSED':
return produce(state, (draft) => {
draft.pluginCommandsQueue.splice(0, action.payload);
});
default:
return state;
}
}
export const registerInstalledPlugins = (
payload: InstalledPluginDetails[],
export const uninstallPlugin = (
payload: UninstallPluginActionPayload,
): Action => ({
type: 'REGISTER_INSTALLED_PLUGINS',
payload,
});
export const uninstallPlugin = (payload: PluginDefinition): Action => ({
type: 'UNINSTALL_PLUGIN',
payload,
});
export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({
type: 'PLUGIN_INSTALLED',
payload,
});
export const loadPlugin = (payload: LoadPluginActionPayload): Action => ({
type: 'LOAD_PLUGIN',
payload,

View File

@@ -40,6 +40,10 @@ export type Action =
| {
type: 'CLEAR_CLIENT_PLUGINS_STATE';
payload: {clientId: string; devicePlugins: Set<string>};
}
| {
type: 'CLEAR_PLUGIN_STATE';
payload: {pluginId: string};
};
const INITIAL_STATE: State = {};
@@ -93,6 +97,19 @@ export default function reducer(
return newState;
}, {});
}
case 'CLEAR_PLUGIN_STATE': {
const {pluginId} = action.payload;
return produce(state, (draft) => {
Object.keys(draft).forEach((pluginKey) => {
const pluginKeyParts = deconstructPluginKey(pluginKey);
if (pluginKeyParts.pluginName === pluginId) {
delete draft[pluginKey];
}
});
});
}
default:
return state;
}

View File

@@ -7,7 +7,8 @@
* @format
*/
import type {Actions} from '.';
import {produce} from 'immer';
import {Actions} from '.';
import {deconstructPluginKey} from '../utils/clientUtils';
export type State = {
@@ -29,6 +30,10 @@ export type Action =
| {
type: 'CLEAR_CLIENT_PLUGINS_STATE';
payload: {clientId: string; devicePlugins: Set<string>};
}
| {
type: 'CLEAR_PLUGIN_STATE';
payload: {pluginId: string};
};
export default function reducer(
@@ -63,6 +68,16 @@ export default function reducer(
}
return newState;
}, {});
} else if (action.type === 'CLEAR_PLUGIN_STATE') {
const {pluginId} = action.payload;
return produce(state, (draft) => {
Object.keys(draft).forEach((pluginKey) => {
const pluginKeyParts = deconstructPluginKey(pluginKey);
if (pluginKeyParts.pluginName === pluginId) {
delete draft[pluginKey];
}
});
});
} else {
return state;
}
@@ -75,3 +90,8 @@ export const setPluginState = (payload: {
type: 'SET_PLUGIN_STATE',
payload,
});
export const clearPluginState = (payload: {pluginId: string}): Action => ({
type: 'CLEAR_PLUGIN_STATE',
payload,
});

View File

@@ -16,10 +16,12 @@ import type {
DownloadablePluginDetails,
ActivatablePluginDetails,
BundledPluginDetails,
InstalledPluginDetails,
} from 'flipper-plugin-lib';
import type {Actions} from '.';
import produce from 'immer';
import {isDevicePluginDefinition} from '../utils/pluginUtils';
import semver from 'semver';
export type State = {
devicePlugins: DevicePluginMap;
@@ -31,6 +33,8 @@ export type State = {
failedPlugins: Array<[ActivatablePluginDetails, string]>;
selectedPlugins: Array<string>;
marketplacePlugins: Array<DownloadablePluginDetails>;
uninstalledPlugins: Set<string>;
installedPlugins: Map<string, InstalledPluginDetails>;
};
export type RegisterPluginAction = {
@@ -67,20 +71,36 @@ export type Action =
| {
type: 'REGISTER_BUNDLED_PLUGINS';
payload: Array<BundledPluginDetails>;
}
| {
type: 'REGISTER_INSTALLED_PLUGINS';
payload: InstalledPluginDetails[];
}
| {
type: 'PLUGIN_INSTALLED';
payload: InstalledPluginDetails;
}
| {
type: 'PLUGIN_UNINSTALLED';
payload: ActivatablePluginDetails;
};
const INITIAL_STATE: State = {
devicePlugins: new Map(),
clientPlugins: new Map(),
loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
uninstalledPlugins: new Set(),
installedPlugins: new Map(),
};
export default function reducer(
state: State | undefined = {
devicePlugins: new Map(),
clientPlugins: new Map(),
loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
},
state: State | undefined = INITIAL_STATE,
action: Actions,
): State {
if (action.type === 'REGISTER_PLUGINS') {
@@ -133,6 +153,31 @@ export default function reducer(
...state,
bundledPlugins: new Map(action.payload.map((p) => [p.id, p])),
};
} else if (action.type === 'REGISTER_INSTALLED_PLUGINS') {
return produce(state, (draft) => {
draft.installedPlugins.clear();
action.payload.forEach((p) => {
if (!draft.uninstalledPlugins.has(p.id)) {
draft.installedPlugins.set(p.id, p);
}
});
});
} else if (action.type === 'PLUGIN_INSTALLED') {
const plugin = action.payload;
return produce(state, (draft) => {
const existing = draft.installedPlugins.get(plugin.name);
if (!existing || semver.gt(plugin.version, existing.version)) {
draft.installedPlugins.set(plugin.name, plugin);
}
});
} else if (action.type === 'PLUGIN_UNINSTALLED') {
const plugin = action.payload;
return produce(state, (draft) => {
draft.clientPlugins.delete(plugin.id);
draft.devicePlugins.delete(plugin.id);
draft.loadedPlugins.delete(plugin.id);
draft.uninstalledPlugins.add(plugin.name);
});
} else {
return state;
}
@@ -189,3 +234,22 @@ export const registerBundledPlugins = (
type: 'REGISTER_BUNDLED_PLUGINS',
payload,
});
export const registerInstalledPlugins = (
payload: InstalledPluginDetails[],
): Action => ({
type: 'REGISTER_INSTALLED_PLUGINS',
payload,
});
export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({
type: 'PLUGIN_INSTALLED',
payload,
});
export const pluginUninstalled = (
payload: ActivatablePluginDetails,
): Action => ({
type: 'PLUGIN_UNINSTALLED',
payload,
});

View File

@@ -164,7 +164,7 @@ export const PluginList = memo(function PluginList({
(id: string) => {
const plugin = disabledPlugins.find((p) => p.id === id)!;
reportUsage('plugin:uninstall', {version: plugin.version}, plugin.id);
dispatch(uninstallPlugin(plugin));
dispatch(uninstallPlugin({plugin}));
},
[disabledPlugins, dispatch],
);

View File

@@ -84,9 +84,6 @@ 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
@@ -184,27 +181,6 @@ 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);
unloadPluginModule(plugin.details);
draft.plugins.clientPlugins.delete(plugin.id);
draft.plugins.devicePlugins.delete(plugin.id);
draft.plugins.loadedPlugins.delete(plugin.id);
draft.pluginManager.uninstalledPlugins.add(plugin.details.name);
});
}
function updateDevicePlugin(state: StoreState, plugin: DevicePluginDefinition) {
const devices = state.connections.devices;
return produce(state, (draft) => {
@@ -235,7 +211,7 @@ function registerLoadedPlugin(
},
plugin: ActivatablePluginDetails,
) {
draft.pluginManager.uninstalledPlugins.delete(plugin.name);
draft.plugins.uninstalledPlugins.delete(plugin.name);
draft.plugins.loadedPlugins.set(plugin.id, plugin);
}

View File

@@ -766,6 +766,8 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
failedPlugins: [],
selectedPlugins: ['TestPlugin'],
marketplacePlugins: [],
installedPlugins: new Map(),
uninstalledPlugins: new Set(),
};
const op = determinePluginsToProcess(
[client1, client2, client3],
@@ -838,6 +840,8 @@ test('test determinePluginsToProcess for no selected plugin present in any clien
failedPlugins: [],
selectedPlugins: ['RandomPlugin'],
marketplacePlugins: [],
installedPlugins: new Map(),
uninstalledPlugins: new Set(),
};
const op = determinePluginsToProcess([client1, client2], device1, plugins);
expect(op).toBeDefined();
@@ -887,6 +891,8 @@ test('test determinePluginsToProcess for multiple clients on same device', async
failedPlugins: [],
selectedPlugins: ['TestPlugin'],
marketplacePlugins: [],
installedPlugins: new Map(),
uninstalledPlugins: new Set(),
};
const op = determinePluginsToProcess([client1, client2], device1, plugins);
expect(op).toBeDefined();
@@ -974,6 +980,8 @@ test('test determinePluginsToProcess for multiple clients on different device',
failedPlugins: [],
selectedPlugins: ['TestPlugin'],
marketplacePlugins: [],
installedPlugins: new Map(),
uninstalledPlugins: new Set(),
};
const op = determinePluginsToProcess(
[client1Device1, client2Device1, client1Device2, client2Device2],
@@ -1057,6 +1065,8 @@ test('test determinePluginsToProcess to ignore archived clients', async () => {
failedPlugins: [],
selectedPlugins: ['TestPlugin'],
marketplacePlugins: [],
installedPlugins: new Map(),
uninstalledPlugins: new Set(),
};
const op = determinePluginsToProcess(
[client, archivedClient],

View File

@@ -14,7 +14,7 @@ import {
act as testingLibAct,
} from '@testing-library/react';
import {queries} from '@testing-library/dom';
import {InstalledPluginDetails} from 'flipper-plugin-lib';
import {BundledPluginDetails, InstalledPluginDetails} from 'flipper-plugin-lib';
import {
RealFlipperClient,
@@ -414,6 +414,24 @@ export function createMockPluginDetails(
};
}
export function createMockBundledPluginDetails(
details?: Partial<BundledPluginDetails>,
): BundledPluginDetails {
return {
id: 'TestBundledPlugin',
name: 'TestBundledPlugin',
specVersion: 0,
pluginType: 'client',
isBundled: true,
isActivatable: true,
main: '',
source: '',
title: 'Testing Bundled Plugin',
version: '',
...details,
};
}
function createMockDevice(options?: StartPluginOptions): RealFlipperDevice {
const logListeners: (undefined | DeviceLogListener)[] = [];
return {