From 8a57d9b5f6968341d19a9c16cb1f0ce0dd57c397 Mon Sep 17 00:00:00 2001 From: CCpersonguy Date: Sun, 21 Jan 2024 16:02:29 -0800 Subject: [PATCH 01/38] display Factorio version in Instances web UI --- packages/controller/src/Controller.ts | 2 +- packages/controller/src/HostConnection.ts | 3 +++ packages/controller/src/InstanceInfo.ts | 4 ++++ packages/host/src/Host.ts | 2 ++ packages/host/src/Instance.ts | 2 ++ packages/lib/src/data/messages_instance.ts | 11 +++++++++-- packages/web_ui/src/components/InstanceList.tsx | 15 ++++++++++++++- 7 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index e1e53293..b59499dc 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -896,7 +896,7 @@ export default class Controller { }; instanceConfig.set("factorio.settings", settings); - let instance = new InstanceInfo(instanceConfig, "unassigned", undefined, Date.now()); + let instance = new InstanceInfo(instanceConfig, "unassigned", undefined, undefined, Date.now()); this.instances.set(instanceId, instance); this.instancesDirty = true; await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance); diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index ab566a40..770a7a9d 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -188,6 +188,7 @@ export default class HostConnection extends BaseConnection { let prev = instance.status; instance.status = request.status; instance.gamePort = request.gamePort; + instance.factorioVersion = request.factorioVersion ?? instance.factorioVersion; instance.updatedAtMs = Date.now(); logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`); this._controller.instanceDetailsUpdated([instance]); @@ -225,6 +226,7 @@ export default class HostConnection extends BaseConnection { let prev = controllerInstance.status; controllerInstance.status = instanceData.status; controllerInstance.gamePort = instanceData.gamePort; + controllerInstance.factorioVersion = instanceData.factorioVersion ?? controllerInstance.factorioVersion; controllerInstance.updatedAtMs = Date.now(); logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`); instanceUpdates.push(controllerInstance); @@ -240,6 +242,7 @@ export default class HostConnection extends BaseConnection { instanceConfig, instanceData.status, instanceData.gamePort, + instanceData.factorioVersion, Date.now(), ); this._controller.instances.set(instanceConfig.get("instance.id"), newInstance); diff --git a/packages/controller/src/InstanceInfo.ts b/packages/controller/src/InstanceInfo.ts index 0534c20d..ab664511 100644 --- a/packages/controller/src/InstanceInfo.ts +++ b/packages/controller/src/InstanceInfo.ts @@ -11,6 +11,7 @@ export default class InstanceInfo { public config: lib.InstanceConfig, public status: lib.InstanceStatus, public gamePort?: number, + public factorioVersion?: string, public updatedAtMs = 0, ) { this.config = config; @@ -22,6 +23,7 @@ export default class InstanceInfo { "status": lib.InstanceStatus, "gamePort": Type.Optional(Type.Number()), "updatedAtMs": Type.Optional(Type.Number()), + "factorioVersion": Type.Optional(Type.String()), }); static fromJSON(json: Static, location: lib.ConfigLocation) { @@ -29,6 +31,7 @@ export default class InstanceInfo { lib.InstanceConfig.fromJSON(json.config, location), json.status, json.gamePort, + json.factorioVersion, json.updatedAtMs, ); } @@ -40,6 +43,7 @@ export default class InstanceInfo { this.config.get("instance.assigned_host") ?? undefined, this.gamePort, this.status, + this.factorioVersion, this.updatedAtMs, ); } diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 75e80ffa..ae5cf861 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -667,6 +667,7 @@ export default class Host extends lib.Link { instanceId, instanceConnection ? instanceConnection.instance.status : "stopped", instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId), + instanceConnection ? instanceConnection.instance.server.version ?? undefined : undefined, ) ); @@ -1060,6 +1061,7 @@ export default class Host extends lib.Link { instanceInfo.config.toRemote("controller"), instanceConnection ? instanceConnection.instance.status : "stopped", instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId), + instanceConnection ? instanceConnection.instance.server.version ?? undefined : undefined, )); } await this.send(new lib.InstancesUpdateRequest(list)); diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index f6163611..a31d7b73 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -453,6 +453,8 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); this.id, status, this.server.gamePort, + // server.version can be null before server is initialized + this.server.version ?? undefined, ), ); } diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index d14894d4..13d80966 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -42,6 +42,7 @@ export class InstanceDetails { public assignedHost: number | undefined, public gamePort: number | undefined, public status: InstanceStatus, + public factorioVersion: string | undefined, /** Millisecond Unix timestamp this entry was last updated at */ public updatedAtMs = 0, ) { } @@ -55,6 +56,7 @@ export class InstanceDetails { "unknown", "unassigned", "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", "deleted", ]), + "factorioVersion": Type.Optional(Type.String()), "updatedAtMs": Type.Optional(Type.Number()), }); @@ -65,6 +67,7 @@ export class InstanceDetails { json.assignedHost, json.gamePort, json.status, + json.factorioVersion, json.updatedAtMs, ); } @@ -717,6 +720,7 @@ export class HostInstanceUpdate { public config: Static, public status: InstanceStatus, public gamePort: undefined | number, + public factorioVersion: undefined | string, ) { } static jsonSchema = Type.Object({ @@ -725,10 +729,11 @@ export class HostInstanceUpdate { "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", ]), "gamePort": Type.Optional(Type.Number()), + "factorioVersion": Type.Optional(Type.String()), }); static fromJSON(json: Static) { - return new this(json.config, json.status, json.gamePort); + return new this(json.config, json.status, json.gamePort, json.factorioVersion); } } @@ -822,6 +827,7 @@ export class InstanceStatusChangedEvent { public instanceId: number, public status: InstanceStatus, public gamePort?: number, + public factorioVersion?: string, ) { } static jsonSchema = Type.Object({ @@ -830,10 +836,11 @@ export class InstanceStatusChangedEvent { "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", ]), "gamePort": Type.Optional(Type.Integer()), + "factorioVersion": Type.Optional(Type.String()), }); static fromJSON(json: Static) { - return new this(json.instanceId, json.status, json.gamePort); + return new this(json.instanceId, json.status, json.gamePort, json.factorioVersion); } } diff --git a/packages/web_ui/src/components/InstanceList.tsx b/packages/web_ui/src/components/InstanceList.tsx index c1aa76d0..41034db5 100644 --- a/packages/web_ui/src/components/InstanceList.tsx +++ b/packages/web_ui/src/components/InstanceList.tsx @@ -9,7 +9,7 @@ import { useAccount } from "../model/account"; import { useHosts } from "../model/host"; import InstanceStatusTag from "./InstanceStatusTag"; import StartStopInstanceButton from "./StartStopInstanceButton"; -import { InstanceDetails } from "@clusterio/lib"; +import { InstanceDetails, integerFactorioVersion } from "@clusterio/lib"; import Link from "./Link"; const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare; @@ -46,6 +46,13 @@ export default function InstanceList(props: InstanceListProps) { return `${host.publicAddress}:${instance.gamePort}`; } + function integerFactorioVersionOrDefault(instance: InstanceDetails) { + if (instance.factorioVersion === undefined) { + return -1; + } + return integerFactorioVersion(instance.factorioVersion); + } + let columns: ColumnsType = [ { title: "Name", @@ -86,6 +93,12 @@ export default function InstanceList(props: InstanceListProps) { sorter: (a, b) => strcmp(instancePublicAddress(a), instancePublicAddress(b)), responsive: ["lg"], }, + { + title: "Factorio Version", + key: "version", + render: (_, instance) => instance.factorioVersion ?? "unknown", + sorter: (a, b) => integerFactorioVersionOrDefault(a) - integerFactorioVersionOrDefault(b) + }, { title: "Status", key: "status", From dc2f86dcf37621a733b6903d2e8a645b62bb62a9 Mon Sep 17 00:00:00 2001 From: CCpersonguy Date: Sun, 21 Jan 2024 16:03:52 -0800 Subject: [PATCH 02/38] allow FactorioServer.version to return undefined --- packages/host/src/Host.ts | 4 ++-- packages/host/src/Instance.ts | 3 +-- packages/host/src/export.ts | 2 +- packages/host/src/server.ts | 10 +++++----- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index ae5cf861..491989d5 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -667,7 +667,7 @@ export default class Host extends lib.Link { instanceId, instanceConnection ? instanceConnection.instance.status : "stopped", instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId), - instanceConnection ? instanceConnection.instance.server.version ?? undefined : undefined, + instanceConnection ? instanceConnection.instance.server.version : undefined, ) ); @@ -1061,7 +1061,7 @@ export default class Host extends lib.Link { instanceInfo.config.toRemote("controller"), instanceConnection ? instanceConnection.instance.status : "stopped", instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId), - instanceConnection ? instanceConnection.instance.server.version ?? undefined : undefined, + instanceConnection ? instanceConnection.instance.server.version : undefined, )); } await this.send(new lib.InstancesUpdateRequest(list)); diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index a31d7b73..5bd1b9ac 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -453,8 +453,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); this.id, status, this.server.gamePort, - // server.version can be null before server is initialized - this.server.version ?? undefined, + this.server.version, ), ); } diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 530f8fc9..f5f83528 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -64,7 +64,7 @@ async function generateExportMod(server: FactorioServer) { sourceDir: path.join(__dirname, "..", "..", "..", "lua", "export"), outputDir: server.writePath("mods"), bumpPatch: false, - factorioVersion: server.version.replace(/\.\d+$/, ""), + factorioVersion: server.version?.replace(/\.\d+$/, ""), dependencies, }); } diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index e61f91c0..f58d476b 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -725,12 +725,12 @@ export class FactorioServer extends events.EventEmitter { } /** - * The version of Factorio in use. This will be the actual version if - * "latest" (the default) was specified as `factorioVersion` to the - * constructor. + * The version of Factorio in use. This will be the actual version if + * "latest" (the default) was specified as `factorioVersion` to the constructor. + * This will be undefined before the server is initialized, or if init failed. */ - get version() { - return this._version!; + get version(): string | undefined { + return this._version ?? undefined; } /** From 3d3276b02cdb0e1a62105b9b30d7d2e74af60cc3 Mon Sep 17 00:00:00 2001 From: CCpersonguy Date: Sun, 21 Jan 2024 16:10:49 -0800 Subject: [PATCH 03/38] fix lint errors --- packages/controller/src/HostConnection.ts | 4 +++- packages/web_ui/src/components/InstanceList.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index 770a7a9d..6f725f7a 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -226,7 +226,9 @@ export default class HostConnection extends BaseConnection { let prev = controllerInstance.status; controllerInstance.status = instanceData.status; controllerInstance.gamePort = instanceData.gamePort; - controllerInstance.factorioVersion = instanceData.factorioVersion ?? controllerInstance.factorioVersion; + if (instanceData.factorioVersion) { + controllerInstance.factorioVersion = instanceData.factorioVersion; + } controllerInstance.updatedAtMs = Date.now(); logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`); instanceUpdates.push(controllerInstance); diff --git a/packages/web_ui/src/components/InstanceList.tsx b/packages/web_ui/src/components/InstanceList.tsx index 41034db5..053320cb 100644 --- a/packages/web_ui/src/components/InstanceList.tsx +++ b/packages/web_ui/src/components/InstanceList.tsx @@ -97,7 +97,7 @@ export default function InstanceList(props: InstanceListProps) { title: "Factorio Version", key: "version", render: (_, instance) => instance.factorioVersion ?? "unknown", - sorter: (a, b) => integerFactorioVersionOrDefault(a) - integerFactorioVersionOrDefault(b) + sorter: (a, b) => integerFactorioVersionOrDefault(a) - integerFactorioVersionOrDefault(b), }, { title: "Status", From 594158deac45239a1a1630d60ae6dd22c26bb58f Mon Sep 17 00:00:00 2001 From: CCpersonguy Date: Mon, 22 Jan 2024 21:00:33 -0800 Subject: [PATCH 04/38] Remove verion-caching logic in Controller --- packages/controller/src/HostConnection.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index 6f725f7a..7540452c 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -188,7 +188,7 @@ export default class HostConnection extends BaseConnection { let prev = instance.status; instance.status = request.status; instance.gamePort = request.gamePort; - instance.factorioVersion = request.factorioVersion ?? instance.factorioVersion; + instance.factorioVersion = request.factorioVersion; instance.updatedAtMs = Date.now(); logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`); this._controller.instanceDetailsUpdated([instance]); @@ -226,9 +226,7 @@ export default class HostConnection extends BaseConnection { let prev = controllerInstance.status; controllerInstance.status = instanceData.status; controllerInstance.gamePort = instanceData.gamePort; - if (instanceData.factorioVersion) { - controllerInstance.factorioVersion = instanceData.factorioVersion; - } + controllerInstance.factorioVersion = instanceData.factorioVersion; controllerInstance.updatedAtMs = Date.now(); logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`); instanceUpdates.push(controllerInstance); From b57c419769b6ee11d26aea7f1edfeee0e22e40f4 Mon Sep 17 00:00:00 2001 From: CCpersonguy Date: Mon, 22 Jan 2024 21:01:39 -0800 Subject: [PATCH 05/38] Version should disappear first on narrow windows --- packages/web_ui/src/components/InstanceList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web_ui/src/components/InstanceList.tsx b/packages/web_ui/src/components/InstanceList.tsx index 053320cb..7c05fd41 100644 --- a/packages/web_ui/src/components/InstanceList.tsx +++ b/packages/web_ui/src/components/InstanceList.tsx @@ -98,6 +98,7 @@ export default function InstanceList(props: InstanceListProps) { key: "version", render: (_, instance) => instance.factorioVersion ?? "unknown", sorter: (a, b) => integerFactorioVersionOrDefault(a) - integerFactorioVersionOrDefault(b), + responsive: ["xl"], }, { title: "Status", From e3753f292babb191bb7522311c4d2f23cc9ae05d Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Tue, 27 Feb 2024 11:25:50 -0600 Subject: [PATCH 06/38] Show current factorio version on instance page --- packages/web_ui/src/components/InstanceViewPage.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/web_ui/src/components/InstanceViewPage.tsx b/packages/web_ui/src/components/InstanceViewPage.tsx index 5973cdfa..2f8b03a1 100644 --- a/packages/web_ui/src/components/InstanceViewPage.tsx +++ b/packages/web_ui/src/components/InstanceViewPage.tsx @@ -59,6 +59,9 @@ function InstanceDescription(props: InstanceDescriptionProps) { buttonContent={assigned ? "Reassign" : "Assign"} />} + + {instance.factorioVersion ?? "unknown"} + ; } From 92912fa9e5312fcdcaa8436a8ce3526227c2ea4c Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Wed, 15 May 2024 10:33:37 +0100 Subject: [PATCH 07/38] Show configured instance version while not running --- packages/host/src/Host.ts | 6 ++++-- packages/host/src/Instance.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 491989d5..2f7742dc 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -667,7 +667,9 @@ export default class Host extends lib.Link { instanceId, instanceConnection ? instanceConnection.instance.status : "stopped", instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId), - instanceConnection ? instanceConnection.instance.server.version : undefined, + instanceConnection + ? instanceConnection.instance.server.version + : instanceInfo.config.get("factorio.version"), ) ); @@ -737,7 +739,7 @@ export default class Host extends lib.Link { } assignGamePort(instanceId: number) { - const availablePorts = lib.parseRanges(this.config.get("host.factorio_port_range"), 1, 2**16 - 1); + const availablePorts = lib.parseRanges(this.config.get("host.factorio_port_range"), 1, 2 ** 16 - 1); for (const [id, instance] of this.instanceInfos) { if (id === instanceId) { continue; diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 5bd1b9ac..98aaf5de 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -453,7 +453,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); this.id, status, this.server.gamePort, - this.server.version, + status === "running"? this.server.version : this.config.get("factorio.version"), ), ); } From f1fd8247fb5480ad9d7ebca56ceb930d2da7d70e Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Wed, 15 May 2024 10:40:36 +0100 Subject: [PATCH 08/38] Add factorio version indicator feature to changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd176b9..f6dbf33c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,20 @@ Many thanks to the following for contributing to this release: [@username](https://github.com/username) --> +## Version 2.0.0-alpha.19 + +### Features + +- Added instance version display [#573](https://github.com/clusterio/clusterio/pull/573) + +### Breaking changes + +- @clusterio/controller export InstanceInfo has added factorioVersion parameter + +Many thanks to the following for contributing to this release: +[@CCpersonguy](https://github.com/CCpersonguy) +[@Danielv123](https://github.com/Danielv123) + ## Version 2.0.0-alpha.18 ### Features From e3ab71a2e9344672107b90db229f445833d3cccf Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Mon, 20 May 2024 12:09:41 +0100 Subject: [PATCH 09/38] Fix broken types Some time between antd 5.15.0 and 5.17.3 they moved around some types, which caused our builds to break. This fixes the issue. --- packages/web_ui/src/components/InstanceViewPage.tsx | 4 ++-- packages/web_ui/src/components/SiteLayout.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/web_ui/src/components/InstanceViewPage.tsx b/packages/web_ui/src/components/InstanceViewPage.tsx index 5973cdfa..13ab8f3f 100644 --- a/packages/web_ui/src/components/InstanceViewPage.tsx +++ b/packages/web_ui/src/components/InstanceViewPage.tsx @@ -1,7 +1,7 @@ import React, { useContext, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Alert, Button, Descriptions, Dropdown, Menu, MenuProps, Modal, Space, Spin, Typography } from "antd"; -import type { ItemType } from "antd/es/menu/hooks/useItems"; +import { Alert, Button, Descriptions, Dropdown, MenuProps, Modal, Space, Spin, Typography } from "antd"; +import { ItemType } from "antd/es/menu/interface"; import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; import DownOutlined from "@ant-design/icons/DownOutlined"; diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 5baece76..0e84d85a 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -1,6 +1,7 @@ import React, { Fragment, useContext, useEffect, useState } from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { Dropdown, Layout, Menu, MenuProps } from "antd"; +import { ItemType, MenuItemType } from "antd/es/menu/interface"; import UserOutlined from "@ant-design/icons/UserOutlined"; import webUiPackage from "../../package.json"; @@ -63,7 +64,7 @@ export default function SiteLayout() { combinedPages.push(...plugin.pages); } - let menuItems = []; + let menuItems: ItemType[] = []; let menuGroups = new Map(); for (let { sidebarName, sidebarGroup, permission, path } of combinedPages) { if ( From fddd65ddb8a4e492851478ba7fd31e26426a0722 Mon Sep 17 00:00:00 2001 From: Lars Corbijn Date: Wed, 22 May 2024 18:52:27 +0200 Subject: [PATCH 10/38] Update documentation links for typescript change (#633) --- docs/communication.md | 2 +- docs/config-system.md | 2 +- docs/devs/protocol.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/communication.md b/docs/communication.md index 077baa15..25eb5023 100644 --- a/docs/communication.md +++ b/docs/communication.md @@ -30,7 +30,7 @@ There's also the virtual connector which lets two links in the same program be c ## Message There's two types of messages currently implemented apart from the Link builtin close message and that's Request and Event. -The builtin messages are defined in [packages/lib/link/messages.js](/packages/lib/link/messages.js) and are attached to links via the attachAllMessages function, which is usally called in the Link subclass constructor. +The builtin messages are defined in [packages/lib/src/link/messages.ts](/packages/lib/src/link/messages.ts) and are attached to links via the attachAllMessages function, which is usally called in the Link subclass constructor. The messages define which links they are valid on and only attaches handlers for those links. There's also a forwarding mechanism for when a request or event is to be forwarded another hop. diff --git a/docs/config-system.md b/docs/config-system.md index e822e2b7..2d080c4a 100644 --- a/docs/config-system.md +++ b/docs/config-system.md @@ -6,7 +6,7 @@ Clusterio uses a field based configuration system to manage settings for the con Plugins can add their own config fields for the controller and instances configs. The fields are defined early in the startup, and fields for disabled plugins will still be created. -The built-in config fields are defined in [packages/lib/config/definitions.js](/packages/lib/config/definitions.js) while plugins can add their own config field definitions to the `controllerConfigFields` and `instanceConfigFields` properties of the `plugin` export. +The built-in config fields are defined in [packages/lib/src/config/definitions.ts](/packages/lib/src/config/definitions.ts) while plugins can add their own config field definitions to the `controllerConfigFields` and `instanceConfigFields` properties of the `plugin` export. A `.load_plugin` field is automatically created for each plugin and it's used to enable/disable the loading plugins on startup. diff --git a/docs/devs/protocol.md b/docs/devs/protocol.md index ea28005c..ae072857 100644 --- a/docs/devs/protocol.md +++ b/docs/devs/protocol.md @@ -1,7 +1,7 @@ # Protocol Clusterio communicates using a simple homebrew message passing protocol based on JSON over WebSocket with limited support for reliable transport of messages. -The protocol is implemented in [packages/lib/link](/packages/lib/link) and uses the `ws` library to handle the WebSocket connections on the Node.js side and the native WebSocket API on the browser side. +The protocol is implemented in [packages/lib/src/link](/packages/lib/src/link) and uses the `ws` library to handle the WebSocket connections on the Node.js side and the native WebSocket API on the browser side. Two general kinds of messages are supported: events and requests. Events are messages sent by one party and received by the other and offer no notification for the sender on whether it was actually received. @@ -166,7 +166,7 @@ Events are messages send in one direction over the link, that the receiver is ex By convention message types ending in `*_event` are events. The data payload of an event consists solely of event specific properties. -See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized core events and their contents. +See [packages/lib/src/link/messages.ts](/packages/lib/src/link/messages.ts) for the recognized core events and their contents. Plugins may defined their own events as well. #### `*_event` @@ -181,7 +181,7 @@ They function a lot like HTTP requests except that both parties of a connection By convention message types ending in `*_request` are requests, and is expected to be replied to with a corresponding `*_response` message. If an error occured while processing the request an error response containing an error property in the data payload is sent instead. -See [packages/lib/link/messages.js](/packages/lib/link/messages.js) for the recognized core requests and their contents. +See [packages/lib/src/link/messages.ts](/packages/lib/src/link/messages.ts) for the recognized core requests and their contents. Plugins may define their own requests as well. #### `*_request` From ffc1a0fccdc1f7dd20e768b514697a0b7564f5ba Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 18 May 2024 20:47:06 +0100 Subject: [PATCH 11/38] Add devcontainer configuration file --- .devcontainer/devcontainer.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..58c111f3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "image":"mcr.microsoft.com/devcontainers/universal:2", + "postCreateCommand": "wget https://factorio.com/get-download/1.1.107/headless/linux64 && tar -xf linux64 && pnpm install", +} From 2dbf264d46bbf6a4cf522cf6ab65e22f14b46560 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Fri, 24 May 2024 14:33:54 +0100 Subject: [PATCH 12/38] Add bun compatibility --- package.json | 5 +++++ packages/web_ui/src/components/InstanceViewPage.tsx | 4 ++-- packages/web_ui/src/components/SiteLayout.tsx | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7078aa49..00fee519 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,11 @@ "engines": { "node": ">=18" }, + "workspaces": [ + "packages/*", + "plugins/*", + "external_plugins/*" + ], "devDependencies": { "@clusterio/controller": "workspace:*", "@clusterio/ctl": "workspace:*", diff --git a/packages/web_ui/src/components/InstanceViewPage.tsx b/packages/web_ui/src/components/InstanceViewPage.tsx index 13ab8f3f..0b773d85 100644 --- a/packages/web_ui/src/components/InstanceViewPage.tsx +++ b/packages/web_ui/src/components/InstanceViewPage.tsx @@ -1,7 +1,6 @@ import React, { useContext, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Alert, Button, Descriptions, Dropdown, MenuProps, Modal, Space, Spin, Typography } from "antd"; -import { ItemType } from "antd/es/menu/interface"; import DeleteOutlined from "@ant-design/icons/DeleteOutlined"; import DownOutlined from "@ant-design/icons/DownOutlined"; @@ -25,6 +24,7 @@ import { useHost } from "../model/host"; import InstanceStatusTag from "./InstanceStatusTag"; import Link from "./Link"; +type MenuItem = Required["items"][number]; const { Title } = Typography; type InstanceDescriptionProps = { @@ -71,7 +71,7 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) { let instance = props.instance; let instanceId = instance.id!; - let instanceButtonMenuItems: ItemType[] = []; + let instanceButtonMenuItems: MenuItem[] = []; if (account.hasPermission("core.instance.export_data")) { instanceButtonMenuItems.push({ disabled: exportingData || instance.status !== "stopped", diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 0e84d85a..a24b3490 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -12,6 +12,8 @@ import ControlContext from "./ControlContext"; import { pages } from "../pages"; import { DraggingContext } from "../model/is_dragging"; +type MenuItem = Required["items"][number]; + const { Header, Sider } = Layout; function isActiveDropzone(element: HTMLElement | null): boolean { @@ -64,7 +66,7 @@ export default function SiteLayout() { combinedPages.push(...plugin.pages); } - let menuItems: ItemType[] = []; + let menuItems: MenuItem[] = []; let menuGroups = new Map(); for (let { sidebarName, sidebarGroup, permission, path } of combinedPages) { if ( From fa0b0a6c2114e80702844a6404494c04a1e486a6 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 26 May 2024 19:28:27 +0100 Subject: [PATCH 13/38] Bump ci action versions (#637) --- .github/workflows/node.js.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index eb2ece36..632e46f9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,12 +16,12 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.4.0 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: version: 8 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: pnpm i --no-optional --no-frozen-lockfile @@ -41,11 +41,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.4.0 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: version: 8 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18.x - run: pnpm i --no-optional --no-frozen-lockfile From 70e95a56b9891e624235856723053949b8864b45 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 18 May 2024 18:35:49 +0100 Subject: [PATCH 14/38] Fix test starting alt host under windows --- test/integration/clusterio.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index acf69b94..cfedb438 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -206,6 +206,12 @@ describe("Integration of Clusterio", function() { }); }); it("should auto start instances with auto_start enabled", async function() { + // On windows there's currently no way to automate graceful shutdown of the host + // process as CTRL+C is some weird terminal thing and SIGINT isn't a thing. + if (process.platform === "win32") { + this.skip(); + } + slowTest(this); let hostProcess; From 2c2521a688101b61152af9ad19ddcf659f52cf86 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:46:49 +0100 Subject: [PATCH 15/38] Add error tests for sendEvent and sendRequest (#638) --- packages/web_ui/src/components/SiteLayout.tsx | 1 - test/controller/Controller.js | 135 +++++++++++++++++- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index a24b3490..381d23f7 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -1,7 +1,6 @@ import React, { Fragment, useContext, useEffect, useState } from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { Dropdown, Layout, Menu, MenuProps } from "antd"; -import { ItemType, MenuItemType } from "antd/es/menu/interface"; import UserOutlined from "@ant-design/icons/UserOutlined"; import webUiPackage from "../../package.json"; diff --git a/test/controller/Controller.js b/test/controller/Controller.js index a1aadde7..4ff84f43 100644 --- a/test/controller/Controller.js +++ b/test/controller/Controller.js @@ -1,14 +1,36 @@ "use strict"; const assert = require("assert").strict; const { Controller } = require("@clusterio/controller"); -const { ControllerConfig } = require("@clusterio/lib"); +const { ControllerConfig, Address, RequestError } = require("@clusterio/lib"); +const { EventEmitter } = require("stream"); + +class MockEvent {} + +class MockInstanceConfig extends EventEmitter { + constructor(data = new Map()) { + super(); + this._data = data; + } + + get(field) { + return this._data.get(field); + } + + set(field, value) { + return this._data.set(field, value); + } +} describe("controller/src/Controller", function() { describe("class Controller", function() { /** @type {Controller} */ - let controller; - before(function() { + let controller, mockInstanceConfig; + before(async function() { controller = new Controller({}, [], "", new ControllerConfig("controller")); + mockInstanceConfig = new MockInstanceConfig(new Map([ + ["instance.id", 100], ["instance.name", "test"], ["factorio.settings", []], + ])); + await controller.instanceCreate(mockInstanceConfig); }); function check(list, ip, present = true) { if (present === false) { @@ -50,5 +72,112 @@ describe("controller/src/Controller", function() { check(list, "192.168.0.18", false); }); }); + + describe(".sendRequest()", function() { + it("should error on loopback", function() { + assert.throws( + () => controller.sendRequest(new MockEvent(), Address.fromShorthand("controller")), + new Error(`Message would return back to sender ${Address.fromShorthand("controller")}.`) + ); + }); + it("should error on invalid controller", function() { + assert.throws( + () => controller.sendRequest(new MockEvent(), Address.fromShorthand({ controlId: 99 })), + new RequestError("Target control connection does not exist.") + ); + }); + it("should error on invalid instance", function() { + assert.throws( + () => controller.sendRequest(new MockEvent(), Address.fromShorthand({ instanceId: 99 })), + new RequestError("Instance with ID 99 does not exist") + ); + }); + it("should error on unassigned instance", function() { + mockInstanceConfig.set("instance.assigned_host", null); + assert.throws( + () => controller.sendRequest(new MockEvent(), Address.fromShorthand({ instanceId: 100 })), + new RequestError("Instance is not assigned to a host") + ); + }); + it("should error on instance with disconnected host", function() { + mockInstanceConfig.set("instance.assigned_host", 99); + assert.throws( + () => controller.sendRequest(new MockEvent(), Address.fromShorthand({ instanceId: 100 })), + new RequestError("Host containing instance is not connected") + ); + }); + it("should error on invalid host", function() { + assert.throws( + () => controller.sendRequest(new MockEvent(), Address.fromShorthand({ hostId: 99 })), + new RequestError("Host is not connected") + ); + }); + it("should error on invalid broadcast", function() { + // This is different to sendEvent because requests do not support broadcasting + assert.throws( + () => controller.sendRequest(new MockEvent(), new Address(Address.broadcast, 99)), + new Error(`Unknown address type ${Address.broadcast}`) + ); + }); + it("should error on invalid address", function() { + assert.throws( + () => controller.sendRequest(new MockEvent(), new Address(99, 99)), + new Error("Unknown address type 99") + ); + }); + }); + + describe(".sendEvent()", function() { + it("should error on loopback", function() { + assert.throws( + () => controller.sendEvent(new MockEvent(), Address.fromShorthand("controller")), + new Error(`Message would return back to sender ${Address.fromShorthand("controller")}.`) + ); + }); + it("should error on invalid controller", function() { + assert.throws( + () => controller.sendEvent(new MockEvent(), Address.fromShorthand({ controlId: 99 })), + new Error("Target control connection does not exist.") + ); + }); + it("should error on invalid instance", function() { + assert.throws( + () => controller.sendEvent(new MockEvent(), Address.fromShorthand({ instanceId: 99 })), + new Error("Instance with ID 99 does not exist") + ); + }); + it("should error on unassigned instance", function() { + mockInstanceConfig.set("instance.assigned_host", null); + assert.throws( + () => controller.sendEvent(new MockEvent(), Address.fromShorthand({ instanceId: 100 })), + new Error("Instance is not assigned to a host") + ); + }); + it("should error on instance with disconnected host", function() { + mockInstanceConfig.set("instance.assigned_host", 99); + assert.throws( + () => controller.sendEvent(new MockEvent(), Address.fromShorthand({ instanceId: 100 })), + new Error("Host containing instance is not connected") + ); + }); + it("should error on invalid host", function() { + assert.throws( + () => controller.sendEvent(new MockEvent(), Address.fromShorthand({ hostId: 99 })), + new Error("Host is not connected") + ); + }); + it("should error on invalid broadcast", function() { + assert.throws( + () => controller.sendEvent(new MockEvent(), new Address(Address.broadcast, 99)), + new Error("Unexpected broadcast target 99") + ); + }); + it("should error on invalid address", function() { + assert.throws( + () => controller.sendEvent(new MockEvent(), new Address(99, 99)), + new Error("Unknown address type 99") + ); + }); + }); }); }); From dbc79779eac7ef0a805240b0d4b08173b2aa8363 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 8 Jun 2024 13:52:29 +0100 Subject: [PATCH 16/38] Fix sending log messages across invalid link (#639) --- packages/lib/src/link/connectors.ts | 19 +++++++++++++++++++ packages/lib/src/logging_utils.ts | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index 999800ba..71bfc964 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -34,6 +34,7 @@ export abstract class BaseConnector extends events.EventEmitter { } protected abstract send(message: libData.MessageRoutable): void; + abstract get valid(): boolean; forward(message: libData.MessageRoutable) { const seq = this._seq; @@ -346,6 +347,14 @@ export abstract class WebSocketBaseConnector extends BaseConnector { get hasSession() { return ["connected", "resuming"].includes(this._state); } + + /** + * True if the connector is valid and can accept new messages, + * only needs to be checked on fringe cases during setup and tear down. + */ + get valid() { + return this.hasSession; + } } /** @@ -723,4 +732,14 @@ export class VirtualConnector extends BaseConnector { send(message: libData.Message) { this.other.emit("message", message); } + + /** + * True if the connector is valid and can accept new messages, + * only needs to be checked on fringe cases during setup and tear down. + * + * For a virtual connector this is always true. + */ + get valid() { + return true; + } } diff --git a/packages/lib/src/logging_utils.ts b/packages/lib/src/logging_utils.ts index b3b34d35..49470bd6 100644 --- a/packages/lib/src/logging_utils.ts +++ b/packages/lib/src/logging_utils.ts @@ -207,7 +207,9 @@ export class LinkTransport extends Transport { } try { - this.link.send(new libData.LogMessageEvent(info)); + if (this.link.connector.valid) { + this.link.send(new libData.LogMessageEvent(info)); + } } catch (err) { // Ignore session lost errors. if (!(err instanceof libErrors.SessionLost)) { From 51c450727d90433140e8a11432e51ae8125e2726 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 8 Jun 2024 13:53:00 +0100 Subject: [PATCH 17/38] Add clean script to remove dist and other artifacts (#640) * Add clean script * Switch to using yargs --- clean.js | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 +++- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 clean.js diff --git a/clean.js b/clean.js new file mode 100644 index 00000000..f7d0a0e5 --- /dev/null +++ b/clean.js @@ -0,0 +1,114 @@ +/** + * Clean the repository of build and execution artifacts. + * + * clean: A quick clean that removes all build artifacts making the output safe to publish + * clean-tests: Removes all build and test artifacts should not be required if tests are setup correctly. + * clean-all: Removes all build, test, and execution artifacts. Including cluster config and logs! + */ + +"use strict"; +const path = require("path"); +const fs = require("fs-extra"); +const yargs = require("yargs"); + +const npmPackage = require("./package.json"); +const workspaces = npmPackage.workspaces.map(p => p.slice(0, -2)); + +let DRY = false; + +/** + * Attempt to remove a file / directory if it exists. + * + * @param {string} filePath The path to remove. + */ +async function tryRemove(filePath) { + if (await fs.exists(filePath)) { + // eslint-disable-next-line no-console + console.log(filePath); + } + + if (!DRY) { + await fs.remove(filePath); + } +} + +/** + * Removes all build artifacts + */ +async function removeBuildArtifacts() { + const tasks = []; + + for (const workspace of workspaces) { + const basePath = path.resolve(workspace); + const packages = await fs.readdir(basePath); + for (const packageName of packages) { + tasks.push( + tryRemove(path.join(basePath, packageName, "dist")) + ); + } + } + + await Promise.all(tasks); +} + +/** + * Removes all test artifacts + */ +async function removeTestArtifacts() { + const artifacts = [ + ".nyc_output", "coverage", "temp", "static", + ]; + + await Promise.all(artifacts.map(artifact => tryRemove(path.resolve(artifact)))); +} + +/** + * Removes all execution artifacts + */ +async function removeExecutionArtifacts() { + const artifacts = [ + "database", "instances", "logs", "mods", + "config-control.json", "config-controller.json", "config-host.json", + "FactorioAdminToken.txt", "plugin-list.json", + ]; + + await Promise.all(artifacts.map(artifact => tryRemove(path.resolve(artifact)))); +} + +/** + * Main function for this script + */ +async function main() { + await yargs + .scriptName("clean") + .usage("$0 [options]") + .option("dry", { + nargs: 0, + describe: "Will not delete any files", + default: false, + type: "boolean", + }) + .command("all", "Remove all artifacts", () => {}, argv => { + DRY = argv.dry; + removeBuildArtifacts(); + removeTestArtifacts(); + removeExecutionArtifacts(); + }) + .command("tests", "Remove all build and test artifacts", () => {}, argv => { + DRY = argv.dry; + removeBuildArtifacts(); + removeTestArtifacts(); + }) + .command("fast", "Remove all build artifacts", () => {}, argv => { + DRY = argv.dry; + removeBuildArtifacts(); + }) + .demandCommand(1, "You need to specify a command to run") + .strict() + .parse(); +} + +// Run main if started from command line +if (module === require.main) { + main(); +} diff --git a/package.json b/package.json index 00fee519..280d5e3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "root", "private": true, + "files": [], "scripts": { "build": "tsc --build", "watch": "tsc --build --watch", @@ -11,6 +12,9 @@ "ci-cover": "nyc --reporter=lcovonly pnpm run-script test", "lint": "eslint packages plugins test", "lint-fix": "eslint --fix packages plugins test", + "clean": "node ./clean.js fast", + "clean-tests": "node ./clean.js tests", + "clean-all": "node ./clean.js all", "docs": "typedoc", "bundle-dependencies": "bundle-dependencies" }, @@ -48,6 +52,7 @@ "nyc": "^15.1.0", "phin": "^3.7.0", "typedoc": "^0.24.8", - "typescript": "^5.1.6" + "typescript": "^5.1.6", + "yargs": "^17.7.2" } } From 498638c0c92c4cd42b3964a336d77d91818283cc Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 8 Jun 2024 13:53:23 +0100 Subject: [PATCH 18/38] Improve config, message, and module templates (#641) * Improve config, message, and module templates * Update global handler in template module * Add comments to empty instance template --- packages/create/create.js | 1 + packages/create/template.js | 33 +++++++++++-- .../templates/common/module/control.lua | 14 ++++-- .../create/templates/plugin-js/controller.js | 10 ++++ packages/create/templates/plugin-js/ctl.js | 1 + packages/create/templates/plugin-js/host.js | 6 +++ packages/create/templates/plugin-js/index.js | 43 ++++++++++++----- .../create/templates/plugin-js/instance.js | 17 +++++-- .../templates/plugin-js/instance_empty.js | 11 +++++ .../create/templates/plugin-js/web/plugin.jsx | 31 ++++++++---- .../create/templates/plugin-ts/controller.ts | 10 ++++ packages/create/templates/plugin-ts/ctl.ts | 4 +- packages/create/templates/plugin-ts/host.ts | 6 +++ packages/create/templates/plugin-ts/index.ts | 48 ++++++++++++++----- .../create/templates/plugin-ts/instance.ts | 14 +++++- .../templates/plugin-ts/instance_empty.ts | 7 +++ .../create/templates/plugin-ts/web/plugin.tsx | 24 ++++++---- 17 files changed, 224 insertions(+), 56 deletions(-) create mode 100644 packages/create/templates/plugin-js/instance_empty.js create mode 100644 packages/create/templates/plugin-ts/instance_empty.ts diff --git a/packages/create/create.js b/packages/create/create.js index c6c0d094..eda9364e 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -699,6 +699,7 @@ async function inquirerMissingArgs(args) { { name: "Command Line", value: "ctl" }, { name: "Web UI", value: "web" }, { name: "No Typescript", value: "js" }, + { name: "No Config", value: "no_config" }, ], }, { diff --git a/packages/create/template.js b/packages/create/template.js index 71ce6734..02549f7d 100644 --- a/packages/create/template.js +++ b/packages/create/template.js @@ -76,8 +76,15 @@ async function copyPluginTemplates(pluginName, templates) { const files = new Map(); const prepare = []; + // Some basic flags for selecting files to include const javascriptOnly = templates.includes("js"); - const webpack = templates.includes("controller") || templates.includes("web"); + const config = !templates.includes("no_config") && templates.some(value => ( + ["controller", "host", "instance", "ctl"].includes(value) + )); + const webpack = config || templates.includes("controller") || templates.includes("web"); + + // A count of the number of isolated contexts the plugin runs under + let pluginContexts = 0; // Get the file extension and path to the templates const ext = javascriptOnly ? "js" : "ts"; @@ -88,7 +95,6 @@ async function copyPluginTemplates(pluginName, templates) { files.set(".gitignore", path.join(commonPath, ".gitignore")); files.set(".npmignore", path.join(commonPath, ".npmignore")); files.set("package.json", path.join(commonPath, "package.json")); - files.set(`messages.${ext}`, path.join(templatePath, `messages.${ext}`)); files.set(`index.${ext}`, path.join(templatePath, `index.${ext}`)); // Files and dependencies to support typescript @@ -105,24 +111,30 @@ async function copyPluginTemplates(pluginName, templates) { if (webpack) { prepare.push("webpack-cli --env production"); files.set("webpack.config.js", path.join(commonPath, "webpack.config.js")); - files.set(`web/index.${ext}x`, path.join(templatePath, - templates.includes("web") ? `web/plugin.${ext}x` : `web/no_plugin.${ext}x` - )); + if (templates.includes("web")) { + files.set(`web/index.${ext}x`, path.join(templatePath, `web/plugin.${ext}x`)); + pluginContexts += 1; + } else { + files.set(`web/index.${ext}x`, path.join(templatePath, `web/no_plugin.${ext}x`)); + } } // Files for the controller if (templates.includes("controller")) { files.set(`controller.${ext}`, path.join(templatePath, `controller.${ext}`)); + pluginContexts += 1; } // Files for hosts if (templates.includes("host")) { files.set(`host.${ext}`, path.join(templatePath, `host.${ext}`)); + pluginContexts += 1; } // Files for instances if (templates.includes("instance")) { files.set(`instance.${ext}`, path.join(templatePath, `instance.${ext}`)); + pluginContexts += 1; } // Files for lua modules @@ -133,16 +145,26 @@ async function copyPluginTemplates(pluginName, templates) { files.set("module/module_exports.lua", path.join(commonPath, "module/module_exports.lua")); if (templates.includes("instance")) { files.set("module/globals.lua", path.join(commonPath, "module/globals.lua")); + } else { + files.set(`instance.${ext}`, path.join(templatePath, `instance_empty.${ext}`)); + pluginContexts += 1; } } // Files for the control tool if (templates.includes("ctl")) { files.set(`ctl.${ext}`, path.join(templatePath, `ctl.${ext}`)); + pluginContexts += 1; + } + + // If there are more than one contexts then include the message file + if (pluginContexts > 1) { + files.set(`messages.${ext}`, path.join(templatePath, `messages.${ext}`)); } // Properties that will control the replacements in the templates const properties = { + config: config, typescript: !javascriptOnly, controller: templates.includes("controller"), host: templates.includes("host"), @@ -150,6 +172,7 @@ async function copyPluginTemplates(pluginName, templates) { module: templates.includes("module"), ctl: templates.includes("ctl"), web: templates.includes("web"), + multi_context: pluginContexts > 1, plugin_name: pluginName, prepare: prepare.join(" && "), ext: ext, diff --git a/packages/create/templates/common/module/control.lua b/packages/create/templates/common/module/control.lua index 2747b83b..458f5cf3 100644 --- a/packages/create/templates/common/module/control.lua +++ b/packages/create/templates/common/module/control.lua @@ -6,8 +6,10 @@ local MyModule = { on_nth_tick = {}, } ---- global is 'synced' between players, you should use your plugin name to avoid conflicts --- setupGlobalData should either be removed or called during clusterio_api.events.on_server_startup +--- global is 'synced' between players, you should use your plugin name to avoid name conflicts +-- The following helper function is desync safe and avoids name conflicts after being called during on_server_startup +-- There are edge cases in the following events: pre player left, player left, console command, and config changed +-- If you use any of these events and access globalData then make sure to call this function first local globalData = {} local function setupGlobalData() if global["__plugin_name__"] == nil then @@ -15,6 +17,12 @@ local function setupGlobalData() -- starting values go here } end + MyModule.on_load() +end + +--- The on_load function is called independently for each client when they first load the map +-- It should be used to restore global aliases and metatables not registered with script.register_metatable +function MyModule.on_load() globalData = global["__plugin_name__"] end @@ -28,7 +36,7 @@ local function bar() game.print("bar") end ---- Clusterio provides a few custom events, on_server_startup is the most useful and should be used in place of on_load +--- Clusterio provides a few custom events, on_server_startup is the most useful and should be used in place of on_init MyModule.events[clusterio_api.events.on_server_startup] = function(event) setupGlobalData() game.print(game.table_to_json(event)) diff --git a/packages/create/templates/plugin-js/controller.js b/packages/create/templates/plugin-js/controller.js index 266e06fa..b578d248 100644 --- a/packages/create/templates/plugin-js/controller.js +++ b/packages/create/templates/plugin-js/controller.js @@ -1,13 +1,17 @@ "use strict"; const lib = require("@clusterio/lib"); const { BaseControllerPlugin } = require("@clusterio/controller"); +//%if multi_context // Subscribing requires multi context const { PluginExampleEvent, PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif +//%if multi_context // Subscribing requires multi context } = require("./messages"); +//%endif class ControllerPlugin extends BaseControllerPlugin { //%if controller & web // Subscribing requires web content and the controller @@ -15,14 +19,18 @@ class ControllerPlugin extends BaseControllerPlugin { storageDirty = false; //%endif +//%if multi_context // Subscribing requires multi context async init() { this.controller.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.controller.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if controller & web // Subscribing requires web content and the controller this.controller.subscriptions.handle(ExampleSubscribableUpdate, this.handleExampleSubscription.bind(this)); this.exampleDatabase = new Map(); // If needed, replace with loading from database file //%endif +//%if multi_context // Subscribing requires multi context } +//%endif async onControllerConfigFieldChanged(field, curr, prev) { this.logger.info(`controller::onControllerConfigFieldChanged ${field}`); @@ -45,6 +53,7 @@ class ControllerPlugin extends BaseControllerPlugin { async onPlayerEvent(instance, event) { this.logger.info(`controller::onPlayerEvent ${instance.id} ${JSON.stringify(event)}`); } +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); @@ -57,6 +66,7 @@ class ControllerPlugin extends BaseControllerPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if controller & web // Subscribing requires web content and the controller async handleExampleSubscription(request) { diff --git a/packages/create/templates/plugin-js/ctl.js b/packages/create/templates/plugin-js/ctl.js index ed1d1a32..95e886b9 100644 --- a/packages/create/templates/plugin-js/ctl.js +++ b/packages/create/templates/plugin-js/ctl.js @@ -1,6 +1,7 @@ "use strict"; const { BaseCtlPlugin } = require("@clusterio/ctl"); const { CommandTree, Command } = require("@clusterio/lib"); +//%// We do not check for multi context here because it doesn't make sense to have a ctl without messages const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); /* eslint-disable no-console */ diff --git a/packages/create/templates/plugin-js/host.js b/packages/create/templates/plugin-js/host.js index 2aed38b9..c1e1468d 100644 --- a/packages/create/templates/plugin-js/host.js +++ b/packages/create/templates/plugin-js/host.js @@ -1,13 +1,17 @@ "use strict"; const lib = require("@clusterio/lib"); const { BaseHostPlugin } = require("@clusterio/host"); +//%if multi_context const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +//%endif class HostPlugin extends BaseHostPlugin { +//%if multi_context async init() { this.host.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.host.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); } +//%endif async onHostConfigFieldChanged(field, curr, prev) { this.logger.info(`host::onInstanceConfigFieldChanged ${field}`); @@ -16,6 +20,7 @@ class HostPlugin extends BaseHostPlugin { async onShutdown() { this.logger.info("host::onShutdown"); } +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); @@ -28,6 +33,7 @@ class HostPlugin extends BaseHostPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } module.exports = { diff --git a/packages/create/templates/plugin-js/index.js b/packages/create/templates/plugin-js/index.js index 67d5df0d..26c5387d 100644 --- a/packages/create/templates/plugin-js/index.js +++ b/packages/create/templates/plugin-js/index.js @@ -1,5 +1,6 @@ "use strict"; const lib = require("@clusterio/lib"); +//%if multi_context const Messages = require("./messages"); lib.definePermission({ @@ -13,6 +14,7 @@ lib.definePermission({ title: "Example permission request", description: "Example Description. Request. Change me in index.js", }); +//%endif //%if controller & web // Subscribing requires web content and the controller lib.definePermission({ @@ -34,9 +36,13 @@ const plugin = { name: "__plugin_name__", title: "__plugin_name__", description: "Example Description. Plugin. Change me in index.js", -//%if controller +//%if controller | host & !config | instance & !config | ctl & !config // Blank line for formatting - controllerEntrypoint: "./controller", +//%endif +//%if controller +controllerEntrypoint: "./dist/node/controller", +//%endif +//%if controller & config controllerConfigFields: { "__plugin_name__.myControllerField": { title: "My Controller Field", @@ -46,9 +52,13 @@ const plugin = { }, }, //%endif -//%if host +//%if host & config // Blank line for formatting - hostEntrypoint: "./host", +//%endif +//%if host + hostEntrypoint: "./dist/node/host", +//%endif +//%if host & config hostConfigFields: { "__plugin_name__.myHostField": { title: "My Host Field", @@ -58,9 +68,13 @@ const plugin = { }, }, //%endif -//%if instance +//%if instance & config | module & config // Blank line for formatting - instanceEntrypoint: "./instance", +//%endif +//%if instance | module // Modules load an empty instance plugin + instanceEntrypoint: "./dist/node/instance", +//%endif +//%if instance & config instanceConfigFields: { "__plugin_name__.myInstanceField": { title: "My Instance Field", @@ -70,9 +84,13 @@ const plugin = { }, }, //%endif -//%if ctl +//%if ctl & config // Blank line for formatting - ctlEntrypoint: "./ctl", +//%endif +//%if ctl + ctlEntrypoint: "./dist/node/ctl", +//%endif +//%if ctl & config controlConfigFields: { "__plugin_name__.myControlField": { title: "My Control Field", @@ -82,14 +100,18 @@ const plugin = { }, }, //%endif +//%if multi_context // Subscribing requires multi context messages: [ Messages.PluginExampleEvent, Messages.PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller Messages.ExampleSubscribableUpdate, //%endif +//%if multi_context // Subscribing requires multi context ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", @@ -97,11 +119,6 @@ const plugin = { "/__plugin_name__", ], //%endif -//%if controller & !web // The controller always includes web entry even if there is no content - - webEntrypoint: "./web", - routes: [], -//%endif }; module.exports = { diff --git a/packages/create/templates/plugin-js/instance.js b/packages/create/templates/plugin-js/instance.js index d3bfa189..6993aa1d 100644 --- a/packages/create/templates/plugin-js/instance.js +++ b/packages/create/templates/plugin-js/instance.js @@ -1,16 +1,23 @@ "use strict"; const lib = require("@clusterio/lib"); const { BaseInstancePlugin } = require("@clusterio/host"); +//%if multi_context const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +//%endif class InstancePlugin extends BaseInstancePlugin { +//%if multi_context | module async init() { +//%if multi_context this.instance.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.instance.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if module this.instance.server.handle("__plugin_name__-plugin_example_ipc", this.handlePluginExampleIPC.bind(this)); //%endif +//%if multi_context | module } +//%endif async onInstanceConfigFieldChanged(field, curr, prev) { this.logger.info(`instance::onInstanceConfigFieldChanged ${field}`); @@ -26,13 +33,16 @@ class InstancePlugin extends BaseInstancePlugin { async onPlayerEvent(event) { this.logger.info(`onPlayerEvent::onPlayerEvent ${JSON.stringify(event)}`); +//%if module + if (this.instance.status === "running") { + this.sendRcon("/sc __plugin_name__.foo()"); + } +//%endif } +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); -//%if module - this.sendRcon("/sc __plugin_name__.foo()"); -//%endif } async handlePluginExampleRequest(request) { @@ -42,6 +52,7 @@ class InstancePlugin extends BaseInstancePlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if module async handlePluginExampleIPC(event) { diff --git a/packages/create/templates/plugin-js/instance_empty.js b/packages/create/templates/plugin-js/instance_empty.js new file mode 100644 index 00000000..177f9952 --- /dev/null +++ b/packages/create/templates/plugin-js/instance_empty.js @@ -0,0 +1,11 @@ +"use strict"; +const { BaseInstancePlugin } = require("@clusterio/host"); + +class InstancePlugin extends BaseInstancePlugin { + // This class is empty because an instance plugin must be defined for a module to be injected + // This requirement may change in the future to allow for standalone modules +} + +module.exports = { + InstancePlugin, +}; diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 9cc5942e..2f94158a 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState, -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller useCallback, useSyncExternalStore, //%endif } from "react"; @@ -12,33 +12,37 @@ import React, { import { BaseWebPlugin, PageLayout, Control, ControlContext, notifyErrorHandler, } from "@clusterio/web_ui"; +//%if multi_context // Subscribing requires multi context import { PluginExampleEvent, PluginExampleRequest, -//%if controller & web // Subscribing requires web content and the controller - ExampleSubscribableUpdate, //%endif +//%if controller // Subscribing requires web content and the controller + ExampleSubscribableUpdate, ExampleSubscribableValue, +//%endif +//%if multi_context // Subscribing requires multi context } from "../messages"; +//%endif import * as lib from "@clusterio/lib"; function MyTemplatePage() { let control = useContext(ControlContext); -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller const plugin = control.plugins.get("__plugin_name__"); const [subscribableData, synced] = plugin.useSubscribableData(); //%endif return

