mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Merge pull request #1157 from bbassie/refactor/plugin-state
Replace the PluginDetails booleans with a single state
This commit is contained in:
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