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", + } + ); + }); +});