Reload single plugin on auto-update
Summary: Implemented a way for re-loading single plugin on auto-update. This make it possible to apply update without full Flipper restart. Reviewed By: mweststrate Differential Revision: D23729972 fbshipit-source-id: ed30f7cde5a0537945db0b5bb6969ae8fde42cb6
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5e979403a0
commit
0982dc06a0
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PluginDetails> = [];
|
||||
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<PluginDetails>) => (
|
||||
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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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<PluginNotification>;
|
||||
invalidatedNotifications: Array<PluginNotification>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<StoreState, Actions, any, any>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export type NpmHostedPluginsSearchArgs = {
|
||||
|
||||
export async function getNpmHostedPlugins(
|
||||
args: NpmHostedPluginsSearchArgs = {},
|
||||
): Promise<NpmPackageDescriptor[]> {
|
||||
): Promise<readonly NpmPackageDescriptor[]> {
|
||||
const index = provideSearchIndex();
|
||||
args = Object.assign(
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ async function installPluginFromTempDir(
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return pluginDetails;
|
||||
return await getPluginDetails(destinationDir);
|
||||
}
|
||||
|
||||
async function getPluginRootDir(dir: string) {
|
||||
|
||||
Reference in New Issue
Block a user