Throw error if lib is imported twice (#863)

The lib library was written with the assumption that there's only one
copy of it in the whole application and everything imports that one
copy.  Unfortunately there are quite a few circumstances were package
managers end up installing multiple copies of lib and then different
parts use different versions of the library.

This causes Clusterio to break with strange and hard to track down
errors.  Most notably permissions and link messages for a plugin may get
registered to one copy of lib while the controller uses a different copy
and thus does not see or register that what the plugin added exists.

Detect this condition when importing @clusterio/lib and throw an error
when it has been imported twice in order to prevent the broken
installation from causing future hard to debug issues.
This commit is contained in:
Hornwitser authored and GitHub committed 2026-03-06 18:50:18 +01:00
1 parent 45f0b94971
commit 276c36b6e9
12 files changed
+63 -3

No files matched your search

+1 -1
View File
@@ -490,7 +490,7 @@ I version of clusterio. Expect things to break. I
`
);
startup().catch(err => {
if (err instanceof lib.StartupError) {
if (err instanceof lib.StartupError || err.code === "InstallationError") {
logger.fatal(`
+-------------------------------+
| Unable to to start controller |
+3
View File
@@ -1068,6 +1068,9 @@ export default class Controller {
this.plugins.set(pluginInfo.name, controllerPlugin);
} catch (err: any) {
if (err.code === "InstallationError") {
throw err;
}
throw new lib.PluginError(pluginInfo.name, err);
}
+1 -1
View File
@@ -299,7 +299,7 @@ I version of clusterio. Expect things to break. I
if (err instanceof lib.WebSocketError) {
// Already handled by connector.on("error") within Host
} else if (err instanceof lib.StartupError) {
} else if (err instanceof lib.StartupError || err.code === "InstallationError") {
logger.fatal(`
+----------------------------------+
| Unable to to start clusteriohost |
+3
View File
@@ -440,6 +440,9 @@ export default class Host extends lib.Link {
this.plugins.set(pluginInfo.name, hostPlugin);
} catch (err: any) {
if (err.code === "InstallationError") {
throw err;
}
throw new lib.PluginError(pluginInfo.name, err);
}
+3
View File
@@ -16,3 +16,6 @@ export * from "./src/subscriptions";
export { default as ExponentialBackoff } from "./src/ExponentialBackoff";
export { default as RateLimiter } from "./src/RateLimiter";
export { default as ValueCache } from "./src/ValueCache";
import { checkSingletonImport } from "./src/check_singleton_import";
checkSingletonImport(import.meta.url);
+3
View File
@@ -38,3 +38,6 @@ export { default as ExponentialBackoff } from "./src/ExponentialBackoff";
export { default as ModStore } from "./src/ModStore";
export { default as RateLimiter } from "./src/RateLimiter";
export { default as ValueCache } from "./src/ValueCache";
import { checkSingletonImport } from "./src/check_singleton_import";
checkSingletonImport(__filename);
@@ -0,0 +1,21 @@
import { InstallationError } from "./errors";
declare global {
// eslint-disable-next-line vars-on-top
var _clusterioLibImportedFrom: string;
}
export function checkSingletonImport(location: string) {
if (!global._clusterioLibImportedFrom) {
global._clusterioLibImportedFrom = location;
} else {
throw new InstallationError(`Attempt to import duplicate copy of @clusterio/lib
Importing more than one copy of @clusterio/lib into the same runtime
will break Clusterio. This typically happens when the package
manager has installed two different versions of @clusterio/lib
and assigned different ones to different components of Clusterio.
Previously imported @clusterio/lib was located at ${_clusterioLibImportedFrom}
This copy of @clusterio/lib is located at ${location}`);
}
}
+7
View File
@@ -10,6 +10,13 @@ export class CommandError extends Error {
code = "CommandError";
}
/**
* Thrown if the installation is broken, such as when multiple copies of lib is imported
*/
export class InstallationError extends Error {
code = "InstallationError";
}
/**
* Thrown from requests sent when an error occured handling it
*/
+3
View File
@@ -45,6 +45,9 @@ export async function loadPluginInfos(pluginList: Map<string, string>) {
pluginInfo = require(pluginPath).plugin;
pluginPackage = require(path.posix.join(pluginPath, "package.json"));
} catch (err: any) {
if (err.code === "InstallationError") {
throw err;
}
throw new libErrors.PluginError(pluginName, err);
}
+1
View File
@@ -7,6 +7,7 @@
"./src/config/*.ts",
"./src/data/*.ts",
"./src/external/*.ts",
"./src/check_singleton_import.ts",
"./src/datastore.ts",
"./src/errors.ts",
"./src/factorio/exchange_string.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": "../../tsconfig.node.json",
"include": ["index.ts", "browser.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
"include": ["index.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
"compilerOptions": {
"allowJs": true,
},
+16
View File
@@ -0,0 +1,16 @@
const assert = require("assert").strict;
const { checkSingletonImport } = require("@clusterio/lib/dist/node/src/check_singleton_import");
// Import lib so it sets the global indicating lib has been imported.
require("@clusterio/lib");
describe("lib/check_singleton_import", function() {
it("Should throw an installation error if called twice", function() {
assert.throws(
() => checkSingletonImport(__filename),
{
message: /Attempt to import duplicate copy of @clusterio\/lib/,
code: "InstallationError",
}
);
});
});