mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Replace the PluginDetails booleans with a single state
loaded, enabled, pendingInstall, removed, noEntrypoint and nameTaken allowed far more combinations than can happen, and every consumer had to know which took priority. PluginDetails now has one state, with loaded, loadsOnRestart and removed as getters over it. 2.0.3 web UIs and ctl send PluginListRequest straight to hosts and require loaded and enabled, so those are still sent next to the optional state. Plugins listed by 2.0.3 hosts get their state from loaded and enabled. The web UI planner gets the same treatment: MachinePlugin has one state in place of disabled, noEntrypoint, dependencyTaken and nameTaken. ctl host plugin list shows the state. Closes #1134 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa2883d74e
commit
814a4a201a
12 files changed
+261
-171
No files matched your search
@@ -271,7 +271,7 @@ hostPluginCommands.add(new lib.Command({
|
||||
let hostId = await lib.resolveHost(ctl, args.host);
|
||||
const plugins = await ctl.sendTo({ hostId }, new lib.PluginListRequest());
|
||||
print(asTable(plugins.map(p => ({
|
||||
title: p.title, version: p.version, loaded: p.loaded, enabled: p.enabled, npmPackage: p.npmPackage,
|
||||
title: p.title, version: p.version, state: p.state, npmPackage: p.npmPackage,
|
||||
}))));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -4,57 +4,94 @@ import type { PluginNodeEnvInfo } from "../plugin.js";
|
||||
import { Address, MessageRequest } from "./messages_core.js";
|
||||
import type { IUser } from "./UserDetails.js";
|
||||
|
||||
export const pluginStateSchema = Type.Union([
|
||||
Type.Literal("loaded"),
|
||||
Type.Literal("disabled"),
|
||||
Type.Literal("unloadOnRestart"),
|
||||
Type.Literal("loadOnRestart"),
|
||||
Type.Literal("noEntrypoint"),
|
||||
Type.Literal("pendingInstall"),
|
||||
Type.Literal("removed"),
|
||||
Type.Literal("removedLoaded"),
|
||||
Type.Literal("nameTaken"),
|
||||
]);
|
||||
/**
|
||||
* State of a plugin on a machine
|
||||
*
|
||||
* - loaded: Running and loaded again on the next restart
|
||||
* - disabled: Not loaded as `<plugin>.load_plugin` is false
|
||||
* - unloadOnRestart: Running, but disabled since startup
|
||||
* - loadOnRestart: Not running, enabled since startup or skipped by recovery mode
|
||||
* - noEntrypoint: Has no entrypoint for this machine, so it is never loaded here
|
||||
* - pendingInstall: Installed since startup, loaded on the next restart
|
||||
* - removed: Uninstalled since startup while not running
|
||||
* - removedLoaded: Uninstalled since startup, running until the next restart
|
||||
* - nameTaken: Not loaded as another package provides a plugin with the same name
|
||||
*/
|
||||
export type PluginState = Static<typeof pluginStateSchema>;
|
||||
|
||||
/* This is similar to other plugin definitions but with lots removed */
|
||||
export class PluginDetails {
|
||||
constructor(
|
||||
public name: string,
|
||||
public title: string,
|
||||
public version: string,
|
||||
public loaded: boolean,
|
||||
public enabled: boolean,
|
||||
public state: PluginState,
|
||||
public description?: string,
|
||||
public npmPackage?: string,
|
||||
/** Version installed on disk, differs from version when an update is waiting for a restart */
|
||||
public installedVersion?: string,
|
||||
/** Installed after the machine started, will be loaded on the next restart */
|
||||
public pendingInstall?: boolean,
|
||||
/** Uninstalled after the machine started, will not be loaded on the next restart */
|
||||
public removed?: boolean,
|
||||
/** Name in the root package.json dependencies, differs from npmPackage for aliases */
|
||||
public dependencyName?: string,
|
||||
/** Has no entrypoint for this machine, so it is never loaded here */
|
||||
public noEntrypoint?: boolean,
|
||||
/** Not loaded as another package provides a plugin with the same name */
|
||||
public nameTaken?: boolean,
|
||||
) {}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
name: Type.String(),
|
||||
title: Type.String(),
|
||||
version: Type.String(),
|
||||
// Missing from 2.0.3 and older, which require loaded and enabled instead
|
||||
state: Type.Optional(pluginStateSchema),
|
||||
loaded: Type.Boolean(),
|
||||
enabled: Type.Boolean(),
|
||||
description: Type.Optional(Type.String()),
|
||||
npmPackage: Type.Optional(Type.String()),
|
||||
installedVersion: Type.Optional(Type.String()),
|
||||
pendingInstall: Type.Optional(Type.Boolean()),
|
||||
removed: Type.Optional(Type.Boolean()),
|
||||
dependencyName: Type.Optional(Type.String()),
|
||||
noEntrypoint: Type.Optional(Type.Boolean()),
|
||||
nameTaken: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.name, json.title, json.version, json.loaded, json.enabled,
|
||||
json.description, json.npmPackage, json.installedVersion, json.pendingInstall, json.removed,
|
||||
json.dependencyName, json.noEntrypoint, json.nameTaken,
|
||||
json.name, json.title, json.version, json.state ?? this.loadState(json.loaded, json.enabled),
|
||||
json.description, json.npmPackage, json.installedVersion, json.dependencyName,
|
||||
);
|
||||
}
|
||||
|
||||
static fromNodeEnvInfo(pluginInfo: PluginNodeEnvInfo, loaded: boolean, enabled: boolean) {
|
||||
toJSON(): Static<typeof PluginDetails.jsonSchema> {
|
||||
return {
|
||||
name: this.name,
|
||||
title: this.title,
|
||||
version: this.version,
|
||||
state: this.state,
|
||||
loaded: this.loaded,
|
||||
enabled: this.loadsOnRestart,
|
||||
description: this.description,
|
||||
npmPackage: this.npmPackage,
|
||||
installedVersion: this.installedVersion,
|
||||
dependencyName: this.dependencyName,
|
||||
};
|
||||
}
|
||||
|
||||
/** State of a plugin known since startup */
|
||||
static loadState(loaded: boolean, enabled: boolean): PluginState {
|
||||
if (loaded) {
|
||||
return enabled ? "loaded" : "unloadOnRestart";
|
||||
}
|
||||
return enabled ? "loadOnRestart" : "disabled";
|
||||
}
|
||||
|
||||
static fromNodeEnvInfo(pluginInfo: PluginNodeEnvInfo, state: PluginState) {
|
||||
return new this(
|
||||
pluginInfo.name, pluginInfo.title, pluginInfo.version, loaded, enabled,
|
||||
pluginInfo.name, pluginInfo.title, pluginInfo.version, state,
|
||||
pluginInfo.description, pluginInfo.npmPackage,
|
||||
);
|
||||
}
|
||||
@@ -63,10 +100,25 @@ export class PluginDetails {
|
||||
static fromPackageJson(packageName: string, packageJson: any, npmPackage?: string, dependencyName?: string) {
|
||||
const description = typeof packageJson.description === "string" ? packageJson.description : undefined;
|
||||
return new this(
|
||||
packageName, packageName, packageJson.version, false, false,
|
||||
description, npmPackage, packageJson.version, true, undefined, dependencyName,
|
||||
packageName, packageName, packageJson.version, "pendingInstall",
|
||||
description, npmPackage, packageJson.version, dependencyName,
|
||||
);
|
||||
}
|
||||
|
||||
/** Running on the machine now */
|
||||
get loaded() {
|
||||
return ["loaded", "unloadOnRestart", "removedLoaded"].includes(this.state);
|
||||
}
|
||||
|
||||
/** Running on the machine after the next restart */
|
||||
get loadsOnRestart() {
|
||||
return ["loaded", "loadOnRestart", "pendingInstall"].includes(this.state);
|
||||
}
|
||||
|
||||
/** Uninstalled since startup */
|
||||
get removed() {
|
||||
return this.state === "removed" || this.state === "removedLoaded";
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginListRequest {
|
||||
|
||||
@@ -128,14 +128,12 @@ export async function listPluginDetails(
|
||||
for (const pluginInfo of pluginInfos) {
|
||||
const dir = await realDir(path.dirname(pluginInfo.packagePath));
|
||||
knownDirs.add(dir);
|
||||
const loaded = isLoaded(pluginInfo);
|
||||
// Without an entrypoint for this machine the plugin has no load_plugin field here
|
||||
const loadable = hasEntrypoint(pluginInfo);
|
||||
const plugin = PluginDetails.fromNodeEnvInfo(
|
||||
pluginInfo, isLoaded(pluginInfo), loadable && isEnabled(pluginInfo),
|
||||
pluginInfo,
|
||||
hasEntrypoint(pluginInfo) ? PluginDetails.loadState(loaded, isEnabled(pluginInfo)) : "noEntrypoint",
|
||||
);
|
||||
if (!loadable) {
|
||||
plugin.noEntrypoint = true;
|
||||
}
|
||||
if (Object.hasOwn(dependencies, pluginInfo.requirePath)) {
|
||||
plugin.dependencyName = pluginInfo.requirePath;
|
||||
}
|
||||
@@ -147,7 +145,7 @@ export async function listPluginDetails(
|
||||
packageJson = await readPackageJson(path.dirname(pluginInfo.packagePath), false);
|
||||
if (packageJson === undefined) {
|
||||
// Uninstalled since startup, it won't be found on the next restart
|
||||
plugin.removed = true;
|
||||
plugin.state = loaded ? "removedLoaded" : "removed";
|
||||
}
|
||||
} catch {
|
||||
// Ignored: npm is still writing it
|
||||
@@ -185,8 +183,7 @@ export async function listPluginDetails(
|
||||
const plugin = PluginDetails.fromPackageJson(packageName, packageJson, npmPackage, dependencyName);
|
||||
if (takenDirs.has(dir)) {
|
||||
// Skipped on startup as its plugin name is taken, will be again
|
||||
plugin.pendingInstall = undefined;
|
||||
plugin.nameTaken = true;
|
||||
plugin.state = "nameTaken";
|
||||
}
|
||||
plugins.push(plugin);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export function PluginVersions(props: { status: PluginStatus, latest?: string })
|
||||
.sort(compareVersions)
|
||||
.reverse();
|
||||
const count = (filter: (machine: MachinePlugin) => boolean) => machines.filter(filter).length;
|
||||
const missing = count(machine => (!machine.plugin || Boolean(machine.plugin.removed)) && !machine.nameTaken);
|
||||
const missing = count(machine => machine.state === "missing" || machine.state === "dependencyTaken");
|
||||
|
||||
return <>
|
||||
{versions.map(version => {
|
||||
@@ -175,14 +175,14 @@ export function PluginVersions(props: { status: PluginStatus, latest?: string })
|
||||
</div>;
|
||||
})}
|
||||
{count(isPending) > 0 && <CountLine label="Restart pending on" count={count(isPending)} />}
|
||||
{count(machine => machine.disabled) > 0 && <CountLine
|
||||
label="Disabled on" count={count(machine => machine.disabled)} type="secondary"
|
||||
{count(machine => machine.state === "disabled") > 0 && <CountLine
|
||||
label="Disabled on" count={count(machine => machine.state === "disabled")} type="secondary"
|
||||
/>}
|
||||
{count(machine => machine.noEntrypoint) > 0 && <CountLine
|
||||
label="No entrypoint on" count={count(machine => machine.noEntrypoint)} type="secondary"
|
||||
{count(machine => machine.state === "noEntrypoint") > 0 && <CountLine
|
||||
label="No entrypoint on" count={count(machine => machine.state === "noEntrypoint")} type="secondary"
|
||||
/>}
|
||||
{count(machine => machine.nameTaken) > 0 && <CountLine
|
||||
label="From another package on" count={count(machine => machine.nameTaken)} type="secondary"
|
||||
{count(machine => machine.state === "nameTaken") > 0 && <CountLine
|
||||
label="From another package on" count={count(machine => machine.state === "nameTaken")} type="secondary"
|
||||
/>}
|
||||
{missing > 0 && <CountLine label="Not installed on" count={missing} type="warning" />}
|
||||
{unknown.length > 0 && <CountLine label="Plugins not reported by" count={unknown.length} type="warning" />}
|
||||
|
||||
@@ -16,18 +16,26 @@ export type Machine = {
|
||||
};
|
||||
export type MachinePlugins = Machine & { plugins: lib.PluginDetails[] };
|
||||
|
||||
/**
|
||||
* What happens with a plugin on one machine, in order of precedence
|
||||
*
|
||||
* - nameTaken: Another package here provides the plugin by the same name, startup skips this one
|
||||
* - dependencyTaken: Not installed, and the group's npm package name is a dependency here for
|
||||
* another plugin, such as an alias
|
||||
* - missing: Not installed
|
||||
* - noEntrypoint: Installed but never loaded on this machine as it has no entrypoint for it
|
||||
* - disabled: Installed but not loaded after the next restart
|
||||
* - enabled: Installed and loaded after the next restart
|
||||
*/
|
||||
export type MachinePluginState =
|
||||
"nameTaken" | "dependencyTaken" | "missing" | "noEntrypoint" | "disabled" | "enabled";
|
||||
|
||||
/** A plugin on one machine, with the version it runs now and the version it will run after a restart */
|
||||
export type MachinePlugin = Machine & {
|
||||
plugin?: lib.PluginDetails,
|
||||
state: MachinePluginState,
|
||||
running?: string,
|
||||
afterRestart?: string,
|
||||
disabled: boolean,
|
||||
/** Installed but never loaded on this machine as it has no entrypoint for it */
|
||||
noEntrypoint: boolean,
|
||||
/** The group's npm package name is a dependency here for another plugin, such as an alias */
|
||||
dependencyTaken: boolean,
|
||||
/** Another package here provides the plugin by the same name, startup skips this one */
|
||||
nameTaken: boolean,
|
||||
/** A host from before installedVersion was reported, it can't install a specific version */
|
||||
oldHost: boolean,
|
||||
};
|
||||
@@ -53,45 +61,51 @@ export function compareVersions(a: string, b: string) {
|
||||
return strcmp(a, b);
|
||||
}
|
||||
|
||||
/** Not uninstalled since startup and not skipped by name, so its plugin name is in use */
|
||||
function holdsName(plugin: lib.PluginDetails) {
|
||||
return !plugin.removed && plugin.state !== "pendingInstall" && plugin.state !== "nameTaken";
|
||||
}
|
||||
|
||||
function machinePluginState(
|
||||
plugins: lib.PluginDetails[], plugin: lib.PluginDetails | undefined, key: string, names: Set<string>,
|
||||
): MachinePluginState {
|
||||
if (plugin?.state === "nameTaken") {
|
||||
return "nameTaken";
|
||||
}
|
||||
const absent = !plugin || plugin.removed;
|
||||
if ((absent || plugin.state === "pendingInstall") && plugins.some(
|
||||
candidate => pluginStatusKey(candidate) !== key && names.has(candidate.name) && holdsName(candidate)
|
||||
)) {
|
||||
return "nameTaken";
|
||||
}
|
||||
if (absent) {
|
||||
return plugins.some(candidate => candidate.dependencyName === key && !candidate.removed)
|
||||
? "dependencyTaken" : "missing";
|
||||
}
|
||||
if (plugin.state === "noEntrypoint") {
|
||||
return "noEntrypoint";
|
||||
}
|
||||
return plugin.loadsOnRestart ? "enabled" : "disabled";
|
||||
}
|
||||
|
||||
/** The plugin with the given key on one machine, names are the plugin names known for that key */
|
||||
function machinePlugin(machine: MachinePlugins, key: string, names: Set<string>): MachinePlugin {
|
||||
const { plugins, ...rest } = machine;
|
||||
// A package removed and installed again is listed twice, the removed one may still be running
|
||||
const candidates = plugins.filter(candidate => pluginStatusKey(candidate) === key);
|
||||
const plugin = candidates.find(candidate => !candidate.removed) ?? candidates[0];
|
||||
const absent = !plugin || Boolean(plugin.removed);
|
||||
const dependencyTaken = absent && plugins.some(
|
||||
candidate => candidate.dependencyName === key && !candidate.removed
|
||||
);
|
||||
const nameTaken = Boolean(plugin?.nameTaken) || (absent || Boolean(plugin.pendingInstall)) && plugins.some(
|
||||
candidate => (
|
||||
pluginStatusKey(candidate) !== key && names.has(candidate.name)
|
||||
&& !candidate.removed && !candidate.pendingInstall && !candidate.nameTaken
|
||||
)
|
||||
);
|
||||
const noEntrypoint = Boolean(plugin?.noEntrypoint);
|
||||
const state = machinePluginState(plugins, plugin, key, names);
|
||||
const oldHost = !machine.isController && plugins.length > 0 && plugins.every(
|
||||
candidate => candidate.installedVersion === undefined && !candidate.removed
|
||||
);
|
||||
const entry: MachinePlugin = {
|
||||
...rest, plugin, disabled: false, noEntrypoint, dependencyTaken, nameTaken, oldHost,
|
||||
return {
|
||||
...rest,
|
||||
plugin,
|
||||
state,
|
||||
oldHost,
|
||||
running: candidates.find(candidate => candidate.loaded)?.version,
|
||||
afterRestart: state === "enabled" && plugin ? plugin.installedVersion ?? plugin.version : undefined,
|
||||
};
|
||||
// What runs now follows loaded, what runs after a restart follows enabled and what is on disk
|
||||
entry.running = candidates.find(candidate => candidate.loaded)?.version;
|
||||
if (nameTaken) {
|
||||
return entry;
|
||||
}
|
||||
if (plugin?.pendingInstall) {
|
||||
// Plugins load by default, so a new install runs after the next restart
|
||||
entry.afterRestart = plugin.installedVersion ?? plugin.version;
|
||||
} else if (plugin && !plugin.removed && !noEntrypoint) {
|
||||
if (plugin.enabled) {
|
||||
entry.afterRestart = plugin.installedVersion ?? plugin.version;
|
||||
} else {
|
||||
entry.disabled = true;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Group the plugin lists of every machine by plugin */
|
||||
@@ -107,7 +121,7 @@ export function computePluginStatuses(
|
||||
if (!titles.has(key) || plugin.loaded) {
|
||||
titles.set(key, plugin.title);
|
||||
}
|
||||
if (!plugin.pendingInstall && !plugin.nameTaken) {
|
||||
if (plugin.state !== "pendingInstall" && plugin.state !== "nameTaken") {
|
||||
pluginNames.set(key, (pluginNames.get(key) ?? new Set()).add(plugin.name));
|
||||
}
|
||||
}
|
||||
@@ -154,15 +168,15 @@ export function planUpdate(
|
||||
if (installed && compareVersions(plugin.installedVersion ?? plugin.version, version) >= 0) {
|
||||
continue;
|
||||
}
|
||||
const kind = !installed || plugin.pendingInstall ? "install" : "update";
|
||||
const kind = !installed || plugin.state === "pendingInstall" ? "install" : "update";
|
||||
const skip = (reason: string, skipKind: PackageOperation["kind"] = kind) => {
|
||||
plan.skipped.push({ kind: skipKind, target, reason });
|
||||
};
|
||||
if (machine.offline) {
|
||||
skip("offline");
|
||||
} else if (machine.nameTaken) {
|
||||
} else if (machine.state === "nameTaken") {
|
||||
skip("another package provides this plugin", "install");
|
||||
} else if (machine.dependencyTaken) {
|
||||
} else if (machine.state === "dependencyTaken") {
|
||||
skip(`another package is installed as ${status.npmPackage}`, "install");
|
||||
} else if (installed && !plugin.npmPackage) {
|
||||
// An alias, private or local package, installing the group's npm package would replace it
|
||||
@@ -211,18 +225,20 @@ export function machineStatusLabels(machine: MachinePlugin) {
|
||||
if (machine.offline) {
|
||||
labels.push("Offline");
|
||||
}
|
||||
if (machine.nameTaken) {
|
||||
if (machine.state === "nameTaken") {
|
||||
labels.push("Another package provides this plugin");
|
||||
} else if (!machine.plugin || machine.plugin.removed) {
|
||||
labels.push(machine.dependencyTaken ? "Another package is installed by this name" : "Not installed");
|
||||
} else if (machine.state === "dependencyTaken") {
|
||||
labels.push("Another package is installed by this name");
|
||||
} else if (machine.state === "missing") {
|
||||
labels.push("Not installed");
|
||||
} else {
|
||||
if (!machine.plugin.npmPackage) {
|
||||
if (!machine.plugin?.npmPackage) {
|
||||
labels.push("Not installed from npm");
|
||||
}
|
||||
if (machine.noEntrypoint) {
|
||||
if (machine.state === "noEntrypoint") {
|
||||
labels.push("No entrypoint for this machine");
|
||||
}
|
||||
if (machine.disabled) {
|
||||
if (machine.state === "disabled") {
|
||||
labels.push("Disabled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,8 +302,7 @@ describe("host/src/Host", function() {
|
||||
await host.loadPlugins();
|
||||
const plugins = await host.handlePluginListRequest(new lib.PluginListRequest());
|
||||
const plugin = plugins.find(candidate => candidate.name === "controller_only");
|
||||
assert.equal(plugin.loaded, false);
|
||||
assert.equal(plugin.noEntrypoint, true);
|
||||
assert.equal(plugin.state, "noEntrypoint");
|
||||
const [entry] = computePluginStatuses([
|
||||
{ name: "host", isController: false, address: { hostId: 1 }, plugins: [plugin] },
|
||||
]).get("controller_only").machines;
|
||||
|
||||
@@ -443,14 +443,12 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
it("should load the plugin on the controller", async function() {
|
||||
const details = await pluginDetails("controller");
|
||||
assert(details.enabled, "test_plugin is not enabled on the controller");
|
||||
assert(details.loaded, "test_plugin is not loaded on the controller");
|
||||
assert.equal(details.state, "loaded", "test_plugin is not loaded on the controller");
|
||||
});
|
||||
|
||||
it("should load the plugin on the host", async function() {
|
||||
const details = await pluginDetails({ hostId: 4 });
|
||||
assert(details.enabled, "test_plugin is not enabled on the host");
|
||||
assert(details.loaded, "test_plugin is not loaded on the host");
|
||||
assert.equal(details.state, "loaded", "test_plugin is not loaded on the host");
|
||||
});
|
||||
|
||||
it("should report the plugin's title and version", async function() {
|
||||
|
||||
@@ -41,7 +41,7 @@ describe("lib/data/SystemInfo", function() {
|
||||
testRoundTripJsonSerialisable(SystemInfo, [
|
||||
base,
|
||||
[...base, []],
|
||||
[...base, [new PluginDetails("name", "Title", "1.0.0", true, true, undefined, "name", "1.1.0")]],
|
||||
[...base, [new PluginDetails("name", "Title", "1.0.0", "loaded", undefined, "name", "1.1.0")]],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+14
-12
@@ -414,12 +414,12 @@ describe("lib/plugin_loader", function() {
|
||||
const loaded = byPackage.get("plugin-npm");
|
||||
assert.equal(loaded.version, "0.0.0");
|
||||
assert.equal(loaded.installedVersion, "0.0.1");
|
||||
assert.equal(loaded.pendingInstall, undefined);
|
||||
assert.equal(loaded.state, "loaded");
|
||||
|
||||
// Dependencies of plugin packages are found the same way as on startup
|
||||
for (const packageName of ["plugin-aliased", "plugin-deep", "plugin-transitive"]) {
|
||||
const pending = byPackage.get(packageName);
|
||||
assert.equal(pending.pendingInstall, true, packageName);
|
||||
assert.equal(pending.state, "pendingInstall", packageName);
|
||||
assert.equal(pending.installedVersion, "0.0.1", packageName);
|
||||
assert.equal(pending.loaded, false, packageName);
|
||||
}
|
||||
@@ -513,13 +513,12 @@ describe("lib/plugin_loader", function() {
|
||||
};
|
||||
let plugins = await lib.listPluginDetails([local], () => true, () => true);
|
||||
const taken = plugins.find(plugin => plugin.name === "plugin-npm");
|
||||
assert.equal(taken.nameTaken, true);
|
||||
assert.equal(taken.pendingInstall, undefined);
|
||||
assert.equal(taken.state, "nameTaken");
|
||||
|
||||
// Loaded on the next restart once the plugin with that name is gone
|
||||
const gone = { ...local, packagePath: path.resolve("plugins", "gone", "package.json") };
|
||||
plugins = await lib.listPluginDetails([gone], () => true, () => true);
|
||||
assert.equal(plugins.find(plugin => plugin.name === "plugin-npm").pendingInstall, true);
|
||||
assert.equal(plugins.find(plugin => plugin.name === "plugin-npm").state, "pendingInstall");
|
||||
});
|
||||
|
||||
it("should list a dependency made direct since startup as pending", async function() {
|
||||
@@ -531,7 +530,7 @@ describe("lib/plugin_loader", function() {
|
||||
await fs.writeFile("package.json", JSON.stringify(root));
|
||||
const plugins = await lib.listPluginDetails([], () => true, () => true);
|
||||
const hidden = plugins.find(plugin => plugin.name === "plugin-hidden");
|
||||
assert.equal(hidden.pendingInstall, true);
|
||||
assert.equal(hidden.state, "pendingInstall");
|
||||
assert.equal(hidden.npmPackage, "plugin-hidden");
|
||||
} finally {
|
||||
await fs.writeFile("package.json", rootPackageJson);
|
||||
@@ -573,7 +572,7 @@ describe("lib/plugin_loader", function() {
|
||||
assert.equal(npm.npmPackage, undefined);
|
||||
assert.equal(npm.dependencyName, "plugin-npm");
|
||||
const deep = plugins.find(plugin => plugin.name === "plugin-deep");
|
||||
assert.equal(deep.pendingInstall, true);
|
||||
assert.equal(deep.state, "pendingInstall");
|
||||
assert.equal(deep.npmPackage, undefined);
|
||||
} finally {
|
||||
await fs.writeFile("package.json", rootPackageJson);
|
||||
@@ -637,7 +636,7 @@ describe("lib/plugin_loader", function() {
|
||||
npmPackage: "plugin-npm",
|
||||
}], () => true, () => true);
|
||||
assert.equal(plugin.npmPackage, undefined);
|
||||
assert.equal(plugin.removed, undefined);
|
||||
assert.equal(plugin.state, "loaded");
|
||||
} finally {
|
||||
await fs.writeFile(packageJsonPath, original);
|
||||
}
|
||||
@@ -650,7 +649,7 @@ describe("lib/plugin_loader", function() {
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify({ ...JSON.parse(original), private: true }));
|
||||
const plugins = await lib.listPluginDetails([], () => true, () => true);
|
||||
const deep = plugins.find(plugin => plugin.name === "plugin-deep");
|
||||
assert.equal(deep.pendingInstall, true);
|
||||
assert.equal(deep.state, "pendingInstall");
|
||||
assert.equal(deep.npmPackage, undefined);
|
||||
} finally {
|
||||
await fs.writeFile(packageJsonPath, original);
|
||||
@@ -705,7 +704,7 @@ describe("lib/plugin_loader", function() {
|
||||
assert.equal(alias.npmPackage, undefined);
|
||||
|
||||
const published = new lib.PluginDetails(
|
||||
"published", "Published", "1.0.0", true, true, undefined, "plugin-aliased", "1.0.0",
|
||||
"published", "Published", "1.0.0", "loaded", undefined, "plugin-aliased", "1.0.0",
|
||||
);
|
||||
const statuses = computePluginStatuses([
|
||||
{ name: "A", isController: false, address: { hostId: 1 }, plugins: [published] },
|
||||
@@ -740,9 +739,12 @@ describe("lib/plugin_loader", function() {
|
||||
}];
|
||||
const plugins = await lib.listPluginDetails(pluginInfos, () => true, () => true);
|
||||
const byPackage = new Map(plugins.map(plugin => [plugin.npmPackage ?? plugin.name, plugin]));
|
||||
assert.equal(byPackage.get("plugin-gone").removed, true);
|
||||
assert.equal(byPackage.get("plugin-gone").state, "removedLoaded");
|
||||
assert.equal(byPackage.get("plugin-gone").installedVersion, undefined);
|
||||
assert.equal(byPackage.get("plugin-npm").removed, undefined);
|
||||
assert.equal(byPackage.get("plugin-npm").state, "loaded");
|
||||
|
||||
const [disabled] = await lib.listPluginDetails([pluginInfos[0]], () => false, () => false);
|
||||
assert.equal(disabled.state, "removed");
|
||||
});
|
||||
|
||||
it("should load existing plugin list", async function() {
|
||||
|
||||
@@ -38,20 +38,46 @@ describe("messages/plugin", function() {
|
||||
describe("PluginDetails", function() {
|
||||
it("has round trip json serialisation", function() {
|
||||
testRoundTripJsonSerialisable(lib.PluginDetails, [
|
||||
["Name", "Title", "Version", true, true],
|
||||
[
|
||||
"Name", "Title", "Version", false, false, "Description", "Npm Package",
|
||||
"Installed Version", true, true, "Dependency Name", true, true,
|
||||
],
|
||||
["Name", "Title", "Version", "loaded"],
|
||||
["Name", "Title", "Version", "removedLoaded", "Description", "Npm Package", "Installed Version", "Dep"],
|
||||
]);
|
||||
});
|
||||
it("sends loaded and enabled for 2.0.3 and older", function() {
|
||||
const cases = [
|
||||
["loaded", true, true],
|
||||
["disabled", false, false],
|
||||
["unloadOnRestart", true, false],
|
||||
["loadOnRestart", false, true],
|
||||
["noEntrypoint", false, false],
|
||||
["pendingInstall", false, true],
|
||||
["removed", false, false],
|
||||
["removedLoaded", true, false],
|
||||
["nameTaken", false, false],
|
||||
];
|
||||
for (const [state, loaded, enabled] of cases) {
|
||||
const json = JSON.parse(JSON.stringify(new lib.PluginDetails("Name", "Title", "Version", state)));
|
||||
assert.deepEqual([json.loaded, json.enabled], [loaded, enabled], state);
|
||||
}
|
||||
});
|
||||
it("reads the state of plugins listed by 2.0.3 and older", function() {
|
||||
const cases = [
|
||||
[true, true, "loaded"],
|
||||
[true, false, "unloadOnRestart"],
|
||||
[false, true, "loadOnRestart"],
|
||||
[false, false, "disabled"],
|
||||
];
|
||||
for (const [loaded, enabled, state] of cases) {
|
||||
const json = { name: "Name", title: "Title", version: "1.0.0", loaded, enabled };
|
||||
assert.equal(lib.PluginDetails.fromJSON(json).state, state);
|
||||
}
|
||||
});
|
||||
it("can be created from NodeEnvInfo interface", function() {
|
||||
const pluginDetails = lib.PluginDetails.fromNodeEnvInfo({
|
||||
name: "Name", title: "Title", version: "Version",
|
||||
description: "Description", npmPackage: "Npm Package",
|
||||
}, true, true);
|
||||
}, "loaded");
|
||||
assert.deepEqual(pluginDetails, new lib.PluginDetails(
|
||||
"Name", "Title", "Version", true, true,
|
||||
"Name", "Title", "Version", "loaded",
|
||||
"Description", "Npm Package",
|
||||
));
|
||||
});
|
||||
@@ -60,8 +86,8 @@ describe("messages/plugin", function() {
|
||||
"package", { version: "1.0.0", description: "Description" }, "package", "package",
|
||||
);
|
||||
assert.deepEqual(pluginDetails, new lib.PluginDetails(
|
||||
"package", "package", "1.0.0", false, false,
|
||||
"Description", "package", "1.0.0", true, undefined, "package",
|
||||
"package", "package", "1.0.0", "pendingInstall",
|
||||
"Description", "package", "1.0.0", "package",
|
||||
));
|
||||
const invalid = lib.PluginDetails.fromPackageJson("package", { version: "1.0.0", description: 1 });
|
||||
assert.equal(invalid.description, undefined);
|
||||
@@ -99,7 +125,7 @@ describe("messages/plugin", function() {
|
||||
|
||||
const response = await controlConnection.handlePluginListRequest(new lib.PluginListRequest());
|
||||
assert.deepEqual(response, [new lib.PluginDetails(
|
||||
"Name", "Title", "Version", true, true,
|
||||
"Name", "Title", "Version", "loaded",
|
||||
"Description", "Npm Package", "Version",
|
||||
)]);
|
||||
});
|
||||
@@ -114,7 +140,7 @@ describe("messages/plugin", function() {
|
||||
|
||||
const response = await host.handlePluginListRequest(new lib.PluginListRequest());
|
||||
assert.deepEqual(response, [new lib.PluginDetails(
|
||||
"Name", "Title", "Version", true, true,
|
||||
"Name", "Title", "Version", "loaded",
|
||||
"Description", "Npm Package", "Version",
|
||||
)]);
|
||||
});
|
||||
|
||||
@@ -13,11 +13,11 @@ describe("web_ui/util/plugin_status", function() {
|
||||
return { name, isController: false, address: { hostId: name.length }, plugins };
|
||||
}
|
||||
function loaded(npmPackage, version) {
|
||||
return new lib.PluginDetails("plugin", "Plugin", version, true, true, undefined, npmPackage, version);
|
||||
return new lib.PluginDetails("plugin", "Plugin", version, "loaded", undefined, npmPackage, version);
|
||||
}
|
||||
function pending(packageName, npmPackage, version) {
|
||||
return new lib.PluginDetails(
|
||||
packageName, packageName, version, false, false, undefined, npmPackage, version, true,
|
||||
packageName, packageName, version, "pendingInstall", undefined, npmPackage, version,
|
||||
);
|
||||
}
|
||||
function skippedText(result) {
|
||||
@@ -50,7 +50,7 @@ describe("web_ui/util/plugin_status", function() {
|
||||
it("should not update a loaded package that isn't from npm", function() {
|
||||
const result = plan([
|
||||
machine("a", [loaded("plugin-aliased", "1.0.0")]),
|
||||
machine("b", [new lib.PluginDetails("plugin-aliased", "Plugin", "1.0.0", true, true)]),
|
||||
machine("b", [new lib.PluginDetails("plugin-aliased", "Plugin", "1.0.0", "loaded")]),
|
||||
]);
|
||||
assert.deepEqual(result.operations.map(op => op.target.name), ["a"]);
|
||||
assert.deepEqual(skippedText(result), [{ kind: "update", text: "b (not installed from npm)" }]);
|
||||
@@ -58,7 +58,7 @@ describe("web_ui/util/plugin_status", function() {
|
||||
|
||||
it("should not send a specific version to hosts that can't parse it", function() {
|
||||
// Hosts from before installedVersion was reported take name@version as a package name
|
||||
const old = new lib.PluginDetails("plugin", "Plugin", "1.0.0", true, true, undefined, "plugin-aliased");
|
||||
const old = new lib.PluginDetails("plugin", "Plugin", "1.0.0", "loaded", undefined, "plugin-aliased");
|
||||
const result = plan([
|
||||
machine("a", [loaded("plugin-aliased", "1.0.0")]),
|
||||
machine("old", [old]),
|
||||
@@ -70,7 +70,7 @@ describe("web_ui/util/plugin_status", function() {
|
||||
});
|
||||
|
||||
it("should not install over a loaded alias that has the package name", function() {
|
||||
const alias = new lib.PluginDetails("real_plugin", "Real", "1.0.0", true, true);
|
||||
const alias = new lib.PluginDetails("real_plugin", "Real", "1.0.0", "loaded");
|
||||
alias.dependencyName = "plugin-aliased";
|
||||
const statuses = computePluginStatuses([
|
||||
machine("a", [loaded("plugin-aliased", "1.0.0")]),
|
||||
@@ -110,7 +110,7 @@ describe("web_ui/util/plugin_status", function() {
|
||||
describe("planMachineUpdate()", function() {
|
||||
it("should plan for the given machine only", function() {
|
||||
const loaded = new lib.PluginDetails(
|
||||
"plugin", "Plugin", "1.0.0", true, true, undefined, "plugin-aliased", "1.0.0",
|
||||
"plugin", "Plugin", "1.0.0", "loaded", undefined, "plugin-aliased", "1.0.0",
|
||||
);
|
||||
const statuses = computePluginStatuses([
|
||||
{ name: "a", isController: false, address: { hostId: 1 }, plugins: [loaded] },
|
||||
@@ -136,7 +136,7 @@ describe("web_ui/util/plugin_status", function() {
|
||||
}
|
||||
function loadedPlugin(version = "1.0.0", installedVersion = version) {
|
||||
return new lib.PluginDetails(
|
||||
"plugin", "Plugin", version, true, true, undefined, "plugin", installedVersion,
|
||||
"plugin", "Plugin", version, "loaded", undefined, "plugin", installedVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,24 +146,22 @@ describe("web_ui/util/plugin_status", function() {
|
||||
assert.deepEqual(labels([]), ["Not installed"]);
|
||||
assert.deepEqual(labels([loadedPlugin()], { offline: true }), ["Offline"]);
|
||||
const disabled = new lib.PluginDetails(
|
||||
"plugin", "Plugin", "1.0.0", false, false, undefined, "plugin", "1.0.0",
|
||||
"plugin", "Plugin", "1.0.0", "disabled", undefined, "plugin", "1.0.0",
|
||||
);
|
||||
assert.deepEqual(labels([disabled]), ["Disabled"]);
|
||||
const local = new lib.PluginDetails("plugin", "Plugin", "1.0.0", true, true);
|
||||
const local = new lib.PluginDetails("plugin", "Plugin", "1.0.0", "loaded");
|
||||
assert.deepEqual(labels([local]), ["Not installed from npm"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computePluginStatuses()", function() {
|
||||
it("should not expect a plugin without an entrypoint for the machine to run after restart", function() {
|
||||
const plugin = new lib.PluginDetails("controller_only", "Plugin", "1.0.0", false, false);
|
||||
plugin.noEntrypoint = true;
|
||||
const plugin = new lib.PluginDetails("controller_only", "Plugin", "1.0.0", "noEntrypoint");
|
||||
const [status] = computePluginStatuses([
|
||||
{ name: "host", isController: false, address: { hostId: 1 }, plugins: [plugin] },
|
||||
]).values();
|
||||
const [entry] = status.machines;
|
||||
assert.equal(entry.noEntrypoint, true);
|
||||
assert.equal(entry.disabled, false);
|
||||
assert.equal(entry.state, "noEntrypoint");
|
||||
assert.equal(entry.running, undefined);
|
||||
assert.equal(entry.afterRestart, undefined);
|
||||
});
|
||||
|
||||
@@ -9,62 +9,64 @@ const target = "2.0.0";
|
||||
const versions = ["1.0.0", "2.0.0", "3.0.0"];
|
||||
|
||||
function details(fields) {
|
||||
const plugin = new lib.PluginDetails(
|
||||
fields.name, fields.name, fields.version, fields.loaded, fields.enabled, undefined, fields.npmPackage,
|
||||
fields.installedVersion, fields.pendingInstall, fields.removed, fields.dependencyName, fields.noEntrypoint,
|
||||
fields.nameTaken,
|
||||
return new lib.PluginDetails(
|
||||
fields.name, fields.name, fields.version, fields.state, undefined, fields.npmPackage,
|
||||
fields.installedVersion, fields.dependencyName,
|
||||
);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
function* states() {
|
||||
yield { label: "absent", plugins: [] };
|
||||
for (const version of versions) {
|
||||
for (const installed of [version, ...versions.filter(other => other !== version)]) {
|
||||
for (const enabled of [true, false]) {
|
||||
for (const loaded of enabled ? [true] : [true, false]) {
|
||||
const state = `${enabled ? "enabled" : "disabled"}${loaded ? " loaded" : ""}`;
|
||||
yield {
|
||||
label: `npm ${version} on disk ${installed} ${state}`,
|
||||
entry: { source: "npm", version, installed, enabled, loaded },
|
||||
plugins: [details({
|
||||
name: "plugin", version, loaded, enabled, npmPackage: "pkg",
|
||||
installedVersion: installed, dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
}
|
||||
for (const [state, loaded, enabled] of [
|
||||
["loaded", true, true],
|
||||
["unloadOnRestart", true, false],
|
||||
["loadOnRestart", false, true],
|
||||
["disabled", false, false],
|
||||
]) {
|
||||
yield {
|
||||
label: `npm ${version} on disk ${installed} ${state}`,
|
||||
entry: { source: "npm", version, installed, enabled, loaded },
|
||||
plugins: [details({
|
||||
name: "plugin", version, state, npmPackage: "pkg",
|
||||
installedVersion: installed, dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
}
|
||||
}
|
||||
yield {
|
||||
label: `npm ${version} removed`,
|
||||
entry: { source: "npm", version, removed: true },
|
||||
plugins: [details({
|
||||
name: "plugin", version, loaded: true, enabled: true, npmPackage: "pkg",
|
||||
removed: true, dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
for (const loaded of [true, false]) {
|
||||
yield {
|
||||
label: `npm ${version} removed${loaded ? " while loaded" : ""}`,
|
||||
entry: { source: "npm", version, removed: true, loaded },
|
||||
plugins: [details({
|
||||
name: "plugin", version, state: loaded ? "removedLoaded" : "removed", npmPackage: "pkg",
|
||||
dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
}
|
||||
yield {
|
||||
label: `npm ${version} no entrypoint`,
|
||||
entry: { source: "npm", version, installed: version, noEntrypoint: true },
|
||||
plugins: [details({
|
||||
name: "plugin", version, loaded: false, enabled: false, npmPackage: "pkg",
|
||||
installedVersion: version, dependencyName: "pkg", noEntrypoint: true,
|
||||
name: "plugin", version, state: "noEntrypoint", npmPackage: "pkg",
|
||||
installedVersion: version, dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
yield {
|
||||
label: `npm ${version} pending`,
|
||||
entry: { source: "npm", version, installed: version, pending: true },
|
||||
plugins: [details({
|
||||
name: "pkg", version, loaded: false, enabled: false, npmPackage: "pkg",
|
||||
installedVersion: version, pendingInstall: true, dependencyName: "pkg",
|
||||
name: "pkg", version, state: "pendingInstall", npmPackage: "pkg",
|
||||
installedVersion: version, dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
yield {
|
||||
label: `alias ${version} pending`,
|
||||
entry: { source: "alias", version, installed: version, pending: true },
|
||||
plugins: [details({
|
||||
name: "pkg", version, loaded: false, enabled: false,
|
||||
installedVersion: version, pendingInstall: true, dependencyName: "pkg",
|
||||
name: "pkg", version, state: "pendingInstall",
|
||||
installedVersion: version, dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
yield {
|
||||
@@ -72,12 +74,12 @@ function* states() {
|
||||
entry: { source: "npm", version, installed: target, replaced: true },
|
||||
plugins: [
|
||||
details({
|
||||
name: "plugin", version, loaded: true, enabled: true, npmPackage: "pkg",
|
||||
removed: true, dependencyName: "pkg",
|
||||
name: "plugin", version, state: "removedLoaded", npmPackage: "pkg",
|
||||
dependencyName: "pkg",
|
||||
}),
|
||||
details({
|
||||
name: "pkg", version: target, loaded: false, enabled: false, npmPackage: "pkg",
|
||||
installedVersion: target, pendingInstall: true, dependencyName: "pkg",
|
||||
name: "pkg", version: target, state: "pendingInstall", npmPackage: "pkg",
|
||||
installedVersion: target, dependencyName: "pkg",
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -86,7 +88,7 @@ function* states() {
|
||||
label: "alias loaded as pkg",
|
||||
taken: "dependency",
|
||||
plugins: [details({
|
||||
name: "real_plugin", version: "1.0.0", loaded: true, enabled: true,
|
||||
name: "real_plugin", version: "1.0.0", state: "loaded",
|
||||
installedVersion: "1.0.0", dependencyName: "pkg",
|
||||
})],
|
||||
};
|
||||
@@ -95,10 +97,10 @@ function* states() {
|
||||
label: `npm ${version} skipped as its name is taken`,
|
||||
taken: "name",
|
||||
plugins: [
|
||||
details({ name: "plugin", version: "1.0.0", loaded: true, enabled: true, installedVersion: "1.0.0" }),
|
||||
details({ name: "plugin", version: "1.0.0", state: "loaded", installedVersion: "1.0.0" }),
|
||||
details({
|
||||
name: "pkg", version, loaded: false, enabled: false, npmPackage: "pkg",
|
||||
installedVersion: version, dependencyName: "pkg", nameTaken: true,
|
||||
name: "pkg", version, state: "nameTaken", npmPackage: "pkg",
|
||||
installedVersion: version, dependencyName: "pkg",
|
||||
}),
|
||||
],
|
||||
entry: { source: "npm", version, installed: version },
|
||||
@@ -108,17 +110,17 @@ function* states() {
|
||||
label: "local plugin with the same name",
|
||||
taken: "name",
|
||||
plugins: [details({
|
||||
name: "plugin", version: "1.0.0", loaded: true, enabled: true, installedVersion: "1.0.0",
|
||||
name: "plugin", version: "1.0.0", state: "loaded", installedVersion: "1.0.0",
|
||||
})],
|
||||
};
|
||||
yield {
|
||||
label: "local plugin with the same name and pkg pending",
|
||||
taken: "name",
|
||||
plugins: [
|
||||
details({ name: "plugin", version: "1.0.0", loaded: true, enabled: true, installedVersion: "1.0.0" }),
|
||||
details({ name: "plugin", version: "1.0.0", state: "loaded", installedVersion: "1.0.0" }),
|
||||
details({
|
||||
name: "pkg", version: "1.0.0", loaded: false, enabled: false, npmPackage: "pkg",
|
||||
installedVersion: "1.0.0", pendingInstall: true, dependencyName: "pkg",
|
||||
name: "pkg", version: "1.0.0", state: "pendingInstall", npmPackage: "pkg",
|
||||
installedVersion: "1.0.0", dependencyName: "pkg",
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -145,7 +147,7 @@ function expected(state) {
|
||||
}
|
||||
let running;
|
||||
let afterRestart;
|
||||
if (entry?.loaded || entry?.removed || entry?.replaced) {
|
||||
if (entry?.loaded || entry?.replaced) {
|
||||
running = entry.version;
|
||||
}
|
||||
if (entry && !entry.removed && !entry.noEntrypoint && (entry.pending || entry.replaced || entry.enabled)) {
|
||||
@@ -159,7 +161,7 @@ function semverGte(a, b) {
|
||||
}
|
||||
|
||||
const reference = { name: "ref", isController: true, address: "controller", plugins: [details({
|
||||
name: "plugin", version: "1.0.0", loaded: true, enabled: true, npmPackage: "pkg",
|
||||
name: "plugin", version: "1.0.0", state: "loaded", npmPackage: "pkg",
|
||||
installedVersion: "1.0.0", dependencyName: "pkg",
|
||||
})] };
|
||||
|
||||
|
||||
Reference in new issue
Block a user