Files
NetMesh/electron/plugins/pluginManager.cjs

177 lines
5.8 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
class PluginManager {
constructor(options) {
this.database = options.database;
this.packageStore = options.packageStore;
this.runtimeSupervisor = options.runtimeSupervisor;
this.contributionService = options.contributionService ?? {
initialize: () => this.runtimeSupervisor.startEnabled(),
onPluginDisabled() {},
onPluginEnabled: (pluginId) => this.runtimeSupervisor.start(pluginId),
};
this.beforeClose = options.beforeClose ?? null;
this.initialized = false;
this.initializePromise = null;
this.mutationTail = Promise.resolve();
this.shuttingDown = false;
this.shutdownPromise = null;
}
initialize() {
this.initializePromise ??= this.#initialize();
return this.initializePromise;
}
async #initialize() {
await this.packageStore.initialize();
await this.contributionService.initialize();
this.initialized = true;
}
async #ready() {
await this.initialize();
}
#mutate(operation) {
if (this.shuttingDown) return Promise.reject(new Error("Plugin manager is shutting down"));
const result = this.mutationTail.then(operation, operation);
this.mutationTail = result.then(() => undefined, () => undefined);
return result;
}
async list() {
await this.#ready();
return this.database.listPlugins();
}
install(archivePath, options) {
return this.#mutate(async () => {
await this.#ready();
let stoppedPlugin = null;
let plugin;
try {
plugin = await this.packageStore.install(archivePath, {
enable: options?.enable === true,
beforeActivate: async ({ pluginId, previousPlugin }) => {
if (!previousPlugin?.enabled) return;
this.database.setEnabled(pluginId, false);
this.contributionService.onPluginDisabled(pluginId);
stoppedPlugin = { pluginId, version: previousPlugin.activeVersion };
await this.runtimeSupervisor.stop(pluginId);
},
});
} catch (error) {
if (stoppedPlugin) {
const current = this.database.getActivePlugin(stoppedPlugin.pluginId);
if (current?.activeVersion === stoppedPlugin.version) {
this.database.setEnabled(stoppedPlugin.pluginId, true);
try {
await this.contributionService.onPluginEnabled(stoppedPlugin.pluginId);
} catch {
this.database.setEnabled(stoppedPlugin.pluginId, false);
}
}
}
throw error;
}
if (plugin?.enabled) {
try {
await this.contributionService.onPluginEnabled(plugin.id);
} catch (error) {
this.database.setEnabled(plugin.id, false);
this.contributionService.onPluginDisabled(plugin.id);
if (stoppedPlugin && stoppedPlugin.version !== plugin.activeVersion) {
try {
this.database.setActiveVersion(stoppedPlugin.pluginId, stoppedPlugin.version, {
enabled: true,
expectedActiveVersion: plugin.activeVersion,
});
await this.contributionService.onPluginEnabled(stoppedPlugin.pluginId);
} catch {
const restored = this.database.getActivePlugin(stoppedPlugin.pluginId);
if (restored?.activeVersion === stoppedPlugin.version && restored.enabled) {
this.database.setEnabled(stoppedPlugin.pluginId, false);
}
}
}
throw error;
}
}
return plugin;
});
}
setEnabled(pluginId, enabled) {
return this.#mutate(async () => {
await this.#ready();
if (enabled) {
const plugin = this.database.getActivePlugin(pluginId);
if (plugin?.runtime?.quarantinedAt != null) {
this.database.clearQuarantine(pluginId, plugin.activeVersion);
}
this.database.setEnabled(pluginId, true);
try { await this.contributionService.onPluginEnabled(pluginId); }
catch (error) {
this.database.setEnabled(pluginId, false);
this.contributionService.onPluginDisabled(pluginId);
throw error;
}
} else {
this.database.setEnabled(pluginId, false);
this.contributionService.onPluginDisabled(pluginId);
await this.runtimeSupervisor.stop(pluginId);
}
return this.database.getActivePlugin(pluginId);
});
}
restart(pluginId) {
return this.#mutate(async () => {
await this.#ready();
await this.runtimeSupervisor.restart(pluginId);
return this.database.getActivePlugin(pluginId);
});
}
uninstall(pluginId) {
return this.#mutate(async () => {
await this.#ready();
const plugin = this.database.getActivePlugin(pluginId);
if (plugin) {
this.database.setEnabled(pluginId, false);
this.contributionService.onPluginDisabled(pluginId);
}
await this.runtimeSupervisor.stop(pluginId);
return this.packageStore.uninstall(pluginId);
});
}
shutdown() {
this.shutdownPromise ??= this.#shutdown();
return this.shutdownPromise;
}
async #shutdown() {
this.shuttingDown = true;
let supervisorError;
const supervisorShutdown = Promise.resolve()
.then(() => this.runtimeSupervisor.shutdown())
.catch((error) => { supervisorError = error; });
await this.mutationTail;
if (this.initializePromise) {
try { await this.initializePromise; } catch {}
}
await supervisorShutdown;
const errors = supervisorError ? [supervisorError] : [];
try { await this.beforeClose?.(); }
catch (error) { errors.push(error); }
try { this.database.close(); }
catch (error) { errors.push(error); }
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Plugin host shutdown failed");
}
}
module.exports = { PluginManager };