From 276c36b6e9321a2db1dfec2828f0248494f88bcb Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 6 Mar 2026 18:50:18 +0100 Subject: [PATCH] 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. --- packages/controller/controller.ts | 2 +- packages/controller/src/Controller.ts | 3 +++ packages/host/host.ts | 2 +- packages/host/src/Host.ts | 3 +++ packages/lib/browser.ts | 3 +++ packages/lib/index.ts | 3 +++ packages/lib/src/check_singleton_import.ts | 21 +++++++++++++++++++++ packages/lib/src/errors.ts | 7 +++++++ packages/lib/src/plugin_loader.ts | 3 +++ packages/lib/tsconfig.browser.json | 1 + packages/lib/tsconfig.node.json | 2 +- test/lib/check_singleton_import.test.js | 16 ++++++++++++++++ 12 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 packages/lib/src/check_singleton_import.ts create mode 100644 test/lib/check_singleton_import.test.js diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 85b20875..7890c85c 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -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 | diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 7ad03e2c..a5671795 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -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); } diff --git a/packages/host/host.ts b/packages/host/host.ts index 8b06fdf0..485f2926 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -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 | diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 63476061..7e778c32 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -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); } diff --git a/packages/lib/browser.ts b/packages/lib/browser.ts index 58c0debf..66b3d5c1 100644 --- a/packages/lib/browser.ts +++ b/packages/lib/browser.ts @@ -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); diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 96a4da80..d8240ffe 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -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); diff --git a/packages/lib/src/check_singleton_import.ts b/packages/lib/src/check_singleton_import.ts new file mode 100644 index 00000000..6a734128 --- /dev/null +++ b/packages/lib/src/check_singleton_import.ts @@ -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}`); + } +} diff --git a/packages/lib/src/errors.ts b/packages/lib/src/errors.ts index 00071895..e163ef60 100644 --- a/packages/lib/src/errors.ts +++ b/packages/lib/src/errors.ts @@ -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 */ diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 115aef46..37b48ca0 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -45,6 +45,9 @@ export async function loadPluginInfos(pluginList: Map) { 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); } diff --git a/packages/lib/tsconfig.browser.json b/packages/lib/tsconfig.browser.json index 05554ac8..97bcd427 100644 --- a/packages/lib/tsconfig.browser.json +++ b/packages/lib/tsconfig.browser.json @@ -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", diff --git a/packages/lib/tsconfig.node.json b/packages/lib/tsconfig.node.json index a963c8a0..ef6ccd30 100644 --- a/packages/lib/tsconfig.node.json +++ b/packages/lib/tsconfig.node.json @@ -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, }, diff --git a/test/lib/check_singleton_import.test.js b/test/lib/check_singleton_import.test.js new file mode 100644 index 00000000..c4af4938 --- /dev/null +++ b/test/lib/check_singleton_import.test.js @@ -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", + } + ); + }); +});