__plugin_name__

-//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller Synced: {String(synced)} Data: {JSON.stringify([...subscribableData.values()])} //%endif
; } export class WebPlugin extends BaseWebPlugin { -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller subscribableData = new lib.EventSubscriber(ExampleSubscribableUpdate, this.control); //%endif @@ -47,15 +51,24 @@ export class WebPlugin extends BaseWebPlugin { { path: "/__plugin_name__", sidebarName: "__plugin_name__", - permission: "__plugin_name__.page.view", + // This permission is client side only, so it must match the permission string of a resource request to be secure + // An undefined value means that the page will always be visible +//%if controller // Subscribing requires web content and the controller + permission: "__plugin_name__.example.permission.subscribe", +//%endif +//%if !controller + permission: undefined, // "__plugin_name__.example.permission.request", +//%endif content: , }, ]; +//%if multi_context this.control.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.control.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif } -//%if controller & web // Subscribing requires web content and the controller +//%if controller// Subscribing requires web content and the controller useSubscribableData() { const control = useContext(ControlContext); @@ -63,6 +76,7 @@ export class WebPlugin extends BaseWebPlugin { return useSyncExternalStore(subscribe, () => this.subscribableData.getSnapshot()); } //%endif +//%if multi_context async handlePluginExampleEvent(event) { this.logger.info(JSON.stringify(event)); @@ -75,4 +89,5 @@ export class WebPlugin extends BaseWebPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } diff --git a/packages/create/templates/plugin-ts/controller.ts b/packages/create/templates/plugin-ts/controller.ts index cab2d3c0..cca31956 100644 --- a/packages/create/templates/plugin-ts/controller.ts +++ b/packages/create/templates/plugin-ts/controller.ts @@ -1,12 +1,16 @@ import * as lib from "@clusterio/lib"; import { BaseControllerPlugin, InstanceInfo } from "@clusterio/controller"; +//%if multi_context // Subscribing requires multi context import { PluginExampleEvent, PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif +//%if multi_context // Subscribing requires multi context } from "./messages"; +//%endif export class ControllerPlugin extends BaseControllerPlugin { //%if controller & web // Subscribing requires web content and the controller @@ -14,14 +18,18 @@ export class ControllerPlugin extends BaseControllerPlugin { storageDirty = false; //%endif +//%if multi_context // Subscribing requires multi context async init() { this.controller.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.controller.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if controller & web // Subscribing requires web content and the controller this.controller.subscriptions.handle(ExampleSubscribableUpdate, this.handleExampleSubscription.bind(this)); this.exampleDatabase = new Map(); // If needed, replace with loading from database file //%endif +//%if multi_context // Subscribing requires multi context } +//%endif async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.logger.info(`controller::onControllerConfigFieldChanged ${field}`); @@ -44,6 +52,7 @@ export class ControllerPlugin extends BaseControllerPlugin { async onPlayerEvent(instance: InstanceInfo, event: lib.PlayerEvent) { this.logger.info(`controller::onPlayerEvent ${instance.id} ${JSON.stringify(event)}`); } +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -56,6 +65,7 @@ export class ControllerPlugin extends BaseControllerPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if controller & web // Subscribing requires web content and the controller async handleExampleSubscription(request: lib.SubscriptionRequest) { diff --git a/packages/create/templates/plugin-ts/ctl.ts b/packages/create/templates/plugin-ts/ctl.ts index 75c3a1a4..dc0fa01b 100644 --- a/packages/create/templates/plugin-ts/ctl.ts +++ b/packages/create/templates/plugin-ts/ctl.ts @@ -1,6 +1,6 @@ -type Control = any; // import type { Control } from "@clusterio/ctl"; -import { BaseCtlPlugin } from "@clusterio/ctl"; +import { BaseCtlPlugin, type Control } from "@clusterio/ctl"; import { CommandTree, Command } from "@clusterio/lib"; +//%// We do not check for multi context here because it doesn't make sense to have a ctl without messages import { PluginExampleEvent, PluginExampleRequest } from "./messages"; /* eslint-disable no-console */ diff --git a/packages/create/templates/plugin-ts/host.ts b/packages/create/templates/plugin-ts/host.ts index bdfc05b5..9a395988 100644 --- a/packages/create/templates/plugin-ts/host.ts +++ b/packages/create/templates/plugin-ts/host.ts @@ -1,12 +1,16 @@ import * as lib from "@clusterio/lib"; import { BaseHostPlugin } from "@clusterio/host"; +//%if multi_context import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +//%endif export class HostPlugin extends BaseHostPlugin { +//%if multi_context async init() { this.host.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.host.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); } +//%endif async onHostConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.logger.info(`host::onInstanceConfigFieldChanged ${field}`); @@ -15,6 +19,7 @@ export class HostPlugin extends BaseHostPlugin { async onShutdown() { this.logger.info("host::onShutdown"); } +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -27,4 +32,5 @@ export class HostPlugin extends BaseHostPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } diff --git a/packages/create/templates/plugin-ts/index.ts b/packages/create/templates/plugin-ts/index.ts index d90f22be..8ef46604 100644 --- a/packages/create/templates/plugin-ts/index.ts +++ b/packages/create/templates/plugin-ts/index.ts @@ -1,4 +1,5 @@ import * as lib from "@clusterio/lib"; +//%if multi_context import * as Messages from "./messages"; lib.definePermission({ @@ -12,6 +13,7 @@ lib.definePermission({ title: "Example permission request", description: "Example Description. Request. Change me in index.ts", }); +//%endif //%if controller & web // Subscribing requires web content and the controller lib.definePermission({ @@ -28,37 +30,46 @@ lib.definePermission({ description: "Example Description. View. Change me in index.ts", }); //%endif +//%if config declare module "@clusterio/lib" { -//%if controller +//%endif +//%if controller & config export interface ControllerConfigFields { "__plugin_name__.myControllerField": string; } //%endif -//%if host +//%if host & config export interface HostConfigFields { "__plugin_name__.myHostField": string; } //%endif -//%if instance +//%if instance & config export interface InstanceConfigFields { "__plugin_name__.myInstanceField": string; } //%endif -//%if ctl +//%if ctl & config export interface ControlConfigFields { "__plugin_name__.myControlField": string; } //%endif +//%if config } +//%endif export const plugin: lib.PluginDeclaration = { name: "__plugin_name__", title: "__plugin_name__", description: "Example Description. Plugin. Change me in index.ts", -//%if controller +//%// There are a lot of statements here to make the formatting look good with and without 'config' +//%if controller | host & !config | instance & !config | ctl & !config // Blank line for formatting +//%endif +//%if controller controllerEntrypoint: "./dist/node/controller", +//%endif +//%if controller & config controllerConfigFields: { "__plugin_name__.myControllerField": { title: "My Controller Field", @@ -68,9 +79,13 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif -//%if host +//%if host & config // Blank line for formatting +//%endif +//%if host hostEntrypoint: "./dist/node/host", +//%endif +//%if host & config hostConfigFields: { "__plugin_name__.myHostField": { title: "My Host Field", @@ -80,9 +95,13 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif -//%if instance +//%if instance & config | module & config // Blank line for formatting +//%endif +//%if instance | module // Modules load an empty instance plugin instanceEntrypoint: "./dist/node/instance", +//%endif +//%if instance & config instanceConfigFields: { "__plugin_name__.myInstanceField": { title: "My Instance Field", @@ -92,9 +111,13 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif -//%if ctl +//%if ctl & config // Blank line for formatting +//%endif +//%if ctl ctlEntrypoint: "./dist/node/ctl", +//%endif +//%if ctl & config controlConfigFields: { "__plugin_name__.myControlField": { title: "My Control Field", @@ -104,14 +127,18 @@ export const plugin: lib.PluginDeclaration = { }, }, //%endif +//%if multi_context // Subscribing requires multi context messages: [ Messages.PluginExampleEvent, Messages.PluginExampleRequest, +//%endif //%if controller & web // Subscribing requires web content and the controller Messages.ExampleSubscribableUpdate, //%endif +//%if multi_context // Subscribing requires multi context ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", @@ -119,9 +146,4 @@ export const plugin: lib.PluginDeclaration = { "/__plugin_name__", ], //%endif -//%if controller & !web // The controller always includes web entry even if there is no content - - webEntrypoint: "./web", - routes: [], -//%endif }; diff --git a/packages/create/templates/plugin-ts/instance.ts b/packages/create/templates/plugin-ts/instance.ts index 3cb7d18a..21af1189 100644 --- a/packages/create/templates/plugin-ts/instance.ts +++ b/packages/create/templates/plugin-ts/instance.ts @@ -1,6 +1,8 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; +//%if multi_context import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +//%endif //%if module type PuginExampleIPC = { @@ -10,13 +12,19 @@ type PuginExampleIPC = { //%endif export class InstancePlugin extends BaseInstancePlugin { +//%if multi_context | module async init() { +//%endif +//%if multi_context this.instance.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.instance.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif //%if module this.instance.server.handle("__plugin_name__-plugin_example_ipc", this.handlePluginExampleIPC.bind(this)); //%endif +//%if multi_context | module } +//%endif async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { this.logger.info(`instance::onInstanceConfigFieldChanged ${field}`); @@ -33,9 +41,12 @@ export class InstancePlugin extends BaseInstancePlugin { async onPlayerEvent(event: lib.PlayerEvent) { this.logger.info(`onPlayerEvent::onPlayerEvent ${JSON.stringify(event)}`); //%if module - this.sendRcon("/sc __plugin_name__.foo()"); + if (this.instance.status === "running") { + this.sendRcon("/sc __plugin_name__.foo()"); + } //%endif } +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -48,6 +59,7 @@ export class InstancePlugin extends BaseInstancePlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif //%if module async handlePluginExampleIPC(event: PuginExampleIPC) { diff --git a/packages/create/templates/plugin-ts/instance_empty.ts b/packages/create/templates/plugin-ts/instance_empty.ts new file mode 100644 index 00000000..e8fa7eeb --- /dev/null +++ b/packages/create/templates/plugin-ts/instance_empty.ts @@ -0,0 +1,7 @@ +import * as lib from "@clusterio/lib"; +import { BaseInstancePlugin } from "@clusterio/host"; + +export class InstancePlugin extends BaseInstancePlugin { + // This class is empty because an instance plugin must be defined for a module to be injected + // This requirement may change in the future to allow for standalone modules +} diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index df32127d..06a7542a 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -12,33 +12,37 @@ import React, { import { BaseWebPlugin, PageLayout, Control, ControlContext, notifyErrorHandler, } from "@clusterio/web_ui"; +//%if multi_context // Subscribing requires multi context import { PluginExampleEvent, PluginExampleRequest, -//%if controller & web // Subscribing requires web content and the controller - ExampleSubscribableUpdate, //%endif +//%if controller & web // Subscribing requires web content and the controller + ExampleSubscribableUpdate, ExampleSubscribableValue, +//%endif +//%if multi_context // Subscribing requires multi context } from "../messages"; +//%endif import * as lib from "@clusterio/lib"; function MyTemplatePage() { const control = useContext(ControlContext); -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller const plugin = control.plugins.get("__plugin_name__") as WebPlugin; const [subscribableData, synced] = plugin.useSubscribableData(); //%endif return

__plugin_name__

-//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller Synced: {String(synced)} Data: {JSON.stringify([...subscribableData.values()])} //%endif
; } export class WebPlugin extends BaseWebPlugin { -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller subscribableData = new lib.EventSubscriber(ExampleSubscribableUpdate, this.control); //%endif @@ -49,20 +53,22 @@ export class WebPlugin extends BaseWebPlugin { sidebarName: "__plugin_name__", // This permission is client side only, so it must match the permission string of a resource request to be secure // An undefined value means that the page will always be visible -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller permission: "__plugin_name__.example.permission.subscribe", //%endif -//%if !controller | !web +//%if !controller permission: undefined, // "__plugin_name__.example.permission.request", //%endif content: , }, ]; +//%if multi_context this.control.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); this.control.handle(PluginExampleRequest, this.handlePluginExampleRequest.bind(this)); +//%endif } -//%if controller & web // Subscribing requires web content and the controller +//%if controller // Subscribing requires web content and the controller useSubscribableData() { const control = useContext(ControlContext); @@ -70,6 +76,7 @@ export class WebPlugin extends BaseWebPlugin { return useSyncExternalStore(subscribe, () => this.subscribableData.getSnapshot()); } //%endif +//%if multi_context async handlePluginExampleEvent(event: PluginExampleEvent) { this.logger.info(JSON.stringify(event)); @@ -82,4 +89,5 @@ export class WebPlugin extends BaseWebPlugin { myResponseNumbers: request.myNumberArray, }; } +//%endif } From 51657bea447bbca461fc076b36392ce36db81cef Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 8 Jun 2024 06:39:58 +0100 Subject: [PATCH 19/38] Extend PageHeader for plugins My plugins used the antd pageheader more than base did. I did not notice that these features dissappeared when we migrated from antd to our own implementation - this brings them back --- packages/web_ui/src/components/PageHeader.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/web_ui/src/components/PageHeader.tsx b/packages/web_ui/src/components/PageHeader.tsx index 97377ea9..4c78bf31 100644 --- a/packages/web_ui/src/components/PageHeader.tsx +++ b/packages/web_ui/src/components/PageHeader.tsx @@ -1,13 +1,19 @@ import React from "react"; +import { Space, Typography } from "antd"; +const { Text } = Typography; type PageHeaderProps = { - title: string|React.ReactElement; + title: string | React.ReactElement; + subTitle?: string | React.ReactElement; extra?: React.JSX.Element; }; export default function PageHeader(props: PageHeaderProps): React.JSX.Element { return
-

{props.title}

+ +

{props.title}

+ {props.subTitle && {props.subTitle}} +
{props.extra &&
{props.extra}
}
; } From 1f47f6dfc995d40d264faa8f6707cde4dd2e69a8 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 8 Jun 2024 14:18:29 +0100 Subject: [PATCH 20/38] Update all pages to use PageHeader --- packages/create/templates/plugin-js/web/plugin.jsx | 4 ++-- packages/create/templates/plugin-ts/web/plugin.tsx | 4 ++-- packages/web_ui/src/components/PluginViewPage.tsx | 3 ++- packages/web_ui/src/components/PluginsPage.tsx | 3 ++- plugins/inventory_sync/web/index.tsx | 4 ++-- plugins/subspace_storage/web/index.tsx | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 2f94158a..87c59767 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -10,7 +10,7 @@ import React, { // } from "antd"; import { - BaseWebPlugin, PageLayout, Control, ControlContext, notifyErrorHandler, + BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler, } from "@clusterio/web_ui"; //%if multi_context // Subscribing requires multi context @@ -34,7 +34,7 @@ function MyTemplatePage() { //%endif return -

__plugin_name__

+ //%if controller // Subscribing requires web content and the controller Synced: {String(synced)} Data: {JSON.stringify([...subscribableData.values()])} //%endif diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index 06a7542a..e54e0a7c 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -10,7 +10,7 @@ import React, { // } from "antd"; import { - BaseWebPlugin, PageLayout, Control, ControlContext, notifyErrorHandler, + BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler, } from "@clusterio/web_ui"; //%if multi_context // Subscribing requires multi context @@ -34,7 +34,7 @@ function MyTemplatePage() { //%endif return -

__plugin_name__

+ //%if controller // Subscribing requires web content and the controller Synced: {String(synced)} Data: {JSON.stringify([...subscribableData.values()])} //%endif diff --git a/packages/web_ui/src/components/PluginViewPage.tsx b/packages/web_ui/src/components/PluginViewPage.tsx index 8b9281fd..58eba31a 100644 --- a/packages/web_ui/src/components/PluginViewPage.tsx +++ b/packages/web_ui/src/components/PluginViewPage.tsx @@ -6,6 +6,7 @@ import type { PluginWebApi } from "@clusterio/lib"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; +import PageHeader from "./PageHeader"; import notify from "../util/notify"; export default function PluginViewPage() { @@ -39,7 +40,7 @@ export default function PluginViewPage() { let pluginMeta = pluginList.find(p => p.name === pluginName); if (!pluginMeta) { return -

Plugin not found

+

Plugin with name {pluginName} was not found on the controller.

; } diff --git a/packages/web_ui/src/components/PluginsPage.tsx b/packages/web_ui/src/components/PluginsPage.tsx index f12b3d00..9d721852 100644 --- a/packages/web_ui/src/components/PluginsPage.tsx +++ b/packages/web_ui/src/components/PluginsPage.tsx @@ -9,6 +9,7 @@ import type { PluginWebApi } from "@clusterio/lib"; import notify from "../util/notify"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; +import PageHeader from "./PageHeader"; const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare; @@ -47,7 +48,7 @@ export default function PluginsPage() { } return -

