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:
committed by
Facebook GitHub Bot
parent
24aed8fd45
commit
01f02b2cab
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user