Make internal plugin distribution code publicly available (#3473)
Summary: Pull Request resolved: https://github.com/facebook/flipper/pull/3473 This diff is the first one which addresses https://github.com/facebook/flipper/issues/3320. In this diff we are making a part of the code used for internal Flipper plugin distribution in Meta also available publicly for re-using in other orgs. Some explanation on how plugin installation and updates is designed now: 1) We periodically poll for plugins available for download. API for retrieving available plugins list is abstracted and will be different between public and fb versions, however all other logic is re-used. 2) In addition to "Enabled" and "Disabled" plugins in the left panel Flipper shows "Detected in App" list. Plugins in this list are those which are known compatible with the currently selected device/app, but not yet installed. 3) User can install any of "Detected in App" plugins by clicking to "Download and install" button near them in the left panel similarly to enabling plugins in "Disabled" list. 4) If we detect that for some installed plugin we have a newer version available for download - we download it silently and store on disk. 5) If the plugin for which we have new downloaded version is disabled - we update it silently without any notifications by loading new version from the disk and unloading the previous version from cache. 6) If the plugin for which we have new downloaded version is enabled then we avoid updating it automatically (because we need to reset plugin state in such case) and instead show notification on top of the plugin and ask user to reload it to apply new version. On reloading we reset the plugin state. 7) On Flipper startup we always update all plugins to their latest versions available on the disk. Reviewed By: aigoncharov Differential Revision: D34380308 fbshipit-source-id: a94d724e42aa5ef78445af266fcd4c424226a703
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5da0a83a36
commit
38c81ca159
@@ -23,7 +23,7 @@ import {
|
|||||||
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
|
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
|
||||||
import {Typography} from 'antd';
|
import {Typography} from 'antd';
|
||||||
import {getPluginStatus, PluginStatus} from '../utils/pluginUtils';
|
import {getPluginStatus, PluginStatus} from '../utils/pluginUtils';
|
||||||
import {loadPluginsFromMarketplace} from './fb-stubs/pluginMarketplace';
|
import {loadPluginsFromMarketplace} from './pluginMarketplace';
|
||||||
import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
|
import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
|
||||||
import {startPluginDownload} from '../reducers/pluginDownloads';
|
import {startPluginDownload} from '../reducers/pluginDownloads';
|
||||||
import isProduction from '../utils/isProduction';
|
import isProduction from '../utils/isProduction';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import plugins from './plugins';
|
|||||||
import user from './fb-stubs/user';
|
import user from './fb-stubs/user';
|
||||||
import pluginManager from './pluginManager';
|
import pluginManager from './pluginManager';
|
||||||
import reactNative from './reactNative';
|
import reactNative from './reactNative';
|
||||||
import pluginMarketplace from './fb-stubs/pluginMarketplace';
|
import pluginMarketplace from './pluginMarketplace';
|
||||||
import pluginDownloads from './pluginDownloads';
|
import pluginDownloads from './pluginDownloads';
|
||||||
import info from '../utils/info';
|
import info from '../utils/info';
|
||||||
import pluginChangeListener from './pluginsChangeListener';
|
import pluginChangeListener from './pluginsChangeListener';
|
||||||
|
|||||||
209
desktop/flipper-ui-core/src/dispatcher/pluginMarketplace.tsx
Normal file
209
desktop/flipper-ui-core/src/dispatcher/pluginMarketplace.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and 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 {Store} from '../reducers/index';
|
||||||
|
import {loadAvailablePlugins} from '../fb-stubs/pluginMarketplaceAPI';
|
||||||
|
import {
|
||||||
|
MarketplacePluginDetails,
|
||||||
|
registerMarketplacePlugins,
|
||||||
|
} from '../reducers/plugins';
|
||||||
|
import {getFlipperLib} from 'flipper-plugin';
|
||||||
|
import {DownloadablePluginDetails} from 'flipper-common';
|
||||||
|
import semver from 'semver';
|
||||||
|
import {startPluginDownload} from '../reducers/pluginDownloads';
|
||||||
|
import {sideEffect} from '../utils/sideEffect';
|
||||||
|
import {switchPlugin} from '../reducers/pluginManager';
|
||||||
|
import {setPluginEnabled} from '../reducers/connections';
|
||||||
|
import isPluginCompatible from '../utils/isPluginCompatible';
|
||||||
|
import {selectCompatibleMarketplaceVersions} from './plugins';
|
||||||
|
import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent';
|
||||||
|
import {isConnectivityOrAuthError} from 'flipper-common';
|
||||||
|
import {isLoggedIn} from '../fb-stubs/user';
|
||||||
|
import {getRenderHostInstance} from '..';
|
||||||
|
|
||||||
|
export const pollingIntervalMs = getRenderHostInstance().serverConfig.env
|
||||||
|
.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL
|
||||||
|
? parseInt(
|
||||||
|
getRenderHostInstance().serverConfig.env
|
||||||
|
.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL!,
|
||||||
|
10,
|
||||||
|
) // for manual testing we could set smaller interval
|
||||||
|
: 300000; // 5 min by default
|
||||||
|
|
||||||
|
function isAutoUpdateDisabled() {
|
||||||
|
return (
|
||||||
|
!getFlipperLib().isFB ||
|
||||||
|
getRenderHostInstance().GK('flipper_disable_plugin_auto_update') ||
|
||||||
|
getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_AUTO_UPDATE !==
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluginMarketplaceDisabled() {
|
||||||
|
return (
|
||||||
|
!getFlipperLib().isFB ||
|
||||||
|
getRenderHostInstance().GK('flipper_disable_plugin_marketplace') ||
|
||||||
|
getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_MARKETPLACE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (store: Store) => {
|
||||||
|
if (isPluginMarketplaceDisabled()) {
|
||||||
|
console.warn(
|
||||||
|
'Loading plugins from Plugin Marketplace disabled by GK or env var',
|
||||||
|
);
|
||||||
|
store.dispatch(registerMarketplacePlugins([]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Run the first refresh immediately and then every time when user is logged
|
||||||
|
sideEffect(
|
||||||
|
store,
|
||||||
|
{
|
||||||
|
name: 'refreshMarketplacePluginsWhenUserLogged',
|
||||||
|
throttleMs: 1000,
|
||||||
|
fireImmediately: true,
|
||||||
|
},
|
||||||
|
(state) => state.user,
|
||||||
|
(_, store) => refreshMarketplacePlugins(store),
|
||||||
|
);
|
||||||
|
// Additionally schedule refreshes with the given interval
|
||||||
|
const handle = setInterval(
|
||||||
|
refreshMarketplacePlugins,
|
||||||
|
pollingIntervalMs,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
// Try to auto-install plugins on every connected / disconnected app
|
||||||
|
sideEffect(
|
||||||
|
store,
|
||||||
|
{
|
||||||
|
name: 'autoInstallPluginsOnClientConnected',
|
||||||
|
throttleMs: 1000,
|
||||||
|
fireImmediately: false,
|
||||||
|
},
|
||||||
|
(state) => ({
|
||||||
|
clients: state.connections.clients,
|
||||||
|
pluginsInitialized: state.plugins.initialized,
|
||||||
|
}),
|
||||||
|
(_, store) =>
|
||||||
|
autoUpdatePlugins(store, store.getState().plugins.marketplacePlugins),
|
||||||
|
);
|
||||||
|
return async () => {
|
||||||
|
clearInterval(handle);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadPluginsFromMarketplace(): Promise<
|
||||||
|
MarketplacePluginDetails[]
|
||||||
|
> {
|
||||||
|
const availablePlugins = await loadAvailablePlugins();
|
||||||
|
return selectCompatibleMarketplaceVersions(availablePlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshMarketplacePlugins(store: Store): Promise<void> {
|
||||||
|
if (getFlipperLib().isFB && !isLoggedIn().get()) {
|
||||||
|
// inside FB we cannot refresh when user is not logged
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const plugins = await loadPluginsFromMarketplace();
|
||||||
|
store.dispatch(registerMarketplacePlugins(plugins));
|
||||||
|
autoUpdatePlugins(store, plugins);
|
||||||
|
} catch (err) {
|
||||||
|
if (isConnectivityOrAuthError(err)) {
|
||||||
|
// This is handled elsewhere and we don't need to create another warning or error for it.
|
||||||
|
console.warn(
|
||||||
|
'Connectivity or auth error while refreshing plugins from Marketplace',
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Error while refreshing plugins from Marketplace', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function autoUpdatePlugins(
|
||||||
|
store: Store,
|
||||||
|
marketplacePlugins: DownloadablePluginDetails[],
|
||||||
|
) {
|
||||||
|
const state = store.getState();
|
||||||
|
if (!state.plugins.initialized) {
|
||||||
|
// skip auto-updating plugins if they are not initialized yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {loadedPlugins, installedPlugins, uninstalledPluginNames} =
|
||||||
|
state.plugins;
|
||||||
|
const autoInstalledPlugins = new Set<string>();
|
||||||
|
for (const client of state.connections.clients.values()) {
|
||||||
|
const enabledPlugins = state.connections.enabledPlugins[client.query.app];
|
||||||
|
if (enabledPlugins) {
|
||||||
|
// If we already have persisted list of enabled plugins -
|
||||||
|
// we should install those of them which are enabled, but not installed by some reason.
|
||||||
|
enabledPlugins.forEach((p) => {
|
||||||
|
if (client.supportsPlugin(p)) {
|
||||||
|
autoInstalledPlugins.add(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If there is no persisted list of enabled plugins this means that user
|
||||||
|
// opened this app for first time. In such case we should enable and install
|
||||||
|
// plugins which are enabled by default.
|
||||||
|
for (const plugin of marketplacePlugins) {
|
||||||
|
if (plugin.isEnabledByDefault && client.supportsPlugin(plugin.id)) {
|
||||||
|
autoInstalledPlugins.add(plugin.id);
|
||||||
|
const loadedPluginInstance = state.plugins.clientPlugins.get(
|
||||||
|
plugin.id,
|
||||||
|
);
|
||||||
|
if (loadedPluginInstance) {
|
||||||
|
// If plugin was already installed before (e.g. for debugging another mobile app),
|
||||||
|
// then we should switch its state to "loaded" for the current app.
|
||||||
|
store.dispatch(
|
||||||
|
switchPlugin({
|
||||||
|
plugin: loadedPluginInstance,
|
||||||
|
selectedApp: client.query.app,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If plugin was not installed before, then we should mark it as enabled
|
||||||
|
// to ensure it is automatically loaded after downloaded from Marketplace.
|
||||||
|
store.dispatch(setPluginEnabled(plugin.id, client.query.app));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isAutoUpdateDisabled() || !isLoggedIn().get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const plugin of marketplacePlugins) {
|
||||||
|
if (uninstalledPluginNames.has(plugin.name)) {
|
||||||
|
// Skip if plugin is marked as uninstalled
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isPluginCompatible(plugin)) {
|
||||||
|
// Skip if new plugin version is not compatible with the current Flipper version
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const loadedPlugin = loadedPlugins.get(plugin.id);
|
||||||
|
if (loadedPlugin && !isPluginVersionMoreRecent(plugin, loadedPlugin)) {
|
||||||
|
// Skip if plugin is installed and new version is less or equal to the installed one
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!loadedPlugin && !autoInstalledPlugins.has(plugin.id)) {
|
||||||
|
// Skip if plugin is not installed and not in the list of auto-installable plugins
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const installedVersion = installedPlugins.get(plugin.name)?.version;
|
||||||
|
if (installedVersion && semver.gte(installedVersion, plugin.version)) {
|
||||||
|
// Skip if the same or newer version already downloaded
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Finally if we are good to go - dispatch downloading of the updated version
|
||||||
|
store.dispatch(startPluginDownload({plugin, startedByUser: false}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,10 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function loadPluginsFromMarketplace() {
|
import {MarketplacePluginDetails} from '../reducers/plugins';
|
||||||
// Marketplace is not implemented in public version of Flipper
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
export async function loadAvailablePlugins(): Promise<
|
||||||
// Marketplace is not implemented in public version of Flipper
|
MarketplacePluginDetails[]
|
||||||
};
|
> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user