Install plugins to pending directory first to enable installing new versions of existing plugins
Summary: Install plugins to pending directory first to enable installing new versions of existing plugins. On startup Flipper moves all the plugins from pending directory into installed plugins directory. Auto-update, after downloading a plugin package, will also extract it to "pending", so after restart update will automatically be applied. Reviewed By: mweststrate Differential Revision: D21929713 fbshipit-source-id: 141b106415e941156ae598cf810ab3bed8c76ced
This commit is contained in:
committed by
Facebook GitHub Bot
parent
eff378defa
commit
eeded4e32f
@@ -33,7 +33,7 @@ import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
|
|||||||
import restartFlipper from '../../utils/restartFlipper';
|
import restartFlipper from '../../utils/restartFlipper';
|
||||||
import {registerInstalledPlugins} from '../../reducers/pluginManager';
|
import {registerInstalledPlugins} from '../../reducers/pluginManager';
|
||||||
import {
|
import {
|
||||||
readInstalledPlugins,
|
getPendingAndInstalledPlugins,
|
||||||
removePlugin,
|
removePlugin,
|
||||||
PluginMap,
|
PluginMap,
|
||||||
PluginDetails,
|
PluginDetails,
|
||||||
@@ -445,7 +445,7 @@ export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
|||||||
}),
|
}),
|
||||||
(dispatch: Dispatch<Action<any>>) => ({
|
(dispatch: Dispatch<Action<any>>) => ({
|
||||||
refreshInstalledPlugins: () => {
|
refreshInstalledPlugins: () => {
|
||||||
readInstalledPlugins().then((plugins) =>
|
getPendingAndInstalledPlugins().then((plugins) =>
|
||||||
dispatch(registerInstalledPlugins(plugins)),
|
dispatch(registerInstalledPlugins(plugins)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
import {Store} from '../reducers/index';
|
import {Store} from '../reducers/index';
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
||||||
import {readInstalledPlugins} from 'flipper-plugin-lib';
|
import {getPendingAndInstalledPlugins} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
function refreshInstalledPlugins(store: Store) {
|
function refreshInstalledPlugins(store: Store) {
|
||||||
readInstalledPlugins().then((plugins) =>
|
getPendingAndInstalledPlugins().then((plugins) =>
|
||||||
store.dispatch(registerInstalledPlugins(plugins)),
|
store.dispatch(registerInstalledPlugins(plugins)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"decompress-unzip": "^4.0.1",
|
"decompress-unzip": "^4.0.1",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"live-plugin-manager": "^0.14.1",
|
"live-plugin-manager": "^0.14.1",
|
||||||
|
"semver": "^7.3.2",
|
||||||
"tmp": "^0.2.1"
|
"tmp": "^0.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import decompressUnzip from 'decompress-unzip';
|
|||||||
import tmp from 'tmp';
|
import tmp from 'tmp';
|
||||||
import PluginDetails from './PluginDetails';
|
import PluginDetails from './PluginDetails';
|
||||||
import getPluginDetails from './getPluginDetails';
|
import getPluginDetails from './getPluginDetails';
|
||||||
import {pluginInstallationDir} from './pluginPaths';
|
import {
|
||||||
|
pluginInstallationDir,
|
||||||
|
pluginPendingInstallationDir,
|
||||||
|
} from './pluginPaths';
|
||||||
|
import semver from 'semver';
|
||||||
|
|
||||||
export type PluginMap = Map<string, PluginDetails>;
|
export type PluginMap = Map<string, PluginDetails>;
|
||||||
|
|
||||||
@@ -33,40 +37,67 @@ function providePluginManagerNoDependencies(): PM {
|
|||||||
return new PM({ignoredDependencies: [/.*/]});
|
return new PM({ignoredDependencies: [/.*/]});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installPluginFromTempDir(pluginDir: string) {
|
function getPluginPendingInstallationDir(
|
||||||
const packageJSONPath = path.join(pluginDir, 'package.json');
|
name: string,
|
||||||
const packageJSON = JSON.parse(
|
version: string,
|
||||||
(await fs.readFile(packageJSONPath)).toString(),
|
): string {
|
||||||
);
|
return path.join(getPluginPendingInstallationsDir(name), version);
|
||||||
const name = packageJSON.name;
|
}
|
||||||
|
|
||||||
await fs.ensureDir(pluginInstallationDir);
|
function getPluginPendingInstallationsDir(name: string): string {
|
||||||
// create empty watchman config (required by metro's file watcher)
|
return path.join(pluginPendingInstallationDir, name);
|
||||||
await fs.writeFile(path.join(pluginInstallationDir, '.watchmanconfig'), '{}');
|
}
|
||||||
const destinationDir = path.join(pluginInstallationDir, name);
|
|
||||||
// Clean up existing destination files.
|
|
||||||
await fs.remove(destinationDir);
|
|
||||||
await fs.ensureDir(destinationDir);
|
|
||||||
|
|
||||||
const isPreBundled = await fs.pathExists(path.join(pluginDir, 'dist'));
|
function getPluginInstallationDir(name: string): string {
|
||||||
if (!isPreBundled) {
|
return path.join(pluginInstallationDir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPluginFromTempDir(sourceDir: string) {
|
||||||
|
const pluginDetails = await getPluginDetails(sourceDir);
|
||||||
|
const {name, version} = pluginDetails;
|
||||||
|
const backupDir = path.join(await getTmpDir(), `${name}-${version}`);
|
||||||
|
const installationsDir = getPluginPendingInstallationsDir(name);
|
||||||
|
const destinationDir = getPluginPendingInstallationDir(name, version);
|
||||||
|
|
||||||
|
if (pluginDetails.specVersion == 1) {
|
||||||
|
// For first version of spec we need to install dependencies
|
||||||
const pluginManager = providePluginManager();
|
const pluginManager = providePluginManager();
|
||||||
// install the plugin dependencies into node_modules
|
// install the plugin dependencies into node_modules
|
||||||
const nodeModulesDir = path.join(destinationDir, 'node_modules');
|
const nodeModulesDir = path.join(
|
||||||
|
await getTmpDir(),
|
||||||
|
`${name}-${version}-modules`,
|
||||||
|
);
|
||||||
pluginManager.options.pluginsPath = nodeModulesDir;
|
pluginManager.options.pluginsPath = nodeModulesDir;
|
||||||
await pluginManager.installFromPath(pluginDir);
|
await pluginManager.installFromPath(sourceDir);
|
||||||
// live-plugin-manager also installs plugin itself into the target dir, it's better remove it
|
// live-plugin-manager also installs plugin itself into the target dir, it's better remove it
|
||||||
await fs.remove(path.join(nodeModulesDir, name));
|
await fs.remove(path.join(nodeModulesDir, name));
|
||||||
|
await fs.move(nodeModulesDir, path.join(sourceDir, 'node_modules'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Moving the existing destination dir to backup
|
||||||
|
if (await fs.pathExists(destinationDir)) {
|
||||||
|
await fs.move(destinationDir, backupDir, {overwrite: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.move(sourceDir, destinationDir);
|
||||||
|
|
||||||
|
// Cleaning up all the previously downloaded packages, because we've got the newest one.
|
||||||
|
const otherPackages = await fs.readdir(installationsDir);
|
||||||
|
for (const otherPackage of otherPackages) {
|
||||||
|
const otherPackageDir = path.join(installationsDir, otherPackage);
|
||||||
|
if (otherPackageDir !== destinationDir) {
|
||||||
|
await fs.remove(otherPackageDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Restore previous version from backup if installation failed
|
||||||
|
await fs.remove(destinationDir);
|
||||||
|
if (await fs.pathExists(backupDir)) {
|
||||||
|
await fs.move(backupDir, destinationDir, {overwrite: true});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
// copying plugin files into the destination folder
|
|
||||||
const pluginFiles = await fs.readdir(pluginDir);
|
|
||||||
await Promise.all(
|
|
||||||
pluginFiles
|
|
||||||
.filter((f) => f !== 'node_modules')
|
|
||||||
.map((f) =>
|
|
||||||
fs.move(path.join(pluginDir, f), path.join(destinationDir, f)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPluginRootDir(dir: string) {
|
async function getPluginRootDir(dir: string) {
|
||||||
@@ -87,6 +118,23 @@ async function getPluginRootDir(dir: string) {
|
|||||||
return isNpmPackage ? packageDir : extensionDir;
|
return isNpmPackage ? packageDir : extensionDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getInstalledPlugin(
|
||||||
|
name: string,
|
||||||
|
): Promise<PluginDetails | null> {
|
||||||
|
const dir = getPluginInstallationDir(name);
|
||||||
|
if (!(await fs.pathExists(dir))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await getPluginDetails(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isPluginPendingInstallation(
|
||||||
|
name: string,
|
||||||
|
version: string,
|
||||||
|
) {
|
||||||
|
return await fs.pathExists(getPluginPendingInstallationDir(name, version));
|
||||||
|
}
|
||||||
|
|
||||||
export async function installPluginFromNpm(name: string) {
|
export async function installPluginFromNpm(name: string) {
|
||||||
const tmpDir = await getTmpDir();
|
const tmpDir = await getTmpDir();
|
||||||
try {
|
try {
|
||||||
@@ -97,9 +145,7 @@ export async function installPluginFromNpm(name: string) {
|
|||||||
const pluginTempDir = path.join(tmpDir, name);
|
const pluginTempDir = path.join(tmpDir, name);
|
||||||
await installPluginFromTempDir(pluginTempDir);
|
await installPluginFromTempDir(pluginTempDir);
|
||||||
} finally {
|
} finally {
|
||||||
if (await fs.pathExists(tmpDir)) {
|
await fs.remove(tmpDir);
|
||||||
await fs.remove(tmpDir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +161,11 @@ export async function installPluginFromFile(packagePath: string) {
|
|||||||
const pluginDir = await getPluginRootDir(tmpDir);
|
const pluginDir = await getPluginRootDir(tmpDir);
|
||||||
await installPluginFromTempDir(pluginDir);
|
await installPluginFromTempDir(pluginDir);
|
||||||
} finally {
|
} finally {
|
||||||
if (await fs.pathExists(tmpDir)) {
|
await fs.remove(tmpDir);
|
||||||
await fs.remove(tmpDir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readInstalledPlugins(): Promise<PluginMap> {
|
export async function getInstalledPlugins(): Promise<PluginMap> {
|
||||||
const pluginDirExists = await fs.pathExists(pluginInstallationDir);
|
const pluginDirExists = await fs.pathExists(pluginInstallationDir);
|
||||||
if (!pluginDirExists) {
|
if (!pluginDirExists) {
|
||||||
return new Map();
|
return new Map();
|
||||||
@@ -146,6 +190,109 @@ export async function readInstalledPlugins(): Promise<PluginMap> {
|
|||||||
return new Map(plugins.filter(Boolean));
|
return new Map(plugins.filter(Boolean));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPendingInstallationPlugins(): Promise<PluginMap> {
|
||||||
|
const pluginDirExists = await fs.pathExists(pluginPendingInstallationDir);
|
||||||
|
if (!pluginDirExists) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
const dirs = await fs.readdir(pluginPendingInstallationDir);
|
||||||
|
const plugins = await Promise.all<[string, PluginDetails]>(
|
||||||
|
dirs.map(
|
||||||
|
(name) =>
|
||||||
|
new Promise(async (resolve, reject) => {
|
||||||
|
const versions = (
|
||||||
|
await fs.readdir(path.join(pluginPendingInstallationDir, name))
|
||||||
|
).sort((v1, v2) => semver.compare(v2, v1, true));
|
||||||
|
if (versions.length === 0) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
const pluginDir = path.join(
|
||||||
|
pluginPendingInstallationDir,
|
||||||
|
name,
|
||||||
|
versions[0],
|
||||||
|
);
|
||||||
|
if (!(await fs.lstat(pluginDir)).isDirectory()) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve([name, await getPluginDetails(pluginDir)]);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return new Map(plugins.filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPendingAndInstalledPlugins(): Promise<PluginMap> {
|
||||||
|
const plugins = await getInstalledPlugins();
|
||||||
|
for (const [name, details] of await getPendingInstallationPlugins()) {
|
||||||
|
if (
|
||||||
|
!plugins.get(name) ||
|
||||||
|
semver.gt(details.version, plugins.get(name)!.version)
|
||||||
|
) {
|
||||||
|
plugins.set(name, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
export async function removePlugin(name: string): Promise<void> {
|
export async function removePlugin(name: string): Promise<void> {
|
||||||
await fs.remove(path.join(pluginInstallationDir, name));
|
await fs.remove(path.join(pluginInstallationDir, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function finishPendingPluginInstallations() {
|
||||||
|
if (!(await fs.pathExists(pluginPendingInstallationDir))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.ensureDir(pluginInstallationDir);
|
||||||
|
// create empty watchman config (required by metro's file watcher)
|
||||||
|
const watchmanConfigPath = path.join(
|
||||||
|
pluginInstallationDir,
|
||||||
|
'.watchmanconfig',
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(watchmanConfigPath)) {
|
||||||
|
await fs.writeFile(watchmanConfigPath, '{}');
|
||||||
|
}
|
||||||
|
const pendingPlugins = await fs.readdir(pluginPendingInstallationDir);
|
||||||
|
for (const pendingPlugin of pendingPlugins) {
|
||||||
|
const pendingInstallationsDir = getPluginPendingInstallationsDir(
|
||||||
|
pendingPlugin,
|
||||||
|
);
|
||||||
|
const pendingVersions = (
|
||||||
|
await fs.readdir(pendingInstallationsDir)
|
||||||
|
).sort((v1, v2) => semver.compare(v2, v1, true)); // sort versions in descending order
|
||||||
|
if (pendingVersions.length === 0) {
|
||||||
|
await fs.remove(pendingInstallationsDir);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const version = pendingVersions[0];
|
||||||
|
const pendingInstallation = path.join(pendingInstallationsDir, version);
|
||||||
|
const installationDir = getPluginInstallationDir(pendingPlugin);
|
||||||
|
const backupDir = path.join(await getTmpDir(), pendingPlugin);
|
||||||
|
try {
|
||||||
|
if (await fs.pathExists(installationDir)) {
|
||||||
|
await fs.move(installationDir, backupDir, {overwrite: true});
|
||||||
|
}
|
||||||
|
await fs.move(pendingInstallation, installationDir, {overwrite: true});
|
||||||
|
await fs.remove(pendingInstallationsDir);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`Error while finishing pending installation for ${pendingPlugin}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
// in case of error, keep the previously installed version
|
||||||
|
await fs.remove(installationDir);
|
||||||
|
if (await fs.pathExists(backupDir)) {
|
||||||
|
await fs.move(backupDir, installationDir, {overwrite: true});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fs.remove(backupDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error while finishing plugin pending installations', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,4 +14,9 @@ export const flipperDataDir = path.join(homedir(), '.flipper');
|
|||||||
|
|
||||||
export const pluginInstallationDir = path.join(flipperDataDir, 'thirdparty');
|
export const pluginInstallationDir = path.join(flipperDataDir, 'thirdparty');
|
||||||
|
|
||||||
|
export const pluginPendingInstallationDir = path.join(
|
||||||
|
flipperDataDir,
|
||||||
|
'pending',
|
||||||
|
);
|
||||||
|
|
||||||
export const pluginCacheDir = path.join(flipperDataDir, 'plugins');
|
export const pluginCacheDir = path.join(flipperDataDir, 'plugins');
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import setup from './setup';
|
|||||||
import isFB from './fb-stubs/isFB';
|
import isFB from './fb-stubs/isFB';
|
||||||
import delegateToLauncher from './launcher';
|
import delegateToLauncher from './launcher';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
import {finishPendingPluginInstallations} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
const VERSION: string = (global as any).__VERSION__;
|
const VERSION: string = (global as any).__VERSION__;
|
||||||
|
|
||||||
@@ -110,17 +111,21 @@ setInterval(() => {
|
|||||||
}
|
}
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
compilePlugins(() => {
|
finishPendingPluginInstallations()
|
||||||
if (win) {
|
.then(() =>
|
||||||
win.reload();
|
compilePlugins(() => {
|
||||||
}
|
if (win) {
|
||||||
}, path.join(flipperDir, 'plugins')).then((dynamicPlugins) => {
|
win.reload();
|
||||||
ipcMain.on('get-dynamic-plugins', (event) => {
|
}
|
||||||
event.returnValue = dynamicPlugins;
|
}, path.join(flipperDir, 'plugins')),
|
||||||
|
)
|
||||||
|
.then((dynamicPlugins) => {
|
||||||
|
ipcMain.on('get-dynamic-plugins', (event) => {
|
||||||
|
event.returnValue = dynamicPlugins;
|
||||||
|
});
|
||||||
|
pluginsCompiled = true;
|
||||||
|
tryCreateWindow();
|
||||||
});
|
});
|
||||||
pluginsCompiled = true;
|
|
||||||
tryCreateWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// check if we already have an instance of this app open
|
// check if we already have an instance of this app open
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|||||||
Reference in New Issue
Block a user