Plugins

+ -

Inventory sync

+ {statsData && <>

Database size: {Math.round(statsData.databaseSize / 1000)}kB

Database entries: {statsData.databaseEntries}

diff --git a/plugins/subspace_storage/web/index.tsx b/plugins/subspace_storage/web/index.tsx index 345795f2..2d1b458a 100644 --- a/plugins/subspace_storage/web/index.tsx +++ b/plugins/subspace_storage/web/index.tsx @@ -3,7 +3,7 @@ import { Input, Table, Typography } from "antd"; import * as lib from "@clusterio/lib"; import { - BaseWebPlugin, PageLayout, Control, ControlContext, + BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, notifyErrorHandler, useItemMetadata, useLocale, } from "@clusterio/web_ui"; import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages"; @@ -60,7 +60,7 @@ function StoragePage() { let numberFormat = new Intl.NumberFormat("en-US"); return -

Storage

+ Date: Sat, 6 Jul 2024 17:48:14 +0100 Subject: [PATCH 21/38] Fix missing codecov upload token --- .github/workflows/node.js.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 632e46f9..69a7fa1f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -34,9 +34,10 @@ jobs: run: pnpm run ci-cover - name : Upload coverage to Codecov if: ${{ matrix.node-version == '18.x' }} - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: files: ./coverage/lcov.info + token: ${{ secrets.CODECOV_TOKEN }} lint: runs-on: ubuntu-latest From 02df956f6cc4f7e5a48fedf80ac8a6120da913a6 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:44:53 +0100 Subject: [PATCH 22/38] Fix breaking change introduced in v5.5 https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#simplified-reference-directive-declaration-emit --- packages/web_ui/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web_ui/src/index.ts b/packages/web_ui/src/index.ts index 601e092c..2053560a 100644 --- a/packages/web_ui/src/index.ts +++ b/packages/web_ui/src/index.ts @@ -1,4 +1,4 @@ -/// +/// export { default as bootstrap } from "./bootstrap"; export { default as notify, notifyErrorHandler } from "./util/notify"; export { default as BaseWebPlugin } from "./BaseWebPlugin"; From 08c522429bc57b63f4ae22d56ae1b957ccbbb232 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:25:54 +0100 Subject: [PATCH 23/38] Implemented configDir for outDir --- CHANGELOG.md | 1 + package.json | 6 +++--- packages/controller/package.json | 2 +- packages/controller/tsconfig.browser.json | 1 - packages/controller/tsconfig.node.json | 3 --- packages/create/templates/plugin-ts/tsconfig.browser.json | 3 --- packages/create/templates/plugin-ts/tsconfig.node.json | 3 --- packages/ctl/package.json | 2 +- packages/ctl/tsconfig.node.json | 3 --- packages/host/package.json | 2 +- packages/host/tsconfig.node.json | 3 --- packages/lib/package.json | 2 +- packages/lib/tsconfig.browser.json | 1 - packages/lib/tsconfig.node.json | 1 - packages/web_ui/package.json | 2 +- packages/web_ui/tsconfig.browser.json | 3 --- plugins/global_chat/package.json | 2 +- plugins/global_chat/tsconfig.browser.json | 3 --- plugins/global_chat/tsconfig.node.json | 3 --- plugins/inventory_sync/package.json | 2 +- plugins/inventory_sync/tsconfig.browser.json | 3 --- plugins/inventory_sync/tsconfig.node.json | 3 --- plugins/player_auth/package.json | 2 +- plugins/player_auth/tsconfig.browser.json | 3 --- plugins/player_auth/tsconfig.node.json | 3 --- plugins/research_sync/package.json | 2 +- plugins/research_sync/tsconfig.browser.json | 3 --- plugins/research_sync/tsconfig.node.json | 3 --- plugins/statistics_exporter/package.json | 2 +- plugins/statistics_exporter/tsconfig.browser.json | 3 --- plugins/statistics_exporter/tsconfig.node.json | 3 --- plugins/subspace_storage/package.json | 2 +- plugins/subspace_storage/tsconfig.browser.json | 3 --- plugins/subspace_storage/tsconfig.node.json | 3 --- tsconfig.browser.json | 1 + tsconfig.node.json | 1 + 36 files changed, 17 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 099eb639..66319a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Many thanks to the following for contributing to this release: - Fixed IPC errors reporting undefined when no stack trace was present [#624](https://github.com/clusterio/clusterio/pull/624). - Fixed loopback routing for instances reporting the wrong error message [#625](https://github.com/clusterio/clusterio/pull/625). +- Bumped Typescript version to 5.5 and implemented `${configDir}` in base configs for "outDir" [#648](https://github.com/clusterio/clusterio/pull/648). ### Breaking Changes diff --git a/package.json b/package.json index 280d5e3c..985bbc79 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "@clusterio/controller": "workspace:*", "@clusterio/ctl": "workspace:*", "@clusterio/host": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/lib": "workspace:*", + "@clusterio/web_ui": "workspace:*", "@swc/core": "^1.4.0", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", @@ -51,8 +51,8 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "phin": "^3.7.0", - "typedoc": "^0.24.8", - "typescript": "^5.1.6", + "typedoc": "^0.26.3", + "typescript": "^5.5.3", "yargs": "^17.7.2" } } diff --git a/packages/controller/package.json b/packages/controller/package.json index 98251fd8..d83a87b8 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -51,7 +51,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.4.14", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-dev-middleware": "^6.1.1", "webpack-merge": "^5.9.0" diff --git a/packages/controller/tsconfig.browser.json b/packages/controller/tsconfig.browser.json index 154bd176..01b8e68f 100644 --- a/packages/controller/tsconfig.browser.json +++ b/packages/controller/tsconfig.browser.json @@ -7,6 +7,5 @@ "include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ], "compilerOptions": { "rootDir": "..", - "outDir": "dist/browser", } } diff --git a/packages/controller/tsconfig.node.json b/packages/controller/tsconfig.node.json index 9dcc91db..52b0c217 100644 --- a/packages/controller/tsconfig.node.json +++ b/packages/controller/tsconfig.node.json @@ -4,7 +4,4 @@ { "path": "../lib/tsconfig.node.json" }, ], "include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/packages/create/templates/plugin-ts/tsconfig.browser.json b/packages/create/templates/plugin-ts/tsconfig.browser.json index e1b39d0f..325fd71d 100644 --- a/packages/create/templates/plugin-ts/tsconfig.browser.json +++ b/packages/create/templates/plugin-ts/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/packages/create/templates/plugin-ts/tsconfig.node.json b/packages/create/templates/plugin-ts/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/packages/create/templates/plugin-ts/tsconfig.node.json +++ b/packages/create/templates/plugin-ts/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/packages/ctl/package.json b/packages/ctl/package.json index de6a89da..385d4eae 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -35,6 +35,6 @@ "@types/node": "^20.4.5", "@types/set-blocking": "^2.0.0", "@types/yargs": "^17.0.24", - "typescript": "^5.1.6" + "typescript": "^5.5.3" } } diff --git a/packages/ctl/tsconfig.node.json b/packages/ctl/tsconfig.node.json index 3a0fb105..40446ef9 100644 --- a/packages/ctl/tsconfig.node.json +++ b/packages/ctl/tsconfig.node.json @@ -4,7 +4,4 @@ { "path": "../lib/tsconfig.node.json" }, ], "include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/packages/host/package.json b/packages/host/package.json index 73384900..22fc8306 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -43,6 +43,6 @@ "@types/semver": "^7.5.0", "@types/set-blocking": "^2.0.0", "@types/yargs": "^17.0.24", - "typescript": "^5.1.6" + "typescript": "^5.5.3" } } diff --git a/packages/host/tsconfig.node.json b/packages/host/tsconfig.node.json index e7a488be..689d80e5 100644 --- a/packages/host/tsconfig.node.json +++ b/packages/host/tsconfig.node.json @@ -4,7 +4,4 @@ { "path": "../lib/tsconfig.node.json" }, ], "include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/packages/lib/package.json b/packages/lib/package.json index bf27858b..f45d13d8 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -41,6 +41,6 @@ "@types/triple-beam": "^1.3.2", "@types/ws": "^8.5.5", "@types/yargs": "^17.0.24", - "typescript": "^5.1.6" + "typescript": "^5.5.3" } } diff --git a/packages/lib/tsconfig.browser.json b/packages/lib/tsconfig.browser.json index e3c20c4e..0eeb49e4 100644 --- a/packages/lib/tsconfig.browser.json +++ b/packages/lib/tsconfig.browser.json @@ -26,6 +26,5 @@ ], "compilerOptions": { "rootDir": ".", - "outDir": "dist/browser", } } diff --git a/packages/lib/tsconfig.node.json b/packages/lib/tsconfig.node.json index d3dab4e1..a963c8a0 100644 --- a/packages/lib/tsconfig.node.json +++ b/packages/lib/tsconfig.node.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.node.json", "include": ["index.ts", "browser.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"], "compilerOptions": { - "outDir": "dist/node", "allowJs": true, }, } diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index 658e4611..f61a632f 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -40,7 +40,7 @@ "style-loader": "^3.3.3", "swc-loader": "^0.2.6", "terser-webpack-plugin": "^5.3.9", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "util": "^0.12.5", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", diff --git a/packages/web_ui/tsconfig.browser.json b/packages/web_ui/tsconfig.browser.json index 62dbe09d..4c6a3729 100644 --- a/packages/web_ui/tsconfig.browser.json +++ b/packages/web_ui/tsconfig.browser.json @@ -4,7 +4,4 @@ { "path": "../lib/tsconfig.browser.json" }, ], "include": [ "src/**/*.tsx", "src/**/*.ts", "package.json", "global.d.ts" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/global_chat/package.json b/plugins/global_chat/package.json index 5b644083..b8d97aa2 100644 --- a/plugins/global_chat/package.json +++ b/plugins/global_chat/package.json @@ -25,7 +25,7 @@ "@clusterio/web_ui": "workspace:*", "@types/node": "^20.4.5", "@types/react": "^18.2.21", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/global_chat/tsconfig.browser.json b/plugins/global_chat/tsconfig.browser.json index e1b39d0f..325fd71d 100644 --- a/plugins/global_chat/tsconfig.browser.json +++ b/plugins/global_chat/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/global_chat/tsconfig.node.json b/plugins/global_chat/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/plugins/global_chat/tsconfig.node.json +++ b/plugins/global_chat/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index ea3cc927..e184e734 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -33,7 +33,7 @@ "antd": "^5.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/inventory_sync/tsconfig.browser.json b/plugins/inventory_sync/tsconfig.browser.json index e1b39d0f..325fd71d 100644 --- a/plugins/inventory_sync/tsconfig.browser.json +++ b/plugins/inventory_sync/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/inventory_sync/tsconfig.node.json b/plugins/inventory_sync/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/plugins/inventory_sync/tsconfig.node.json +++ b/plugins/inventory_sync/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index 17cbe97c..a7c40a5b 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -36,7 +36,7 @@ "phin": "^3.7.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/player_auth/tsconfig.browser.json b/plugins/player_auth/tsconfig.browser.json index e1b39d0f..325fd71d 100644 --- a/plugins/player_auth/tsconfig.browser.json +++ b/plugins/player_auth/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/player_auth/tsconfig.node.json b/plugins/player_auth/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/plugins/player_auth/tsconfig.node.json +++ b/plugins/player_auth/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index 5cf31dc2..955874ae 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -30,7 +30,7 @@ "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "@types/react": "^18.2.21", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/research_sync/tsconfig.browser.json b/plugins/research_sync/tsconfig.browser.json index e1b39d0f..325fd71d 100644 --- a/plugins/research_sync/tsconfig.browser.json +++ b/plugins/research_sync/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/research_sync/tsconfig.node.json b/plugins/research_sync/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/plugins/research_sync/tsconfig.node.json +++ b/plugins/research_sync/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index 09d09f0a..cb4e9d0c 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -25,7 +25,7 @@ "@clusterio/web_ui": "workspace:*", "@types/node": "^20.4.5", "@types/react": "^18.2.21", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/statistics_exporter/tsconfig.browser.json b/plugins/statistics_exporter/tsconfig.browser.json index f2dc36da..ff89a801 100644 --- a/plugins/statistics_exporter/tsconfig.browser.json +++ b/plugins/statistics_exporter/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/statistics_exporter/tsconfig.node.json b/plugins/statistics_exporter/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/plugins/statistics_exporter/tsconfig.node.json +++ b/plugins/statistics_exporter/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index 26fa03e8..e6de5b57 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -34,7 +34,7 @@ "antd": "^5.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^5.1.6", + "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "webpack-merge": "^5.9.0" diff --git a/plugins/subspace_storage/tsconfig.browser.json b/plugins/subspace_storage/tsconfig.browser.json index e1b39d0f..325fd71d 100644 --- a/plugins/subspace_storage/tsconfig.browser.json +++ b/plugins/subspace_storage/tsconfig.browser.json @@ -5,7 +5,4 @@ { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], - "compilerOptions": { - "outDir": "dist/browser", - } } diff --git a/plugins/subspace_storage/tsconfig.node.json b/plugins/subspace_storage/tsconfig.node.json index b6197500..ce0e013f 100644 --- a/plugins/subspace_storage/tsconfig.node.json +++ b/plugins/subspace_storage/tsconfig.node.json @@ -5,7 +5,4 @@ ], "include": ["./**/*.ts"], "exclude": ["test/*", "./dist/*"], - "compilerOptions": { - "outDir": "dist/node", - }, } diff --git a/tsconfig.browser.json b/tsconfig.browser.json index 605c2c61..b5d82db1 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -7,5 +7,6 @@ "target": "es2022", "jsx": "react-jsx", "noEmitOnError": true, + "outDir": "${configDir}/dist/browser", }, } diff --git a/tsconfig.node.json b/tsconfig.node.json index 700d02da..dd436f25 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -6,5 +6,6 @@ "moduleResolution": "node16", "target": "es2022", "forceConsistentCasingInFileNames": true, + "outDir": "${configDir}/dist/node", }, } From 660af756e6beba8f856a489f6d3d9c71a320e74e Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:03:09 +0100 Subject: [PATCH 24/38] Minor Corrections --- packages/controller/package.json | 4 ++-- packages/controller/src/Controller.ts | 2 +- packages/create/templates/common/package.json | 2 +- packages/lib/src/link/link.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/controller/package.json b/packages/controller/package.json index d83a87b8..2256e2a9 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -29,7 +29,6 @@ "jsonwebtoken": "^9.0.1", "jszip": "^3.10.1", "set-blocking": "^2.0.0", - "webpack-cli": "^5.1.4", "winston": "^3.10.0", "winston-daily-rotate-file": "^4.7.1", "ws": "^8.13.0", @@ -54,7 +53,8 @@ "typescript": "^5.5.3", "webpack": "^5.88.2", "webpack-dev-middleware": "^6.1.1", - "webpack-merge": "^5.9.0" + "webpack-merge": "^5.9.0", + "webpack-cli": "^5.1.4" }, "publishConfig": { "access": "public" diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index f256da32..8ae227bc 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -95,7 +95,7 @@ export default class Controller { public shouldRestart: boolean = false; _fallbackedRequests: Map, lib.RequestHandler> = new Map(); _registeredRequests: Map, lib.RequestHandler> = new Map(); - _registeredEvents = new Map(); + _registeredEvents = new Map, lib.EventHandler>(); _snoopedEvents = new Map(); devMiddleware: any | null = null; diff --git a/packages/create/templates/common/package.json b/packages/create/templates/common/package.json index 346cce77..b05122b3 100644 --- a/packages/create/templates/common/package.json +++ b/packages/create/templates/common/package.json @@ -20,7 +20,7 @@ "devDependencies": { //%// Typescript and type declarations //%if typescript - "typescript": "^5.1.6", + "typescript": "^5.5.3", "@types/node": "^20.4.5", //%endif //%if typescript & web diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index ae91a4c8..236b5340 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -538,7 +538,7 @@ export class Link { if (!entry) { throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`); } - if (this.connector.src.equals(dst)) { + if (this.connector.src.addressedTo(dst)) { throw new Error(`Message would return back to sender ${dst}.`); } if (this.validateSent) { @@ -575,7 +575,7 @@ export class Link { if (!entry) { throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`); } - if (this.connector.src.equals(dst)) { + if (this.connector.src.addressedTo(dst)) { throw new Error(`Message would return back to sender ${dst}.`); } if (this.validateSent) { From 70cf680eddff8f3ca628a573ba098abb51d5f39d Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 29 Aug 2024 21:04:34 +0200 Subject: [PATCH 25/38] Escape arguments when installer invokes commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The usage of shell: true means that no escaping is done on the arguments passed to the commands invoked by the installer. This leads to the installation failing if the user for example puts a space in the username for the admin account. Fix by escaping the arguments passed on command lines. This is unreasonably complicated on Windows 😡. --- packages/create/create.js | 25 ++++++++---- packages/create/escape_arg.js | 62 ++++++++++++++++++++++++++++++ test/create/echo_args.js | 4 ++ test/create/escape_arg.js | 71 +++++++++++++++++++++++++++++++++++ test/create/forward_args.cmd | 3 ++ test/create/forward_args.sh | 3 ++ 6 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 packages/create/escape_arg.js create mode 100644 test/create/echo_args.js create mode 100644 test/create/escape_arg.js create mode 100644 test/create/forward_args.cmd create mode 100755 test/create/forward_args.sh diff --git a/packages/create/create.js b/packages/create/create.js index eda9364e..037c31c8 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -16,6 +16,7 @@ const { copyPluginTemplates } = require("./template"); let dev = false; const scriptExt = process.platform === "win32" ? ".cmd" : ""; const finished = util.promisify(stream.finished); +const { escapeArg } = require("./escape_arg"); // I hate this, there is a bug with rl.close on windows that was meant to have been fixed in Node14.4 // See nodejs/node#21771 and nodejs/node#30701 for the apparent fix that was applied @@ -106,15 +107,25 @@ async function safeOutputFile(file, data, options={}) { } async function execFile(cmd, args) { - logger.verbose(`executing ${cmd} ${args.join(" ")}`); + const escaped = args.map(escapeArg); + logger.verbose(`executing ${cmd} ${escaped.join(" ")}`); return new Promise((resolve, reject) => { - let child = child_process.execFile(cmd, args, { shell: true }, (err, stdout, stderr) => { - if (err) { - reject(err); - } else { - resolve({ stdout, stderr }); + let child = child_process.execFile( + cmd, + escaped, + { + shell: true, + // eslint-disable-next-line node/no-process-env + env: process.platform === "win32" ? { ...process.env, pct: "%" } : undefined, + }, + (err, stdout, stderr) => { + if (err) { + reject(err); + } else { + resolve({ stdout, stderr }); + } } - }); + ); let stdout = new LineSplitter({ readableObjectMode: true }); stdout.on("data", line => { logger.verbose(line.toString()); }); child.stdout.pipe(stdout); diff --git a/packages/create/escape_arg.js b/packages/create/escape_arg.js new file mode 100644 index 00000000..143257d2 --- /dev/null +++ b/packages/create/escape_arg.js @@ -0,0 +1,62 @@ +"use strict"; + +function escapeArg(arg) { + // When shell: true is used execFile does not escaping of arguments, they are + // simply joined by space and then passed to the shell. + + // For Windows the command is cmd.exe /d /s /c "command arg arg ..." + // The parsing and escaping on windows is cursed. The following references are not for mortals: + // https://daviddeley.com/autohotkey/parameters/parameters.htm#WIN + // https://stackoverflow.com/a/4095133 + // https://ss64.com/nt/syntax-esc.html + if (process.platform === "win32") { + if (!/[\t "%&<>\\^|]/.test(arg)) { + return arg; + } + + const escaped = []; + let pos = 0; + while (pos < arg.length) { + if (arg[pos] === "\\") { + // If a backslash is followed by a quote or the end of the string, then + // double the number of backslashes because why not! + let peek = pos + 1; + while (peek !== arg.length && arg[peek] === "\\") { + peek += 1; + } + if (peek === arg.length || arg[peek] === '"') { + const count = peek - pos; + escaped.push("\\".repeat(count * 2)); + pos = peek; + continue; + } else { + escaped.push("\\"); + } + } else if (arg[pos] === '"') { + escaped.push('""'); + } else if (arg[pos] === "%") { + // Expect a pct environment variable to contain %, because cmd does variable substitutions + // before parsing string quotes, and there's no mechanism for escaping % symbols, and this + // is a half sane workaround. The less sane workaround is to replace % with %%cd:~,%. + escaped.push("%pct%"); + } else { + escaped.push(arg[pos]); + } + pos += 1; + } + return `"${escaped.join("")}"`; + } + + // For all other platforms the command is /bin/sh -c + // The command and its arguments are passed as a single argument to the shell. + // See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02 + // eslint-disable-next-line no-lonely-if + if (!/[|&;<>()$`\\"' \t*?\[#~=%]/.test(arg)) { + return arg; + } + return `"${arg.replace(/[$`"\\]/g, "\\$&")}"`; +} + +module.exports = { + escapeArg, +}; diff --git a/test/create/echo_args.js b/test/create/echo_args.js new file mode 100644 index 00000000..1319b1cf --- /dev/null +++ b/test/create/echo_args.js @@ -0,0 +1,4 @@ +"use strict"; +if (module === require.main) { + process.stdout.write(JSON.stringify(process.argv.slice(2))); +} diff --git a/test/create/escape_arg.js b/test/create/escape_arg.js new file mode 100644 index 00000000..808036c5 --- /dev/null +++ b/test/create/escape_arg.js @@ -0,0 +1,71 @@ +"use strict"; +const child_process = require("child_process"); +const util = require("util"); +const assert = require("assert").strict; +const path = require("path"); + +const { escapeArg } = require("../../packages/create/escape_arg"); +const execFile = util.promisify(child_process.execFile); + +async function exec(file, args) { + const { stdout, stderr } = await execFile( + file, + args.map(escapeArg), + // eslint-disable-next-line node/no-process-env + { shell: true, cwd: __dirname, env: { ...process.env, pct: "%"} } + ); + return { stdout: JSON.parse(stdout), stderr }; +} + +const strings = [ + '"a"', + "a sentence of text.", + " irregular \tspaces ", + '"C:\\path\\"', + "C:\\path\\", + '\\ "\\" \\\\ "\\\\" \\\\\\ "\\\\\\" \\\\\\\\ "\\\\\\\\" \\\\\\\\\\ "\\\\\\\\\\" \\\\\\\\\\\ "\\\\\\\\\\\\"', + '"C:\\path"', + "C:\\path", + "%OS%", + "!OS!", + "😮", +]; +for (let i=1; i < 128; i++) { + const char = String.fromCodePoint(i); + // Skip newline and carriage return because those are difficult to escape + // Skip most alphanumeric characters as they don't behave differently to each other. + if (/[\n\r1-9B-Zb-z]/.test(char)) { + continue; + } + strings.push(char, `${char}${char}`, `${char}"`); +} + +// Test 100 strings at a time. +const tests = [[]]; +for (const string of strings) { + if (tests[tests.length - 1].length >= 100) { + tests.push([]); + } + tests[tests.length - 1].push(string); +} + + +describe("create/escape_arg", function() { + it("should correctly escape strings when args are forwarded", async function() { + const ext = process.platform === "win32" ? ".cmd" : ".sh"; + for (const args of tests) { + assert.deepEqual( + await exec(`.${path.sep}forward_args${ext}`, ["echo_args.js", ...args]), + { stdout: args, stderr: "" } + ); + } + }); + it("should correctly escape strings when invoking node directly", async function() { + for (const args of tests) { + assert.deepEqual( + await exec("node", ["echo_args.js", ...args]), + { stdout: args, stderr: "" } + ); + } + }); +}); diff --git a/test/create/forward_args.cmd b/test/create/forward_args.cmd new file mode 100644 index 00000000..fc77d29c --- /dev/null +++ b/test/create/forward_args.cmd @@ -0,0 +1,3 @@ +@ECHO off +REM this is how npm and package.json bin entries forwards arguments on Windows +node.exe %* diff --git a/test/create/forward_args.sh b/test/create/forward_args.sh new file mode 100755 index 00000000..f7044d7f --- /dev/null +++ b/test/create/forward_args.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# this is how package.json bin entries forwards arguments on Linux +exec node "$@" From 0cd24d120cea49965b38fab7caca4c447cd625f6 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 30 Aug 2024 00:58:34 +0200 Subject: [PATCH 26/38] Install optional dependencies on CI The @swc/core module depends on optional dependencies being installed in order to get the correct native binding's package installed. Remove --no-optional flag passed to pnpm to install these. --- .github/workflows/node.js.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 69a7fa1f..189c0dec 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -24,7 +24,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - run: pnpm i --no-optional --no-frozen-lockfile + - run: pnpm i --no-frozen-lockfile - run: wget -q -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz - name: Run tests if: ${{ matrix.node-version != '18.x' }} @@ -49,5 +49,5 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 18.x - - run: pnpm i --no-optional --no-frozen-lockfile + - run: pnpm i --no-frozen-lockfile - run: pnpm run lint From 0b50a6a29e50e9c6c149969b59907aaefd28ddf3 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 2 Sep 2024 22:43:53 +0200 Subject: [PATCH 27/38] Add changelog entry for #652 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66319a98..7ab5d832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Many thanks to the following for contributing to this release: - Fixed IPC errors reporting undefined when no stack trace was present [#624](https://github.com/clusterio/clusterio/pull/624). - Fixed loopback routing for instances reporting the wrong error message [#625](https://github.com/clusterio/clusterio/pull/625). - Bumped Typescript version to 5.5 and implemented `${configDir}` in base configs for "outDir" [#648](https://github.com/clusterio/clusterio/pull/648). +- Fixed spaces passed in arguments to the installer causing it to break [#620](https://github.com/clusterio/clusterio/issues/620) ### Breaking Changes @@ -37,6 +38,7 @@ Many thanks to the following for contributing to this release: Many thanks to the following for contributing to this release: [@Cooldude2606](https://github.com/Cooldude2606) +[@Hornwitser](https://github.com/Hornwitser) ## Version 2.0.0-alpha.18 From 5215ee8eefe3a896c44b1ef3652425bc8cc7f040 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:09:33 +0100 Subject: [PATCH 28/38] Include mono repo plugins in build --- clean.js | 4 +++- package.json | 3 ++- pnpm-workspace.yaml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/clean.js b/clean.js index f7d0a0e5..1a94d74d 100644 --- a/clean.js +++ b/clean.js @@ -12,7 +12,9 @@ const fs = require("fs-extra"); const yargs = require("yargs"); const npmPackage = require("./package.json"); -const workspaces = npmPackage.workspaces.map(p => p.slice(0, -2)); +const workspaces = npmPackage.workspaces + .map(p => p.slice(0, -2)) + .filter(p => !p.includes("external_plugins")); let DRY = false; diff --git a/package.json b/package.json index 985bbc79..34397619 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "workspaces": [ "packages/*", "plugins/*", - "external_plugins/*" + "external_plugins/*", + "external_plugins/*/*" ], "devDependencies": { "@clusterio/controller": "workspace:*", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a74adfca..fd9fdcd3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,4 +2,5 @@ packages: - "packages/*" - "plugins/*" - "external_plugins/*" + - "external_plugins/*/*" - "test/" From 7db582196764395f35a52d0e3a811b075dc21a2f Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:06:40 +0100 Subject: [PATCH 29/38] Update change log and publish steps (#646) --- CHANGELOG.md | 14 ++++++++++++++ docs/contributing.md | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab5d832..9650040b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,22 @@ Many thanks to the following for contributing to this release: ## Version 2.0.0-alpha.19 +### Features + +- Added clean script to speed up publishing and debugging of build steps [#640](https://github.com/clusterio/clusterio/pull/640). +- Added sub-tile option on plugin page headers [#642](https://github.com/clusterio/clusterio/pull/642). +- Greatly improved plugin and module template generation [#641](https://github.com/clusterio/clusterio/pull/641). + ### Changes +- Added tests for sendEvent and sendRequest throwing the correct error messages [#638](https://github.com/clusterio/clusterio/pull/638). +- Updated all web ui pages to use correct page header component [#643](https://github.com/clusterio/clusterio/pull/643). - Fixed IPC errors reporting undefined when no stack trace was present [#624](https://github.com/clusterio/clusterio/pull/624). - Fixed loopback routing for instances reporting the wrong error message [#625](https://github.com/clusterio/clusterio/pull/625). +- Fixed docs linking to non-existing js files after ts migration [#633](https://github.com/clusterio/clusterio/pull/633). +- Fixed CI workflows using deprecated version of nodejs [#637](https://github.com/clusterio/clusterio/pull/637). +- Fixed test running on windows that should be linux only [#628](https://github.com/clusterio/clusterio/pull/628). +- Fixed log events being sent from a host over an invalid websocket [#639](https://github.com/clusterio/clusterio/pull/639). - Bumped Typescript version to 5.5 and implemented `${configDir}` in base configs for "outDir" [#648](https://github.com/clusterio/clusterio/pull/648). - Fixed spaces passed in arguments to the installer causing it to break [#620](https://github.com/clusterio/clusterio/issues/620) @@ -38,7 +50,9 @@ Many thanks to the following for contributing to this release: Many thanks to the following for contributing to this release: [@Cooldude2606](https://github.com/Cooldude2606) +[@Danielv123](https://github.com/Danielv123) [@Hornwitser](https://github.com/Hornwitser) +[@Laar](https://github.com/Laar) ## Version 2.0.0-alpha.18 diff --git a/docs/contributing.md b/docs/contributing.md index 9a4ef6a9..e7cabcf4 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -319,6 +319,6 @@ You will need commit access to the repository as well publish access to the @clu 4. Publish the packages. ```sh - rm -r ./{plugins,packages}/*/dist + pnpm clean pnpm -r --filter='./{plugins,packages}/*' publish ``` From 55fcd90ed23d148c81d9c10966286115e15d925f Mon Sep 17 00:00:00 2001 From: Arvids Godjuks Date: Tue, 3 Sep 2024 01:13:49 +0300 Subject: [PATCH 30/38] Fix bootstrap command missing type declaration for admin name Lack of typing was resulting in admin name being serialized to storage as a number and breaking websocket communications protocol due to type mismatch: expected string, got number. Fixes #536 --- CHANGELOG.md | 2 ++ packages/controller/controller.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9650040b..b46d098a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Many thanks to the following for contributing to this release: - Fixed log events being sent from a host over an invalid websocket [#639](https://github.com/clusterio/clusterio/pull/639). - Bumped Typescript version to 5.5 and implemented `${configDir}` in base configs for "outDir" [#648](https://github.com/clusterio/clusterio/pull/648). - Fixed spaces passed in arguments to the installer causing it to break [#620](https://github.com/clusterio/clusterio/issues/620) +- Fixed numeric admin name breaking login [#536](https://github.com/clusterio/clusterio/issues/536) ### Breaking Changes @@ -53,6 +54,7 @@ Many thanks to the following for contributing to this release: [@Danielv123](https://github.com/Danielv123) [@Hornwitser](https://github.com/Hornwitser) [@Laar](https://github.com/Laar) +[@psihius](https://github.com/psihius) ## Version 2.0.0-alpha.18 diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index b4161f65..4b59b18c 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -248,8 +248,12 @@ async function initialize(): Promise { .command("config", "Manage Controller config", lib.configCommand) .command("bootstrap", "Bootstrap access to cluster", yargs => { yargs - .command("create-admin ", "Create a cluster admin") - .command("generate-user-token ", "Generate authentication token for the given user") + .command("create-admin ", "Create a cluster admin", yargs => { + yargs.positional("name", { describe: "Name of the admin user", type: "string" }); + }) + .command("generate-user-token ", "Generate authentication token for the given user", yargs => { + yargs.positional("name", { describe: "Name of the user", type: "string" }); + }) .command("generate-host-token ", "Generate authentication token for the given host", yargs => { yargs.positional("id", { describe: "ID of the host", type: "number" }); }) From 47896247fea392943d88b13dc1717978c777bb42 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Tue, 3 Sep 2024 01:04:50 +0200 Subject: [PATCH 31/38] Add option to set path to server executable (#657) Allow overriding the logic selecting the path to the Factorio executable. This is mainly useful when working with development versions of the game. --- CHANGELOG.md | 1 + packages/host/src/Instance.ts | 1 + packages/host/src/server.ts | 11 ++++++++++- packages/lib/src/config/definitions.ts | 9 +++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b46d098a..4a0c6669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Many thanks to the following for contributing to this release: - Bumped Typescript version to 5.5 and implemented `${configDir}` in base configs for "outDir" [#648](https://github.com/clusterio/clusterio/pull/648). - Fixed spaces passed in arguments to the installer causing it to break [#620](https://github.com/clusterio/clusterio/issues/620) - Fixed numeric admin name breaking login [#536](https://github.com/clusterio/clusterio/issues/536) +- Added factorio.executable_path option which allows overriding the default path to the Factorio executable run. ### Breaking Changes diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index f6163611..7452ece5 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -169,6 +169,7 @@ export default class Instance extends lib.Link { let serverOptions = { logger: this.logger, version: this.config.get("factorio.version"), + executablePath: this.config.get("factorio.executable_path") ?? undefined, gamePort: this.config.get("factorio.game_port") ?? host.assignGamePort(this.id), rconPort: this.config.get("factorio.rcon_port") ?? undefined, rconPassword: this.config.get("factorio.rcon_password") ?? undefined, diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 48c401b2..aa438c8f 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -407,6 +407,8 @@ export interface FactorioServerOptions { * the latest version found in `factorioDir`. */ version?: string, + /** Path to executable to invoke when starting the server */ + executablePath?: string; /** UDP port to host game on. */ gamePort?: number, /** TCP port to use for RCON. */ @@ -450,6 +452,8 @@ export interface FactorioServerOptions { * @extends events.EventEmitter */ export class FactorioServer extends events.EventEmitter { + /** Path to executable to invoke when starting the server */ + executablePath?: string; /** UDP port used for hosting the Factorio game server on */ gamePort: number; /** TCP port used for RCON on the Factorio game server */ @@ -504,6 +508,7 @@ export class FactorioServer extends events.EventEmitter { this._logger = options.logger || lib.logger; this._targetVersion = options.version || "latest"; + this.executablePath = options.executablePath; /** UDP port used for hosting the Factorio game server on */ this.gamePort = options.gamePort || randomDynamicPort(); /** TCP port used for RCON on the Factorio game server */ @@ -1155,11 +1160,15 @@ export class FactorioServer extends events.EventEmitter { /** * Get Factorio binary path * - * Get the path to the factorio binary depending on the platform (MacOS support) + * Get the path to the factorio binary depending on the configuration + * and or the platform (MacOS support) * * @returns Path to factorio binary */ binaryPath() { + if (this.executablePath) { + return this.dataPath("..", this.executablePath); + } if (process.platform === "darwin") { return this.dataPath("..", "MacOS", "factorio"); } diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index 445a66ad..3079e5a7 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -261,6 +261,7 @@ export interface InstanceConfigFields { "instance.auto_start": boolean; "factorio.version": string; + "factorio.executable_path": string | null; "factorio.game_port": number | null; "factorio.host_assigned_game_port": number | null; "factorio.rcon_port": number | null; @@ -311,6 +312,14 @@ export class InstanceConfig extends classes.Config { type: "string", initialValue: "latest", }, + "factorio.executable_path": { + description: + "Relative path from the Factorio installation directory to the executable to run. " + + "Defaults to auto detect the path, only needed in special setups.", + restartRequired: true, + type: "string", + optional: true, + }, "factorio.game_port": { description: "UDP port to run game on, uses a port in host.factorio_port_range if null", restartRequired: true, From 8054bd66d2b65bcadbec6c7244a0e18046d59edb Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:31:32 +0100 Subject: [PATCH 32/38] Fix instance subscriptions (#655) Remove some assumptions that prevent subscription listeners from working on an instance. This means allowing the host to forward the event from the instance to the controller; correctly storing subscription listeners by address rather than by connection; and a bunch of network and event stuff to correctly clean up a listener. Remove the assumption that all subscriptions events contains the updates property. This meant fixing inconsistent types; always checking if the property existed; fixing lots of the tests that made incorrect assumptions. Minior fixes: - Fix double firing of listener callbacks on first connection. - Fix synced being set to true too early on first connection. - Fix edge case interaction between last response and last snapshot for values with updated set as 0. Also change the the output made during the start of tests to indicate something is happening when start up is slow and allow for silent testing to reduce the amount of text output to the console. --- packages/controller/src/BaseConnection.ts | 2 +- packages/controller/src/Controller.ts | 5 + packages/host/src/Instance.ts | 1 + packages/lib/src/subscriptions.ts | 156 ++++++++++++++------- test/integration/index.js | 29 +++- test/lib/subscriptions.js | 161 +++++++++++++++------- test/mock.js | 1 + 7 files changed, 250 insertions(+), 105 deletions(-) diff --git a/packages/controller/src/BaseConnection.ts b/packages/controller/src/BaseConnection.ts index 3ebac51e..75832538 100644 --- a/packages/controller/src/BaseConnection.ts +++ b/packages/controller/src/BaseConnection.ts @@ -35,7 +35,7 @@ export default class BaseConnection extends lib.Link { this.handle(lib.SubscriptionRequest, this.handleSubscriptionRequest.bind(this)); this.connector.on("close", () => { - this._controller.subscriptions.unsubscribe(this); + this._controller.subscriptions.unsubscribeLink(this); }); } diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 8ae227bc..d7f9f7b1 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -1043,6 +1043,11 @@ export default class Controller { } instanceDetailsUpdated(instances: InstanceInfo[]) { + for (const instance of instances) { + if (instance.status === "stopped") { + this.subscriptions.unsubscribeAddress(lib.Address.fromShorthand({instanceId: instance.id})); + } + } const updates = instances.map(instance => instance.toInstanceDetails()); this.subscriptions.broadcast(new lib.InstanceDetailsUpdatesEvent(updates)); } diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 7452ece5..330e73d4 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -470,6 +470,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); notifyExit() { this._loadedSave = null; this.notifyStatus("stopped"); + this.connector.emit("close"); this.config.off("fieldChanged", this._configFieldChanged); clearTimeout(this._playerCheckInterval); diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index 120cf2fe..5caccc1d 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -1,11 +1,12 @@ import { Type, Static } from "@sinclair/typebox"; -import { Link, Event, EventClass, RequestHandler, WebSocketClientConnector, WebSocketBaseConnector } from "./link"; -import { logger } from "./logging"; -import { Address, MessageRequest, IControllerUser } from "./data"; +import { Link, Event, EventClass, RequestHandler, WebSocketBaseConnector } from "./link"; +import { Address, MessageRequest, IControllerUser, plainJson, JsonBoolean } from "./data"; import isDeepStrictEqual from "./is_deep_strict_equal"; +import { RequestError, SessionLost } from "./errors"; +import { logger } from "./logging"; export type SubscriptionRequestHandler = RequestHandler | null>; -export type EventSubscriberCallback = (updates: T[], synced: boolean) => void +export type EventSubscriberCallback = (event: T | null, synced: boolean) => void /** * A subscription request sent by a subscriber, this updates what events the subscriber will be sent @@ -15,7 +16,7 @@ export type EventSubscriberCallback = (updates: T[], synced: boolean) => void export class SubscriptionRequest { declare ["constructor"]: typeof SubscriptionRequest; static type = "request" as const; - static src = ["control", "instance"] as const; + static src = ["control", "host", "instance"] as const; static dst = "controller" as const; static permission(user: IControllerUser, message: MessageRequest) { if (typeof message.data === "object" && message.data !== null) { @@ -31,6 +32,9 @@ export class SubscriptionRequest { } } + /** Indicates if updates have been sent */ + static Response = JsonBoolean; + constructor( public eventName: string, public subscribe: boolean, @@ -56,9 +60,14 @@ export class SubscriptionRequest { } } +type Subscriber = { + link: Link, + dst: Address, +} + type EventData = { subscriptionUpdate?: SubscriptionRequestHandler, - subscriptions: Set, + subscriptions: Array, }; /** @@ -85,7 +94,7 @@ export class SubscriptionController { } this._events.set(entry.name, { subscriptionUpdate: subscriptionUpdate, - subscriptions: new Set(), + subscriptions: [], }); } @@ -102,13 +111,28 @@ export class SubscriptionController { if (!eventData) { throw new Error(`Event ${entry.name} is not a registered as subscribable`); } - for (let link of eventData.subscriptions) { - if ((link.connector as WebSocketBaseConnector).closing) { - eventData.subscriptions.delete(link); + const toRemove: Subscriber[] = []; + for (const subscriber of eventData.subscriptions) { + if ((subscriber.link.connector as WebSocketBaseConnector).closing) { + toRemove.push(subscriber); } else { - link.send(event); + subscriber.link.sendTo(subscriber.dst, event); } } + eventData.subscriptions = eventData.subscriptions.filter(subscriber => !toRemove.includes(subscriber)); + } + + /** + * Unsubscribe from all events of a given address. + * Used when an instance is stopped to stop all active subscriptions. + * @param address - Address to stop sending events to. + */ + unsubscribeAddress(address: Address) { + for (let eventData of this._events.values()) { + eventData.subscriptions = eventData.subscriptions.filter( + subscriber => !subscriber.dst.addressedTo(address) + ); + } } /** @@ -116,9 +140,11 @@ export class SubscriptionController { * Used when a link is closed to stop all active subscriptions. * @param link - Link to stop sending events to. */ - unsubscribe(link: Link) { + unsubscribeLink(link: Link) { for (let eventData of this._events.values()) { - eventData.subscriptions.delete(link); + eventData.subscriptions = eventData.subscriptions.filter( + subscriber => subscriber.link !== link + ); } } @@ -138,16 +164,23 @@ export class SubscriptionController { throw new Error(`Event ${event.eventName} is not a registered as subscribable`); } if (event.subscribe === false) { - eventData.subscriptions.delete(link); + const index = eventData.subscriptions.findLastIndex(subscriber => subscriber.dst.addressedTo(src)); + if (index >= 0) { + eventData.subscriptions.splice(index, 1); + } } else { - eventData.subscriptions.add(link); + if (!eventData.subscriptions.some(subscriber => subscriber.dst.addressedTo(src))) { + eventData.subscriptions.push({ link: link, dst: src }); + } if (eventData.subscriptionUpdate) { const eventReplay = await eventData.subscriptionUpdate(event, src, dst); if (eventReplay) { - link.send(eventReplay); + link.sendTo(src, eventReplay); + return true; } } } + return false; } } @@ -157,9 +190,9 @@ export interface SubscribableValue { isDeleted: boolean, } -export interface EventSubscribable extends Event { +export type EventSubscribable = Event & Partial<{ updates: V[], -} +}> /** * Component for subscribing to and tracking updates of a remote resource @@ -167,43 +200,61 @@ export interface EventSubscribable extends Event */ export class EventSubscriber< T extends EventSubscribable, - K extends string | number = T["updates"][number]["id"], - V extends SubscribableValue = T["updates"][number], + K extends string | number = NonNullable[number]["id"], + V extends SubscribableValue = NonNullable[number], > { - _callbacks = new Array>(); + /** The time at which an event was last received, is less than 0 when there have been no events */ + lastResponseTimeMs = -1; /** Values of the subscribed resource */ values = new Map(); /** True if this subscriber is currently synced with the source */ synced = false; - _snapshot?: readonly [ReadonlyMap>, boolean]; - _snapshotLastUpdatedMs = 0; - lastResponseTimeMs = -1; + /** True if this subscriber is expecting to receive updates from the source */ + _syncing = false; + /** Repeat calls to getSnapshot will return the same readonly copy unless values has updated */ + _snapshot: readonly [ReadonlyMap>, boolean] = [new Map(), false]; + _snapshotLastUpdatedMs = -1; + /** Callbacks will be called when an event is received or the synced state changes */ + _callbacks = new Array>(); constructor( private Event: EventClass, public control: Link, ) { - control.handle(Event, this._handle.bind(this)); + control.handle(Event, this._handleEvent.bind(this)); control.connector.on("connect", () => { - this._updateSubscription(); + this.handleConnectionEvent("connect"); }); control.connector.on("close", () => { - if (this.synced) { - this.synced = false; - for (let callback of this._callbacks) { - callback([], this.synced); - } - } + this.handleConnectionEvent("close"); }); } + /** + * Handle connection events from this connector or another in the chain to the controller + * This should be called by an instance plugin during onControllerConnectionEvent + * @param event - event type that occurred + */ + handleConnectionEvent(event: "connect" | "drop" | "resume" | "close") { + if (event === "connect" || event === "resume") { + this._updateSubscription(); + } else if (this.synced) { + this.synced = false; + for (let callback of this._callbacks) { + callback(null, false); + } + } + } + /** * Handle incoming events and distribute it to the correct callbacks * @param event - event from subscribed resource * @internal */ - async _handle(event: EventSubscribable) { - for (const value of event.updates) { + async _handleEvent(event: T) { + // We support the automatic maintaining of a map for events with the updates property + const updates = event.updates ?? []; + for (const value of updates) { this.lastResponseTimeMs = Math.max(this.lastResponseTimeMs, value.updatedAtMs); // Warn about updates which changes content but don't update updatedAtMs const existing = this.values.get(value.id as K); @@ -219,8 +270,13 @@ export class EventSubscriber< this.values.set(value.id as K, value); } } - for (let callback of this._callbacks) { - callback(event.updates, this.synced); + // Event handling logic + if (this._syncing && !this.synced) { + this._snapshotLastUpdatedMs = -1; + this.synced = true; + } + for (const callback of this._callbacks) { + callback(event, this.synced); } } @@ -228,10 +284,10 @@ export class EventSubscriber< * Subscribe to receive all event notifications * @param handler - * callback invoked whenever the subscribed resource changes or the - * synced property changes, in which case the updates will be empty. + * synced property changes, in which case the event will be null. * @returns function that will unsubscribe from notifications */ - subscribe(handler: EventSubscriberCallback) { + subscribe(handler: EventSubscriberCallback) { this._callbacks.push(handler); if (this._callbacks.length === 1) { this._updateSubscription(); @@ -277,26 +333,30 @@ export class EventSubscriber< * Update the subscription with the controller based on current handler counts */ async _updateSubscription() { - if (!(this.control.connector as WebSocketClientConnector).connected) { + if (!this.control.connector.valid) { return; } const entry = Link._eventsByClass.get(this.Event)!; try { - await this.control.send(new SubscriptionRequest( + this._syncing = this._callbacks.length > 0; + const updatesSent = await this.control.sendTo("controller", new SubscriptionRequest( entry.name, - this._callbacks.length > 0, + this._syncing, this.lastResponseTimeMs )); - this.synced = this._callbacks.length > 0; - this._snapshotLastUpdatedMs = 0; - for (let callback of this._callbacks) { - callback([], this.synced); + if (!updatesSent) { + this.synced = this._syncing; + for (const callback of this._callbacks) { + callback(null, this.synced); + } } } catch (err: any) { - logger.error( - `Unexpected error updating ${entry.name} subscription:\n${err.stack}` - ); + if (!(err instanceof RequestError) || err.code !== "SessionLost") { + logger.error( + `Unexpected error updating ${entry.name} subscription:\n${err.stack}` + ); + } } } } diff --git a/test/integration/index.js b/test/integration/index.js index e9584a5c..c2081cf9 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -161,20 +161,30 @@ function getControl() { } function spawn(name, cmd, waitFor) { + // eslint-disable-next-line node/no-process-env + const silent = process.env.SILENT_TEST; return new Promise((resolve, reject) => { console.log(cmd); let parts = cmd.split(" "); let process = child_process.spawn(parts[0], parts.slice(1), { cwd: path.join("temp", "test") }); let stdout = new LineSplitter({ readableObjectMode: true }); - stdout.on("data", line => { + let stderr = new LineSplitter({ readableObjectMode: true }); + let onDataOut = line => { line = line.toString("utf8"); if (waitFor.test(line)) { + if (silent) { + stdout.off("data", onDataOut); + stderr.off("data", onDataErr); + } resolve(process); } console.log(name, line); - }); - let stderr = new LineSplitter({ readableObjectMode: true }); - stderr.on("data", line => { console.log(name, line.toString("utf8")); }); + }; + let onDataErr = line => { + console.log(name, line.toString("utf8")); + }; + stdout.on("data", onDataOut); + stderr.on("data", onDataErr); process.stdout.pipe(stdout); process.stderr.pipe(stderr); }); @@ -183,10 +193,17 @@ function spawn(name, cmd, waitFor) { before(async function() { this.timeout(40000); + // eslint-disable-next-line node/no-process-env + const silent = process.env.SILENT_TEST; + if (silent) { + console.log("SILENT_TEST is present in env, loggers after bootstrap will be muted."); + } + // Some integration tests may cause log events logger.add(new ConsoleTransport({ level: "info", format: new lib.TerminalFormat(), + filter: () => !silent, })); // If fast test is enabled then output that it is @@ -206,6 +223,7 @@ before(async function() { await fs.ensureDir(path.join("temp", "test")); + console.log("Building Mods"); await fs.ensureDir(modsDir); await lib.build({ build: true, @@ -232,6 +250,7 @@ before(async function() { } await fs.copyFile(`mods/${ssZip}`, `${modsDir}/${ssZip}`); + console.log("Setting Controller Config"); await exec("node ../../packages/controller config set controller.auth_secret TestSecretDoNotUse"); await exec("node ../../packages/controller config set controller.http_port 8880"); await exec("node ../../packages/controller config set controller.https_port 4443"); @@ -240,12 +259,14 @@ before(async function() { await exec("node ../../packages/controller config set controller.tls_certificate ../../test/file/tls/cert.pem"); await exec("node ../../packages/controller config set controller.tls_private_key ../../test/file/tls/key.pem"); + console.log("Setting Controller Plugins"); await exec("node ../../packages/ctl plugin add ../../plugins/global_chat"); await exec("node ../../packages/ctl plugin add ../../plugins/research_sync"); await exec("node ../../packages/ctl plugin add ../../plugins/statistics_exporter"); await exec("node ../../packages/ctl plugin add ../../plugins/subspace_storage"); await exec("node ../../packages/ctl plugin add ../../plugins/player_auth"); + console.log("Bootstrapping"); await exec("node ../../packages/controller bootstrap create-admin test"); await exec("node ../../packages/controller bootstrap create-ctl-config test"); await exec("node ../../packages/ctl control-config set control.tls_ca ../../test/file/tls/cert.pem"); diff --git a/test/lib/subscriptions.js b/test/lib/subscriptions.js index 4367f858..e8028fd5 100644 --- a/test/lib/subscriptions.js +++ b/test/lib/subscriptions.js @@ -101,13 +101,13 @@ describe("lib/subscriptions", function() { const connectorSetupDate = [{ id: 0, request: new lib.SubscriptionRequest(RegisteredEvent.name, true), - src: addr({ controlId: 0 }), - dst: addr("controller"), + dst: addr({ controlId: 0 }), + src: addr("controller"), }, { id: 1, request: new lib.SubscriptionRequest(RegisteredEvent.name, true), - src: addr({ controlId: 1 }), - dst: addr("controller"), + dst: addr({ controlId: 1 }), + src: addr("controller"), }]; function getLink(id) { @@ -163,7 +163,7 @@ describe("lib/subscriptions", function() { subscriptions.handle(RegisteredEvent); for (let connectorData of connectorSetupDate) { subscriptions.handleRequest( - getLink(connectorData.id), connectorData.request, connectorData.src, connectorData.dst + getLink(connectorData.id), connectorData.request, connectorData.dst, connectorData.src ); } }); @@ -190,7 +190,7 @@ describe("lib/subscriptions", function() { const connectorData = connectorSetupDate[0]; const request = new lib.SubscriptionRequest(RegisteredEvent.name, false); await subscriptions.handleRequest( - getLink(connectorData.id), request, connectorData.src, connectorData.dst, + getLink(connectorData.id), request, connectorData.dst, connectorData.src ); subscriptions.broadcast(new RegisteredEvent()); await onceConnectorSend(1); @@ -212,14 +212,22 @@ describe("lib/subscriptions", function() { subscriptions.handle(RegisteredEvent); for (let connectorData of connectorSetupDate) { subscriptions.handleRequest( - getLink(connectorData.id), connectorData.request, connectorData.src, connectorData.dst + getLink(connectorData.id), connectorData.request, connectorData.dst, connectorData.src ); } }); - it("should remove an active subscription", async function() { + it("should remove an active subscription link", async function() { const link = getLink(0); - subscriptions.unsubscribe(link); + subscriptions.unsubscribeLink(link); + subscriptions.broadcast(new RegisteredEvent()); + await onceConnectorSend(1); + assertNoEvent(0); + assertLastEvent(1, RegisteredEvent); + }); + it("should remove an active subscription address", async function() { + const link = getLink(0); + subscriptions.unsubscribeAddress(connectorSetupDate[0].dst); subscriptions.broadcast(new RegisteredEvent()); await onceConnectorSend(1); assertNoEvent(0); @@ -251,7 +259,7 @@ describe("lib/subscriptions", function() { it("should accept a subscription", async function() { const request = new lib.SubscriptionRequest(RegisteredEvent.name, true); await subscriptions.handleRequest( - getLink(0), request, connectorSetupDate[0].src, connectorSetupDate[0].dst + getLink(0), request, connectorSetupDate[0].dst, connectorSetupDate[0].src ); subscriptions.broadcast(new RegisteredEvent()); await onceConnectorSend(0); @@ -260,7 +268,7 @@ describe("lib/subscriptions", function() { it("should accept an unsubscription", async function() { const request = new lib.SubscriptionRequest(RegisteredEvent.name, true); await subscriptions.handleRequest( - getLink(0), request, connectorSetupDate[0].src, connectorSetupDate[0].dst + getLink(0), request, connectorSetupDate[0].dst, connectorSetupDate[0].src ); subscriptions.broadcast(new RegisteredEvent()); await onceConnectorSend(0); @@ -271,7 +279,7 @@ describe("lib/subscriptions", function() { const unsubRequest = new lib.SubscriptionRequest(RegisteredEvent.name, false); await subscriptions.handleRequest( - getLink(0), unsubRequest, connectorSetupDate[0].src, connectorSetupDate[0].dst + getLink(0), unsubRequest, connectorSetupDate[0].dst, connectorSetupDate[0].src ); subscriptions.broadcast(new RegisteredEvent()); assertNoEvent(0); @@ -283,7 +291,7 @@ describe("lib/subscriptions", function() { const request = new lib.SubscriptionRequest(StringPermissionEvent.name, true); await subscriptions.handleRequest( - getLink(0), request, connectorSetupDate[0].src, connectorSetupDate[0].dst + getLink(0), request, connectorSetupDate[0].dst, connectorSetupDate[0].src ); await onceConnectorSend(0); assertLastEvent(0, StringPermissionEvent); @@ -301,7 +309,7 @@ describe("lib/subscriptions", function() { const request = new lib.SubscriptionRequest(StringPermissionEvent.name, true); await subscriptions.handleRequest( - getLink(0), request, connectorSetupDate[0].src, connectorSetupDate[0].dst + getLink(0), request, connectorSetupDate[0].dst, connectorSetupDate[0].src ); await new Promise(resolve => setImmediate(resolve)); assertNoEvent(0); @@ -319,12 +327,12 @@ describe("lib/subscriptions", function() { registeredEvent = new lib.EventSubscriber(RegisteredEvent, mockControl); }); - function assertLastRequest(request) { - const Request = request.constructor; + function assertLastMessage(message) { + const Message = message.constructor; const lastMessage = mockControl.connector.sentMessages.at(-1); - const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; + const name = Message.plugin ? `${Message.plugin}:${Message.name}` : Message.name; assert.equal(lastMessage.name, name); - assert.deepEqual(lastMessage.data, request); + assert.deepEqual(lastMessage.data, message); } function assertNoMessageSent() { @@ -348,19 +356,19 @@ describe("lib/subscriptions", function() { let calledWith = null; let calledWithTwo = null; const item = { id: 1, updatedAtMs: 1, isDeleted: false }; - const event = new RegisteredEvent([item]); - registeredEvent.subscribe(function(updates, synced) { calledWith = [updates, synced]; }); - registeredEvent.subscribe(function(updates, synced) { calledWithTwo = [updates, synced]; }); - await registeredEvent._handle(event); - assert.deepEqual(calledWith, [[item], false]); - assert.deepEqual(calledWithTwo, [[item], false]); + const eventUpdate = new RegisteredEvent([item]); + registeredEvent.subscribe(function(event, synced) { calledWith = [event, synced]; }); + registeredEvent.subscribe(function(event, synced) { calledWithTwo = [event, synced]; }); + await registeredEvent._handleEvent(eventUpdate); + assert.deepEqual(calledWith, [eventUpdate, true]); + assert.deepEqual(calledWithTwo, [eventUpdate, true]); }); it("should allow unsubscribing from an event", async function() { let called = false; let event = new RegisteredEvent(); function callback() { called = true; } const unsubscribe = registeredEvent.subscribe(callback); - await registeredEvent._handle(event); + await registeredEvent._handleEvent(event); assert.deepEqual(called, true); called = false; @@ -368,7 +376,7 @@ describe("lib/subscriptions", function() { unsubscribe(); // Wait for unsubscribe to happen. await new Promise(resolve => setImmediate(resolve)); - await registeredEvent._handle(event); + await registeredEvent._handleEvent(event); assert.equal(called, false); }); it("should not throw error on incorrectly unsubscribing multiple times", async function() { @@ -390,13 +398,13 @@ describe("lib/subscriptions", function() { const update3 = [{ id: 1, updatedAtMs: 4, isDeleted: true }]; const snap0 = registeredEvent.getSnapshot(); assert.deepEqual(snap0, [new Map(), false]); - await registeredEvent._handle(new RegisteredEvent(update1)); + await registeredEvent._handleEvent(new RegisteredEvent(update1)); const snap1 = registeredEvent.getSnapshot(); assert.deepEqual(snap1, [new Map([[1, update1[0]]]), false]); - await registeredEvent._handle(new RegisteredEvent(update2)); + await registeredEvent._handleEvent(new RegisteredEvent(update2)); const snap2 = registeredEvent.getSnapshot(); assert.deepEqual(snap2, [new Map([[1, update2[0]], [2, update2[1]]]), false]); - await registeredEvent._handle(new RegisteredEvent(update3)); + await registeredEvent._handleEvent(new RegisteredEvent(update3)); const snap3 = registeredEvent.getSnapshot(); assert.deepEqual(snap3, [new Map([[2, update2[1]]]), false]); // validate previous snapshots were not modified @@ -406,59 +414,108 @@ describe("lib/subscriptions", function() { }); }); + describe("handleConnectionEvent", function() { + it("should be called on connector connect event", async function() { + registeredEvent.subscribe(() => {}); + await events.once(mockControl.connector, "send"); + mockControl.connector.sentMessages.pop(); + + registeredEvent.control.connector.emit("connect"); + await events.once(mockControl.connector, "send"); + assertLastMessage(new lib.SubscriptionRequest(RegisteredEvent.name, true, -1)); + mockControl.connector.sentMessages.pop(); + + registeredEvent.handleConnectionEvent("connect"); + await events.once(mockControl.connector, "send"); + assertLastMessage(new lib.SubscriptionRequest(RegisteredEvent.name, true, -1)); + mockControl.connector.sentMessages.pop(); + + registeredEvent.handleConnectionEvent("resume"); + await events.once(mockControl.connector, "send"); + assertLastMessage(new lib.SubscriptionRequest(RegisteredEvent.name, true, -1)); + }); + it("should set synced to false on connection close", async function() { + registeredEvent.synced = true; + registeredEvent.control.connector.emit("close"); + assert(!registeredEvent.synced, "synced was not set to false during close event"); + + registeredEvent.synced = true; + registeredEvent.handleConnectionEvent("drop"); + assert(!registeredEvent.synced, "synced was not set to false during drop"); + + registeredEvent.synced = true; + registeredEvent.handleConnectionEvent("close"); + assert(!registeredEvent.synced, "synced was not set to false during close"); + }); + it("should invoke callbacks on connector close", async function() { + let calledWith; + registeredEvent.subscribe((values, synced) => { calledWith = [values, synced]; }); + const [msg] = await events.once(mockControl.connector, "send"); + mockControl.connector.emit("message", new lib.MessageResponse(0, msg.dst, msg.src, + lib.SubscriptionRequest.Response.fromJSON(false) + )); + await new Promise(resolve => setImmediate(resolve)); + assert(registeredEvent.synced, "synced was not set to true before close"); + calledWith = undefined; + + registeredEvent.control.connector.emit("close"); + assert(!registeredEvent.synced, "synced was not set to false"); + assert(calledWith !== undefined, "callback not called"); + assert.deepEqual(calledWith, [null, false]); + }); + }); + describe("_updateSubscription()", function() { - it("should do nothing if not connected", async function() { - registeredEvent.control.connector.connected = false; + it("should do nothing if connection is not valid", async function() { + registeredEvent.control.connector.valid = false; registeredEvent.subscribe(() => true); await new Promise(resolve => setImmediate(resolve)); assertNoMessageSent(); }); it("should correctly request a subscription", async function() { - const expected = new lib.SubscriptionRequest(RegisteredEvent.name, true, -1); registeredEvent.subscribe(() => true); await events.once(mockControl.connector, "send"); - assertLastRequest(expected); + assertLastMessage(new lib.SubscriptionRequest(RegisteredEvent.name, true, -1)); }); - it("should correctly request no subscriptions", async function() { - const expected = new lib.SubscriptionRequest(RegisteredEvent.name, false, -1); + it("should correctly request an unsubscription", async function() { function callback() {} const unsubscribe = registeredEvent.subscribe(callback); await events.once(mockControl.connector, "send"); unsubscribe(); await events.once(mockControl.connector, "send"); - assertLastRequest(expected); + assertLastMessage(new lib.SubscriptionRequest(RegisteredEvent.name, false, -1)); }); it("should set synced and invoke callbacks when request is responded to", async function() { let calledWith; registeredEvent.subscribe((values, synced) => { calledWith = [values, synced]; }); const [msg] = await events.once(mockControl.connector, "send"); assert(!registeredEvent.synced, "synced was set before response"); - mockControl.connector.emit("message", new lib.MessageResponse(0, msg.dst, msg.src)); + mockControl.connector.emit("message", new lib.MessageResponse(0, msg.dst, msg.src, + lib.SubscriptionRequest.Response.fromJSON(false) + )); await new Promise(resolve => setImmediate(resolve)); assert(calledWith !== undefined, "callback not called"); - assert.deepEqual(calledWith, [[], true]); + assert.deepEqual(calledWith, [null, true]); assert(registeredEvent.synced, "synced was not set"); }); - it("should be called on connector connect event", async function() { - const expected = new lib.SubscriptionRequest(RegisteredEvent.name, true, -1); - registeredEvent._callbacks.push(() => {}); - registeredEvent.control.connector.emit("connect"); - await events.once(mockControl.connector, "send"); - assertLastRequest(expected); - }); - it("should revert synced and invoke callbacks on connector close event", async function() { + it("should set synced and invoke callbacks when request is responded to with data", async function() { let calledWith; registeredEvent.subscribe((values, synced) => { calledWith = [values, synced]; }); const [msg] = await events.once(mockControl.connector, "send"); - mockControl.connector.emit("message", new lib.MessageResponse(0, msg.dst, msg.src)); + assert(!registeredEvent.synced, "synced was set before response"); + mockControl.connector.emit("message", new lib.MessageResponse(0, msg.dst, msg.src, + lib.SubscriptionRequest.Response.fromJSON(true) + )); await new Promise(resolve => setImmediate(resolve)); - assert(registeredEvent.synced, "synced was not set before close"); - calledWith = undefined; - registeredEvent.control.connector.emit("close"); + assert(!registeredEvent.synced, "synced was set before update"); + assert(calledWith === undefined, "callback called before update"); + const eventUpdate = new RegisteredEvent(); + registeredEvent._handleEvent(eventUpdate); + + assert(registeredEvent.synced, "synced was not set"); assert(calledWith !== undefined, "callback not called"); - assert.deepEqual(calledWith, [[], false]); - assert(!registeredEvent.synced, "synced was not reset"); + assert.deepEqual(calledWith, [eventUpdate, true]); }); }); }); diff --git a/test/mock.js b/test/mock.js index 1c1fb84b..365b3000 100644 --- a/test/mock.js +++ b/test/mock.js @@ -72,6 +72,7 @@ class MockConnector extends lib.BaseConnector { constructor(src, dst) { super(src, dst); + this.valid = true; this.connected = true; this.hasSession = true; this.sentMessages = []; From b2f0000a642c55619a523ff006e13f2d566cef05 Mon Sep 17 00:00:00 2001 From: Arvids Godjuks Date: Tue, 3 Sep 2024 14:58:38 +0300 Subject: [PATCH 33/38] Update CHANGELOG.md Fixed extra new line in changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 688cc823..7acd543b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,6 @@ Many thanks to the following for contributing to this release: ### Features - - Added clean script to speed up publishing and debugging of build steps [#640](https://github.com/clusterio/clusterio/pull/640). - Added sub-tile option on plugin page headers [#642](https://github.com/clusterio/clusterio/pull/642). - Greatly improved plugin and module template generation [#641](https://github.com/clusterio/clusterio/pull/641). From 5a73a73c36b820c80817fc5b0924a58345f6a0d9 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 2 Sep 2024 23:28:40 +0200 Subject: [PATCH 34/38] Fix installer setting bad controller_url The host.controller_url config is required to end with a / for the url joining logic to work. Fix code added #619 setting this to a value not ending with a /. --- packages/create/create.js | 2 +- test/integration/create.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create/create.js b/packages/create/create.js index 037c31c8..7a2291e2 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -904,7 +904,7 @@ async function main() { let hostToken = result.stdout.split("\n").slice(-2)[0]; // Default to localhost on correct port for host in standalone mode - await execHost(["config", "set", "host.controller_url", `http://localhost:${answers.httpPort}`]); + await execHost(["config", "set", "host.controller_url", `http://localhost:${answers.httpPort}/`]); await execHost(["config", "set", "host.controller_token", hostToken]); await execHost(["config", "set", "host.public_address", answers.publicAddress]); await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]); diff --git a/test/integration/create.js b/test/integration/create.js index 164e8d48..fc6a7535 100644 --- a/test/integration/create.js +++ b/test/integration/create.js @@ -45,7 +45,7 @@ describe("Integration of create tool", function () { assert.equal(hostConfig["host.name"], "local"); assert.equal(hostConfig["host.public_address"], "localhost"); assert.equal(hostConfig["host.factorio_directory"], "test/factorio"); - assert.equal(hostConfig["host.controller_url"], "http://localhost:8099"); + assert.equal(hostConfig["host.controller_url"], "http://localhost:8099/"); }).timeout(1200000); it("should create a new controller installation", async function () { this.skip(); From a883e692e4933cd86096802121e290fed5e6873d Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 2 Sep 2024 23:32:57 +0200 Subject: [PATCH 35/38] Add changelog for #619 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acd543b..7ef76ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Many thanks to the following for contributing to this release: ### Changes +- Added prompt for the HTTP port to host the controller to the installer [#619](https://github.com/clusterio/clusterio/pull/619). - Added tests for sendEvent and sendRequest throwing the correct error messages [#638](https://github.com/clusterio/clusterio/pull/638). - Updated all web ui pages to use correct page header component [#643](https://github.com/clusterio/clusterio/pull/643). - Fixed IPC errors reporting undefined when no stack trace was present [#624](https://github.com/clusterio/clusterio/pull/624). From 4068129124a0b97f70562a3ec85bb7ec5fc4984e Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Tue, 3 Sep 2024 01:06:06 +0200 Subject: [PATCH 36/38] Remove useless div on drop zone This would render on top of modals and doesn't appear to be necessary. --- packages/web_ui/src/components/ModsPage.tsx | 103 +++++++++---------- packages/web_ui/src/components/SavesList.tsx | 11 +- 2 files changed, 50 insertions(+), 64 deletions(-) diff --git a/packages/web_ui/src/components/ModsPage.tsx b/packages/web_ui/src/components/ModsPage.tsx index 2637d1cc..5747d82d 100644 --- a/packages/web_ui/src/components/ModsPage.tsx +++ b/packages/web_ui/src/components/ModsPage.tsx @@ -236,62 +236,55 @@ export default function ModsPage() { action={`${webRoot}api/upload-mod`} showUploadList={false} > -
+
( + strcmp(a.name, b.name) || a.integerVersion - b.integerVersion + ), + }, + { + title: "Version", + key: "version", + align: "right", + render: (_, mod) => <> + {`${mod.version} `} + {`/ ${mod.factorioVersion}`} + , + }, + { + title: "Filename", + dataIndex: "filename", + responsive: ["xl"], + sorter: (a, b) => strcmp(a.filename, b.filename), + }, + { + title: "Size", + key: "size", + responsive: ["lg"], + render: (_, mod) => lib.formatBytes(mod.size), + align: "right", + sorter: (a, b) => a.size - b.size, + }, + { + title: "Action", + key: "action", + responsive: ["lg"], + render: (_, mod) => actions(mod), + }, + ]} + expandable={{ + expandedRowRender: (mod: lib.ModInfo) => , + expandedRowClassName: () => "no-expanded-padding", }} - > - -
( - strcmp(a.name, b.name) || a.integerVersion - b.integerVersion - ), - }, - { - title: "Version", - key: "version", - align: "right", - render: (_, mod) => <> - {`${mod.version} `} - {`/ ${mod.factorioVersion}`} - , - }, - { - title: "Filename", - dataIndex: "filename", - responsive: ["xl"], - sorter: (a, b) => strcmp(a.filename, b.filename), - }, - { - title: "Size", - key: "size", - responsive: ["lg"], - render: (_, mod) => lib.formatBytes(mod.size), - align: "right", - sorter: (a, b) => a.size - b.size, - }, - { - title: "Action", - key: "action", - responsive: ["lg"], - render: (_, mod) => actions(mod), - }, - ]} - expandable={{ - expandedRowRender: (mod: lib.ModInfo) => , - expandedRowClassName: () => "no-expanded-padding", - }} - dataSource={[...mods.values()]} - pagination={false} - rowKey={mod => mod.id} - /> - + dataSource={[...mods.values()]} + pagination={false} + rowKey={mod => mod.id} + /> ; diff --git a/packages/web_ui/src/components/SavesList.tsx b/packages/web_ui/src/components/SavesList.tsx index 9a962a87..4c3625ef 100644 --- a/packages/web_ui/src/components/SavesList.tsx +++ b/packages/web_ui/src/components/SavesList.tsx @@ -357,15 +357,8 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) { { account.hasPermission("core.instance.save.upload") ? -
- - {saveTable} -
+ + {saveTable}
: saveTable } From f3150a9261384b1dc9af569564b673b8d5df2a71 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Tue, 3 Sep 2024 01:09:37 +0200 Subject: [PATCH 37/38] Add tests for Address.addressedTo Make sure the confusing logic in addressedTo is working correctly by adding tests for it. --- test/lib/data/messages_core.js | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 test/lib/data/messages_core.js diff --git a/test/lib/data/messages_core.js b/test/lib/data/messages_core.js new file mode 100644 index 00000000..3ff28509 --- /dev/null +++ b/test/lib/data/messages_core.js @@ -0,0 +1,83 @@ +"use strict"; +const assert = require("assert").strict; +const lib = require("@clusterio/lib"); +const { Address } = lib; + +describe("lib/data/messages_core", function() { + describe("class Address", function() { + const controller = new Address(Address.controller, 0); + const host1 = new Address(Address.host, 1); + const host2 = new Address(Address.host, 2); + const instance1 = new Address(Address.instance, 1); + const instance2 = new Address(Address.instance, 2); + const control1 = new Address(Address.control, 1); + const control2 = new Address(Address.control, 2); + const hostBr = new Address(Address.broadcast, Address.host); + const instanceBr = new Address(Address.broadcast, Address.instance); + const controlBr = new Address(Address.broadcast, Address.control); + + it("Should correctly resolve addressedTo", function() { + assert(controller.addressedTo(controller)); + assert(!controller.addressedTo(host1)); + assert(!controller.addressedTo(instance1)); + assert(!controller.addressedTo(control1)); + assert(!controller.addressedTo(hostBr)); + assert(!controller.addressedTo(instanceBr)); + assert(!controller.addressedTo(controlBr)); + + assert(!host1.addressedTo(controller)); + assert(host1.addressedTo(host1)); + assert(!host1.addressedTo(host2)); + assert(!host1.addressedTo(instance1)); + assert(!host1.addressedTo(control1)); + assert(!host1.addressedTo(hostBr)); + assert(!host1.addressedTo(instanceBr)); + assert(!host1.addressedTo(controlBr)); + + assert(!instance1.addressedTo(controller)); + assert(!instance1.addressedTo(host1)); + assert(instance1.addressedTo(instance1)); + assert(!instance1.addressedTo(instance2)); + assert(!instance1.addressedTo(control1)); + assert(!instance1.addressedTo(hostBr)); + assert(!instance1.addressedTo(instanceBr)); + assert(!instance1.addressedTo(controlBr)); + + assert(!control1.addressedTo(controller)); + assert(!control1.addressedTo(host1)); + assert(!control1.addressedTo(instance1)); + assert(control1.addressedTo(control1)); + assert(!control1.addressedTo(control2)); + assert(!control1.addressedTo(hostBr)); + assert(!control1.addressedTo(instanceBr)); + assert(!control1.addressedTo(controlBr)); + + assert(!hostBr.addressedTo(controller)); + assert(hostBr.addressedTo(host1)); + assert(hostBr.addressedTo(host2)); + assert(!hostBr.addressedTo(instance1)); + assert(!hostBr.addressedTo(control1)); + assert(!hostBr.addressedTo(hostBr)); + assert(!hostBr.addressedTo(instanceBr)); + assert(!hostBr.addressedTo(controlBr)); + + assert(!instanceBr.addressedTo(controller)); + assert(!instanceBr.addressedTo(host1)); + assert(instanceBr.addressedTo(instance1)); + assert(instanceBr.addressedTo(instance2)); + assert(!instanceBr.addressedTo(control1)); + assert(!instanceBr.addressedTo(hostBr)); + assert(!instanceBr.addressedTo(instanceBr)); + assert(!instanceBr.addressedTo(controlBr)); + + assert(!controlBr.addressedTo(controller)); + assert(!controlBr.addressedTo(host1)); + assert(!controlBr.addressedTo(instance1)); + assert(controlBr.addressedTo(control1)); + assert(controlBr.addressedTo(control2)); + assert(!controlBr.addressedTo(hostBr)); + assert(!controlBr.addressedTo(instanceBr)); + assert(!controlBr.addressedTo(controlBr)); + }); + }); +}); From 5d97e8e8162a4e8957cf11c3b08513cf569d4e3d Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Tue, 3 Sep 2024 13:18:50 +0200 Subject: [PATCH 38/38] Source factorio dir from config-host.json in tests If a factorio dir is configured in config-host.json then use that in the tests. This removes the requirement of having a factorio installation in ./factorio or linking to one there when running the tests. --- test/integration/export.js | 4 +++- test/integration/index.js | 25 ++++++++++++++++++++++++- test/integration/server.js | 6 +++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/test/integration/export.js b/test/integration/export.js index a0d8345a..94648233 100644 --- a/test/integration/export.js +++ b/test/integration/export.js @@ -5,12 +5,14 @@ const path = require("path"); const { _exportLocale } = require("@clusterio/host/dist/node/src/export"); const { FactorioServer } = require("@clusterio/host/dist/node/src/server"); +const { factorioDir } = require("./index"); + describe("Integration of lib/factorio/export", function() { describe("exportLocale()", function() { it("returns a nested Map with base game locale information", async function() { let writePath = path.join("temp", "test", "server"); - let testServer = new FactorioServer("factorio", writePath, {}); + let testServer = new FactorioServer(factorioDir, writePath, {}); await testServer.init(); let locale = await _exportLocale(testServer, new Map(), ["base"], "en"); diff --git a/test/integration/index.js b/test/integration/index.js index c2081cf9..62e14af9 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -126,15 +126,35 @@ async function get(urlPath) { return res; } +function loadJSON(filePath) { + try { + // eslint-disable-next-line node/no-sync + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (err) { + if (err.code === "ENOENT") { + return undefined; + } + throw err; + } +} + +function getFactorioDir(hostConfig) { + const factorioDir = hostConfig?.["host.factorio_directory"] ?? "factorio"; + return path.join(factorioDir); +} + let controllerProcess; let hostProcess; let control; +const baseHostConfig = loadJSON("config-host.json"); + let url = "https://localhost:4443/"; let controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")); let instancesDir = path.join("temp", "test", "instances"); let modsDir = path.join("temp", "test", "mods"); let databaseDir = path.join("temp", "test", "database"); +let factorioDir = getFactorioDir(baseHostConfig); let pluginListPath = path.join("temp", "test", "plugin-list.json"); let controllerConfigPath = path.join("temp", "test", "config-controller.json"); let hostConfigPath = path.join("temp", "test", "config-host.json"); @@ -274,7 +294,9 @@ before(async function() { controllerProcess = await spawn("controller:", "node ../../packages/controller run", /Started controller/); await execCtl("host create-config --id 4 --name host --generate-token"); - await exec(`node ../../packages/host config set host.factorio_directory ${path.join("..", "..", "factorio")}`); + + const relativeFactorioDir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir); + await exec(`node ../../packages/host config set host.factorio_directory ${relativeFactorioDir}`); await exec("node ../../packages/host config set host.tls_ca ../../test/file/tls/cert.pem"); hostProcess = await spawn("host:", "node ../../packages/host run", /Started host/); @@ -338,6 +360,7 @@ module.exports = { instancesDir, modsDir, databaseDir, + factorioDir, controllerConfigPath, hostConfigPath, controlConfigPath, diff --git a/test/integration/server.js b/test/integration/server.js index 9073bbb8..02d088cf 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -8,13 +8,13 @@ const lib = require("@clusterio/lib"); const { FactorioServer, _getVersion } = require("@clusterio/host/dist/node/src/server"); const { logger } = lib; -const { slowTest } = require("./index"); +const { slowTest, factorioDir } = require("./index"); describe("Integration of host/src/server", function() { describe("_getVersion()", function() { it("should get a version from factorio's changelog.txt", async function() { - let version = await _getVersion(path.join("factorio", "data", "changelog.txt")); + let version = await _getVersion(path.join(factorioDir, "data", "changelog.txt")); if (!/^\d+\.\d+\.\d+$/.test(version)) { assert.fail(`Detected version '${version}' does not followed the format x.y.z`); } @@ -23,7 +23,7 @@ describe("Integration of host/src/server", function() { describe("class FactorioServer", function() { let writePath = path.join("temp", "test", "integration"); - let server = new FactorioServer("factorio", writePath, {}); + let server = new FactorioServer(factorioDir, writePath, {}); let logFile; before(async function() {