From a2a5cf6d4c874105e73b3410cc820ef8f8d671db Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 31 Oct 2021 07:26:56 +0100 Subject: [PATCH 01/18] Warn on instances with duplicated ids It's possible that someone copies an instance folder and don't change the instance id in it. Emit a warning in case this happens. --- packages/slave/slave.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/slave/slave.js b/packages/slave/slave.js index ecc492ed..a4db4b7e 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -886,6 +886,11 @@ async function discoverInstances(instancesDir) { continue; } + if (instanceInfos.has(instanceConfig.get("instance.id"))) { + logger.warn(`Ignoring instance with duplicate id in folder ${entry.name}`); + continue; + } + let instancePath = path.join(instancesDir, entry.name); logger.verbose(`found instance ${instanceConfig.get("instance.name")} in ${instancePath}`); instanceInfos.set(instanceConfig.get("instance.id"), { From 7769c1f89e015807fd1a1b589ba18f344eda3dd9 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 31 Oct 2021 08:58:26 +0100 Subject: [PATCH 02/18] Add SlaveViewPage Add slave overview page displaying instances assigned and some basic properties. --- .../web_ui/src/components/InstanceList.jsx | 58 +++++++++++++++++++ .../web_ui/src/components/InstancesPage.jsx | 46 +-------------- .../web_ui/src/components/SlaveViewPage.jsx | 56 ++++++++++++++++++ packages/web_ui/src/components/SlavesPage.jsx | 7 +++ packages/web_ui/src/index.jsx | 1 + packages/web_ui/src/pages.jsx | 6 ++ 6 files changed, 131 insertions(+), 43 deletions(-) create mode 100644 packages/web_ui/src/components/InstanceList.jsx create mode 100644 packages/web_ui/src/components/SlaveViewPage.jsx diff --git a/packages/web_ui/src/components/InstanceList.jsx b/packages/web_ui/src/components/InstanceList.jsx new file mode 100644 index 00000000..4a010ffa --- /dev/null +++ b/packages/web_ui/src/components/InstanceList.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import { useHistory } from "react-router-dom"; +import { Table } from "antd"; + +import { useSlaveList } from "../model/slave"; + +const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare; + + +export default function InstanceList(props) { + let history = useHistory(); + let [slaveList] = useSlaveList(); + + function slaveName(slaveId) { + let slave = slaveList.find(s => s.id === slaveId); + if (slave) { + return slave.name; + } + return String(slaveId); + } + + let columns = [ + { + title: "Name", + dataIndex: "name", + defaultSortOrder: "ascend", + sorter: (a, b) => strcmp(a["name"], b["name"]), + }, + { + title: "Assigned Slave", + key: "assigned_slave", + render: instance => slaveName(instance["assigned_slave"]), + sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])), + }, + { + title: "Status", + dataIndex: "status", + sorter: (a, b) => strcmp(a["status"], b["status"]), + }, + ]; + + if (props.hideAssignedSlave) { + columns.splice(1, 1); + } + + return instance["id"]} + pagination={false} + onRow={(record, rowIndex) => ({ + onClick: event => { + history.push(`/instances/${record.id}/view`); + }, + })} + />; +} diff --git a/packages/web_ui/src/components/InstancesPage.jsx b/packages/web_ui/src/components/InstancesPage.jsx index 85996fe2..fb6dbd71 100644 --- a/packages/web_ui/src/components/InstancesPage.jsx +++ b/packages/web_ui/src/components/InstancesPage.jsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; +import { Button, Form, Input, Modal, PageHeader } from "antd"; import { libConfig, libLink } from "@clusterio/lib"; @@ -8,10 +8,8 @@ import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import { useInstanceList } from "../model/instance"; +import InstanceList from "./InstanceList"; import { notifyErrorHandler } from "../util/notify"; -import { useSlaveList } from "../model/slave"; - -const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare; function CreateInstanceButton(props) { @@ -61,19 +59,9 @@ function CreateInstanceButton(props) { } export default function InstancesPage() { - let history = useHistory(); let account = useAccount(); - let [slaveList] = useSlaveList(); let [instanceList] = useInstanceList(); - function slaveName(slaveId) { - let slave = slaveList.find(s => s.id === slaveId); - if (slave) { - return slave.name; - } - return String(slaveId); - } - return } /> -
strcmp(a["name"], b["name"]), - }, - { - title: "Assigned Slave", - key: "assigned_slave", - render: instance => slaveName(instance["assigned_slave"]), - sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])), - }, - { - title: "Status", - dataIndex: "status", - sorter: (a, b) => strcmp(a["status"], b["status"]), - }, - ]} - dataSource={instanceList} - rowKey={instance => instance["id"]} - pagination={false} - onRow={(record, rowIndex) => ({ - onClick: event => { - history.push(`/instances/${record.id}/view`); - }, - })} - /> + ; } diff --git a/packages/web_ui/src/components/SlaveViewPage.jsx b/packages/web_ui/src/components/SlaveViewPage.jsx new file mode 100644 index 00000000..f4076073 --- /dev/null +++ b/packages/web_ui/src/components/SlaveViewPage.jsx @@ -0,0 +1,56 @@ +import React, { useContext, useState } from "react"; +import { Descriptions, PageHeader, Spin, Typography } from "antd"; +import { useParams } from "react-router-dom"; + +import InstanceList from "./InstanceList"; +import { useAccount } from "../model/account"; +import { useInstanceList } from "../model/instance"; +import { useSlave } from "../model/slave"; +import PageLayout from "./PageLayout"; + +const { Title } = Typography; + + +export default function SlaveViewPage(props) { + let params = useParams(); + let slaveId = Number(params.id); + let account = useAccount(); + let [instanceList] = useInstanceList(); + let [slave] = useSlave(slaveId); + + instanceList = instanceList.filter(instance => instance["assigned_slave"] === slaveId); + + let nav = [{ name: "Slaves", path: "/slaves" }, { name: slave.name || slaveId }]; + if (slave.loading) { + return ; + } + + if (slave.missing) { + return + +

Slave with id {slaveId} was not found on the master server.

+
; + } + + return + + + + {slave["name"]} + {slave["connected"] ? "yes" : "no"} + {slave["agent"]} + {slave["version"]} + + + {account.hasPermission("core.instance.list") && <> + Instances + + } + ; +} diff --git a/packages/web_ui/src/components/SlavesPage.jsx b/packages/web_ui/src/components/SlavesPage.jsx index f94001a8..9333752a 100644 --- a/packages/web_ui/src/components/SlavesPage.jsx +++ b/packages/web_ui/src/components/SlavesPage.jsx @@ -1,4 +1,5 @@ import React, { useContext, useRef, useState } from "react"; +import { useHistory } from "react-router-dom"; import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; import { libLink } from "@clusterio/lib"; @@ -75,6 +76,7 @@ function GenerateSlaveTokenButton(props) { export default function SlavesPage() { let account = useAccount(); + let history = useHistory(); let [slaveList] = useSlaveList(); return @@ -112,6 +114,11 @@ export default function SlavesPage() { dataSource={slaveList} rowKey={slave => slave["id"]} pagination={false} + onRow={(record, rowIndex) => ({ + onClick: event => { + history.push(`/slaves/${record.id}/view`); + }, + })} /> ; }; diff --git a/packages/web_ui/src/index.jsx b/packages/web_ui/src/index.jsx index a692e723..febfde98 100644 --- a/packages/web_ui/src/index.jsx +++ b/packages/web_ui/src/index.jsx @@ -15,6 +15,7 @@ export { default as MasterConfigTree } from "./components/MasterConfigTree"; export { default as AssignInstanceModal } from "./components/AssignInstanceModal"; export { default as InstanceConsole } from "./components/InstanceConsole"; +export { default as InstanceList } from "./components/InstanceList"; export { default as InstanceRcon } from "./components/InstanceRcon"; export { default as PageLayout } from "./components/PageLayout"; diff --git a/packages/web_ui/src/pages.jsx b/packages/web_ui/src/pages.jsx index b643b0e5..cec0de30 100644 --- a/packages/web_ui/src/pages.jsx +++ b/packages/web_ui/src/pages.jsx @@ -2,6 +2,7 @@ import React from "react"; import MasterPage from "./components/MasterPage"; import SlavesPage from "./components/SlavesPage"; +import SlaveViewPage from "./components/SlaveViewPage"; import InstancesPage from "./components/InstancesPage"; import InstanceViewPage from "./components/InstanceViewPage"; import UsersPage from "./components/UsersPage"; @@ -24,6 +25,11 @@ export const pages = [ permission: "core.slave.list", content: , }, + { + path: "/slaves/:id/view", + sidebarPath: "/slaves", + content: , + }, { path: "/instances", sidebarName: "Instances", From 6e5e5f0aeb1b04d4a7fe7b924439293ddd11b111 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 31 Oct 2021 10:11:57 +0100 Subject: [PATCH 03/18] Show log console for master and slaves in web UI Rename InstanceConsole to LogConsole and use it to show logs for the master server and slaves as well. --- .../src/components/InstanceViewPage.jsx | 4 +- .../{InstanceConsole.jsx => LogConsole.jsx} | 20 +++--- packages/web_ui/src/components/MasterPage.jsx | 8 +++ .../web_ui/src/components/SlaveViewPage.jsx | 6 +- packages/web_ui/src/index.jsx | 3 +- packages/web_ui/src/util/websocket.jsx | 69 +++++++++++++------ 6 files changed, 78 insertions(+), 32 deletions(-) rename packages/web_ui/src/components/{InstanceConsole.jsx => LogConsole.jsx} (88%) diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index e9bf72c9..c5a27cb5 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -9,7 +9,7 @@ import { useAccount } from "../model/account"; import ControlContext from "./ControlContext"; import PageLayout from "./PageLayout"; import InstanceConfigTree from "./InstanceConfigTree"; -import InstanceConsole from "./InstanceConsole"; +import LogConsole from "./LogConsole"; import InstanceRcon from "./InstanceRcon"; import AssignInstanceModal from "./AssignInstanceModal"; import StartStopInstanceButton from "./StartStopInstanceButton"; @@ -127,7 +127,7 @@ export default function InstanceViewPage(props) { account.hasAnyPermission("core.log.follow", "core.instance.send_rcon") && Console } - {account.hasPermission("core.log.follow") && } + {account.hasPermission("core.log.follow") && } { account.hasPermission("core.instance.send_rcon") && diff --git a/packages/web_ui/src/components/InstanceConsole.jsx b/packages/web_ui/src/components/LogConsole.jsx similarity index 88% rename from packages/web_ui/src/components/InstanceConsole.jsx rename to packages/web_ui/src/components/LogConsole.jsx index aa2d50ca..aa76c7a7 100644 --- a/packages/web_ui/src/components/InstanceConsole.jsx +++ b/packages/web_ui/src/components/LogConsole.jsx @@ -49,7 +49,7 @@ function formatLog(info, key) { return [{level}] {info.message}
; } -export default function InstanceConsole(props) { +export default function LogConsole(props) { let account = useAccount(); let control = useContext(ControlContext); let anchor = useRef(); @@ -57,16 +57,20 @@ export default function InstanceConsole(props) { let [lines, setLines] = useState([]); useEffect(() => { + let logFilter = { + all: props.all || false, + master: props.master || false, + slave_ids: props.slaves || [], + instance_ids: props.instances || [], + }; + // Scroll view to the anchor so it sticks to the bottom let parent = anchor.current.parentElement; parent.scrollTop = parent.scrollHeight - parent.clientHeight; if (account.hasPermission("core.log.query")) { libLink.messages.queryLog.send(control, { - all: false, - master: false, - slave_ids: [], - instance_ids: [props.id], + ...logFilter, max_level: null, limit: 400, order: "desc", @@ -85,11 +89,11 @@ export default function InstanceConsole(props) { )); } - control.onInstanceLog(props.id, logHandler); + control.onLog(logFilter, logHandler); return () => { - control.offInstanceLog(props.id, logHandler); + control.offLog(logFilter, logHandler); }; - }, [props.id]); + }, [props.all, props.master, props.slaves, props.instances]); return <> diff --git a/packages/web_ui/src/components/MasterPage.jsx b/packages/web_ui/src/components/MasterPage.jsx index 53c83877..088d347e 100644 --- a/packages/web_ui/src/components/MasterPage.jsx +++ b/packages/web_ui/src/components/MasterPage.jsx @@ -1,15 +1,23 @@ import React, { useContext } from "react"; +import { Typography } from "antd"; +import LogConsole from "./LogConsole"; import { useAccount } from "../model/account"; import MasterConfigTree from "./MasterConfigTree"; import PageLayout from "./PageLayout"; +const { Title } = Typography; + export default function SlavesPage() { let account = useAccount(); return

Master

+ {account.hasPermission("core.log.follow") && <> + Console + + } {account.hasPermission("core.master.get_config") && }
; }; diff --git a/packages/web_ui/src/components/SlaveViewPage.jsx b/packages/web_ui/src/components/SlaveViewPage.jsx index f4076073..c5a985bd 100644 --- a/packages/web_ui/src/components/SlaveViewPage.jsx +++ b/packages/web_ui/src/components/SlaveViewPage.jsx @@ -3,6 +3,7 @@ import { Descriptions, PageHeader, Spin, Typography } from "antd"; import { useParams } from "react-router-dom"; import InstanceList from "./InstanceList"; +import LogConsole from "./LogConsole"; import { useAccount } from "../model/account"; import { useInstanceList } from "../model/instance"; import { useSlave } from "../model/slave"; @@ -47,10 +48,13 @@ export default function SlaveViewPage(props) { {slave["agent"]} {slave["version"]} - {account.hasPermission("core.instance.list") && <> Instances } + {account.hasPermission("core.log.follow") && <> + Console + + } ; } diff --git a/packages/web_ui/src/index.jsx b/packages/web_ui/src/index.jsx index febfde98..ae2b6bbc 100644 --- a/packages/web_ui/src/index.jsx +++ b/packages/web_ui/src/index.jsx @@ -13,8 +13,9 @@ export { default as BaseConfigTree } from "./components/BaseConfigTree"; export { default as InstanceConfigTree } from "./components/InstanceConfigTree"; export { default as MasterConfigTree } from "./components/MasterConfigTree"; +export { default as LogConsole } from "./components/LogConsole"; + export { default as AssignInstanceModal } from "./components/AssignInstanceModal"; -export { default as InstanceConsole } from "./components/InstanceConsole"; export { default as InstanceList } from "./components/InstanceList"; export { default as InstanceRcon } from "./components/InstanceRcon"; diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx index 18c54f1c..5473ad49 100644 --- a/packages/web_ui/src/util/websocket.jsx +++ b/packages/web_ui/src/util/websocket.jsx @@ -27,6 +27,22 @@ export class ControlConnector extends libLink.WebSocketClientConnector { } } +function logFilter({ all, master, slave_ids, instance_ids }, info) { + if (all) { + return true; + } + if (master && info.slave_id === undefined) { + return true; + } + if (info.slave_id && slave_ids.includes(info.slave_id)) { + return true; + } + if (info.instance_id && instance_ids.includes(info.instance_id)) { + return true; + } + return false; +} + /** * Handles running the control * @@ -70,7 +86,7 @@ export class Control extends libLink.Link { this.slaveUpdateHandlers = new Map(); this.instanceUpdateHandlers = new Map(); this.saveListUpdateHandlers = new Map(); - this.instanceLogHandlers = new Map(); + this.logHandlers = new Map(); this.connector.on("connect", data => { this.accountName = data.account.name; @@ -302,23 +318,20 @@ export class Control extends libLink.Link { async logMessageEventHandler(message) { let info = message.data.info; - if (info.instance_id !== undefined) { - let instanceHandlers = this.instanceLogHandlers.get(info.instance_id); - for (let handler of instanceHandlers || []) { - handler(info); + for (let [filter, handlers] of this.logHandlers) { + if (logFilter(filter, info)) { + for (let handler of handlers) { + handler(info); + } } } } - async onInstanceLog(id, handler) { - if (!Number.isInteger(id)) { - throw new Error("Invalid instance id"); - } - - let handlers = this.instanceLogHandlers.get(id); + async onLog(filter, handler) { + let handlers = this.logHandlers.get(filter); if (!handlers) { handlers = []; - this.instanceLogHandlers.set(id, handlers); + this.logHandlers.set(filter, handlers); } handlers.push(handler); @@ -328,15 +341,15 @@ export class Control extends libLink.Link { } } - async offInstanceLog(id, handler) { - let handlers = this.instanceLogHandlers.get(id); + async offLog(filter, handler) { + let handlers = this.logHandlers.get(filter); if (!handlers || !handlers.length) { - throw new Error(`No handlers for instance ${id} exist`); + throw new Error("No handlers for the given filter exists"); } let index = handlers.lastIndexOf(handler); if (index === -1) { - throw new Error(`Given handler is not registered for instance ${id}`); + throw new Error("Given handler is not registered for the filter"); } handlers.splice(index, 1); @@ -350,13 +363,29 @@ export class Control extends libLink.Link { return; } - await libLink.messages.setLogSubscriptions.send(this, { + let combinedFilter = { all: false, master: false, slave_ids: [], - instance_ids: [...this.instanceLogHandlers.keys()], - max_level: null, - }); + instance_ids: [], + }; + + for (let filter of this.logHandlers.keys()) { + if (filter.all) { combinedFilter.all = true; } + if (filter.master) { combinedFilter.master = true; } + for (let slaveId of filter.slave_ids || []) { + if (!combinedFilter.slave_ids.includes(slaveId)) { + combinedFilter.slave_ids.push(slaveId); + } + } + for (let instanceId of filter.instance_ids || []) { + if (!combinedFilter.instance_ids.includes(instanceId)) { + combinedFilter.instance_ids.push(instanceId); + } + } + } + + await libLink.messages.setLogSubscriptions.send(this, { ...combinedFilter, max_level: null }); } async debugWsMessageEventHandler(message) { From e23d07e965a25e6246f9cd10353dbef181c5c657 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 3 Nov 2021 16:14:18 +0100 Subject: [PATCH 04/18] Use safe file writing pattern (#421) Write new file contents to a temporary file and then rename over the target file to avoid data loss in the unlikely event there's an error writing the new file contents or the process is interrupted. Resolves #416. --- packages/create/create.js | 22 ++++++++++++++-------- packages/lib/database.js | 5 +++-- packages/lib/factorio/patch.js | 4 +++- packages/lib/factorio/server.js | 11 ++++++----- packages/lib/file_ops.js | 22 ++++++++++++++++++++++ packages/lib/shared_commands.js | 7 ++++--- packages/master/master.js | 7 +++++-- packages/master/src/Master.js | 7 ++++--- packages/master/src/UserManager.js | 3 ++- packages/master/src/routes.js | 4 ++-- packages/slave/slave.js | 10 +++++----- plugins/inventory_sync/master.js | 3 ++- plugins/research_sync/master.js | 3 ++- plugins/subspace_storage/master.js | 3 ++- test/lib/file_ops.js | 18 ++++++++++++++++++ 15 files changed, 94 insertions(+), 35 deletions(-) diff --git a/packages/create/create.js b/packages/create/create.js index 1b3a1bbe..3497ec9d 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -85,6 +85,12 @@ class LineSplitter extends stream.Transform { class InstallError extends Error { } +async function safeOutputFile(file, data, options={}) { + let temporary = `${file}.tmp`; + await fs.outputFile(temporary, data, options); + await fs.rename(temporary, file); +} + async function execFile(cmd, args) { logger.verbose(`executing ${cmd} ${args.join(" ")}`); const asyncExec = util.promisify(child_process.execFile); @@ -165,7 +171,7 @@ async function validateInstallDir() { async function installClusterio(mode, plugins) { try { - await fs.writeFile("package.json", JSON.stringify({ + await safeOutputFile("package.json", JSON.stringify({ name: "clusterio-install", private: true, }, null, 2), { flag: "wx" }); @@ -213,7 +219,7 @@ async function installClusterio(mode, plugins) { } } try { - await fs.writeFile("plugin-list.json", JSON.stringify([...pluginList], null, 4)); + await safeOutputFile("plugin-list.json", JSON.stringify([...pluginList], null, 4)); } catch (err) { throw new InstallError(`Error writing plugin-list.json: ${err.message}`); } @@ -223,17 +229,17 @@ async function installClusterio(mode, plugins) { async function writeScripts(mode) { if (["standalone", "master"].includes(mode)) { if (process.platform === "win32") { - await fs.writeFile( + await safeOutputFile( "run-master.cmd", "@echo off\n.\\node_modules\\.bin\\clusteriomaster.cmd run\n" ); } else { - await fs.writeFile( + await safeOutputFile( "run-master.sh", "#!/bin/sh\nexec ./node_modules/.bin/clusteriomaster run\n", { mode: 0o755 }, ); - await fs.outputFile( + await safeOutputFile( "systemd/clusteriomaster.service", `[Unit] Description=Clusterio Master @@ -254,17 +260,17 @@ WantedBy=multi-user.target if (["standalone", "slave"].includes(mode)) { if (process.platform === "win32") { - await fs.writeFile( + await safeOutputFile( "run-slave.cmd", "@echo off\n.\\node_modules\\.bin\\clusterioslave.cmd run\n" ); } else { - await fs.writeFile( + await safeOutputFile( "run-slave.sh", "#!/bin/sh\nexec ./node_modules/.bin/clusterioslave run\n", { mode: 0o755 }, ); - await fs.outputFile( + await safeOutputFile( "systemd/clusterioslave.service", `[Unit] Description=Clusterio Slave diff --git a/packages/lib/database.js b/packages/lib/database.js index 665cf58a..87d3ea5e 100644 --- a/packages/lib/database.js +++ b/packages/lib/database.js @@ -8,6 +8,7 @@ "use strict"; const fs = require("fs-extra"); +const libFileOps = require("./file_ops"); const { basicType } = require("./helpers"); @@ -78,7 +79,7 @@ async function loadJsonAsMap(filePath) { */ async function saveMapAsJson(filePath, map) { let obj = mapToObject(map); - await fs.outputFile(filePath, JSON.stringify(obj, null, 4)); + await libFileOps.safeOutputFile(filePath, JSON.stringify(obj, null, 4)); } /** @@ -138,7 +139,7 @@ async function loadJsonArrayAsMap(filePath) { * @throws {Error} if an error occured writing to the file. */ async function saveMapAsJsonArray(filePath, map) { - await fs.outputFile(filePath, JSON.stringify([...map.values()], null, 4)); + await libFileOps.safeOutputFile(filePath, JSON.stringify([...map.values()], null, 4)); } diff --git a/packages/lib/factorio/patch.js b/packages/lib/factorio/patch.js index 59df7b83..f96e931a 100644 --- a/packages/lib/factorio/patch.js +++ b/packages/lib/factorio/patch.js @@ -284,9 +284,11 @@ async function patch(savePath, modules) { root.file("clusterio.json", JSON.stringify(patchInfo, null, 4)); // Write back the save + let tempSavePath = `${savePath}.tmp`; let stream = zip.generateNodeStream({ compression: "DEFLATE" }); - let pipe = stream.pipe(fs.createWriteStream(savePath)); + let pipe = stream.pipe(fs.createWriteStream(tempSavePath)); await events.once(pipe, "finish"); + await fs.rename(tempSavePath, savePath); } module.exports = { diff --git a/packages/lib/factorio/server.js b/packages/lib/factorio/server.js index ec2d4766..d680aae6 100644 --- a/packages/lib/factorio/server.js +++ b/packages/lib/factorio/server.js @@ -6,10 +6,11 @@ const path = require("path"); const events = require("events"); const util = require("util"); const crypto = require("crypto"); - -const libIni = require("../ini"); const rconClient = require("rcon-client"); + const libErrors = require("../errors"); +const libFileOps = require("../file_ops"); +const libIni = require("../ini"); const { logger } = require("../logging"); const { escapeRegExp } = require("../helpers"); @@ -1173,17 +1174,17 @@ class FactorioServer extends events.EventEmitter { "write-data": this.writePath(), }, }); - await fs.outputFile(this.writePath("config.ini"), content); + await libFileOps.safeOutputFile(this.writePath("config.ini"), content); } async _writeMapSettings(mapGenSettings, mapSettings) { if (mapGenSettings) { - await fs.outputFile( + await libFileOps.safeOutputFile( this.writePath("map-gen-settings.json"), JSON.stringify(mapGenSettings, null, 4) ); } if (mapSettings) { - await fs.outputFile( + await libFileOps.safeOutputFile( this.writePath("map-settings.json"), JSON.stringify(mapSettings, null, 4) ); } diff --git a/packages/lib/file_ops.js b/packages/lib/file_ops.js index bc5fe91e..4460fe1b 100644 --- a/packages/lib/file_ops.js +++ b/packages/lib/file_ops.js @@ -69,7 +69,29 @@ async function findUnusedName(directory, name, extension = "") { } } +/** + * Safely write data to a file + * + * Same as fs-extra.outputFile except the data is written to a temporary + * file that's renamed over the target file. The name of the temporary file + * is the same as the target file with the suffix `.tmp` added. + * + * If the operation fails it may leave behind the temporary file. This + * should not be too much of an issue as the next time the same file is + * written the temporary will be overwritten and renamed to the target file. + * + * @param {string} file - Path to file to write. + * @param {string|Buffer} data - Content to write. + * @param {object|string} options - see fs.writeFile, `flag` must not be set. + */ +async function safeOutputFile(file, data, options={}) { + let temporary = `${file}.tmp`; + await fs.outputFile(temporary, data, options); + await fs.rename(temporary, file); +} + module.exports = { getNewestFile, findUnusedName, + safeOutputFile, }; diff --git a/packages/lib/shared_commands.js b/packages/lib/shared_commands.js index 51155323..1a2f62a3 100644 --- a/packages/lib/shared_commands.js +++ b/packages/lib/shared_commands.js @@ -7,6 +7,7 @@ const fs = require("fs-extra"); const path = require("path"); const libConfig = require("./config"); +const libFileOps = require("./file_ops"); const { logger } = require("./logging"); const libHelpers = require("./helpers"); @@ -70,7 +71,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { } pluginList.set(pluginInfo.name, pluginPath); - await fs.outputFile(pluginListPath, JSON.stringify([...pluginList], null, 4)); + await libFileOps.safeOutputFile(pluginListPath, JSON.stringify([...pluginList], null, 4)); print(`Added ${pluginInfo.name}`); } else if (command === "remove") { @@ -80,7 +81,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { return; } - await fs.outputFile(pluginListPath, JSON.stringify([...pluginList], null, 4)); + await libFileOps.safeOutputFile(pluginListPath, JSON.stringify([...pluginList], null, 4)); print(`Removed ${args.name}`); } else if (command === "list") { @@ -155,7 +156,7 @@ async function handleConfigCommand(args, instance, configPath) { try { instance.set(args.field, args.value); - await fs.outputFile(configPath, JSON.stringify(instance.serialize(), null, 4)); + await libFileOps.safeOutputFile(configPath, JSON.stringify(instance.serialize(), null, 4)); } catch (err) { if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) { logger.error(err.message); diff --git a/packages/master/master.js b/packages/master/master.js index f1a06899..89d60bf8 100755 --- a/packages/master/master.js +++ b/packages/master/master.js @@ -26,6 +26,7 @@ const jwt = require("jsonwebtoken"); // homebrew modules const libErrors = require("@clusterio/lib/errors"); +const libFileOps = require("@clusterio/lib/file_ops"); const libPluginLoader = require("@clusterio/lib/plugin_loader"); const libPrometheus = require("@clusterio/lib/prometheus"); const libConfig = require("@clusterio/lib/config"); @@ -162,7 +163,7 @@ async function handleBootstrapCommand(args, masterConfig) { console.log(content); } else { logger.info(`Writing ${args.output}`); - await fs.outputFile(args.output, content); + await libFileOps.safeOutputFile(args.output, content); } } } @@ -294,7 +295,9 @@ async function initialize() { let asyncRandomBytes = util.promisify(crypto.randomBytes); let bytes = await asyncRandomBytes(256); parameters.masterConfig.set("master.auth_secret", bytes.toString("base64")); - await fs.outputFile(parameters.masterConfigPath, JSON.stringify(parameters.masterConfig.serialize(), null, 4)); + await libFileOps.safeOutputFile( + parameters.masterConfigPath, JSON.stringify(parameters.masterConfig.serialize(), null, 4) + ); } if (command === "config") { diff --git a/packages/master/src/Master.js b/packages/master/src/Master.js index 52711027..c4f2ee6e 100644 --- a/packages/master/src/Master.js +++ b/packages/master/src/Master.js @@ -10,6 +10,7 @@ const path = require("path"); const libConfig = require("@clusterio/lib/config"); const libErrors = require("@clusterio/lib/errors"); +const libFileOps = require("@clusterio/lib/file_ops"); const libLink = require("@clusterio/lib/link"); const libPlugin = require("@clusterio/lib/plugin"); const libPluginLoader = require("@clusterio/lib/plugin_loader"); @@ -241,7 +242,7 @@ class Master { logger.info("Stopping master"); logger.info("Saving config"); - await fs.outputFile(this.configPath, JSON.stringify(this.config.serialize(), null, 4)); + await libFileOps.safeOutputFile(this.configPath, JSON.stringify(this.config.serialize(), null, 4)); if (this.devMiddleware) { await new Promise((resolve, reject) => { this.devMiddleware.close(resolve); }); @@ -297,7 +298,7 @@ class Master { } static async saveSlaves(filePath, slaves) { - await fs.outputFile(filePath, JSON.stringify([...slaves.entries()], null, 4)); + await libFileOps.safeOutputFile(filePath, JSON.stringify([...slaves.entries()], null, 4)); } static async loadInstances(filePath) { @@ -329,7 +330,7 @@ class Master { serialized.push(instance.config.serialize()); } - await fs.outputFile(filePath, JSON.stringify(serialized, null, 4)); + await libFileOps.safeOutputFile(filePath, JSON.stringify(serialized, null, 4)); } static addAppRoutes(app, pluginInfos) { diff --git a/packages/master/src/UserManager.js b/packages/master/src/UserManager.js index e0fa8288..3c00a85e 100644 --- a/packages/master/src/UserManager.js +++ b/packages/master/src/UserManager.js @@ -1,6 +1,7 @@ "use strict"; const fs = require("fs-extra"); +const libFileOps = require("@clusterio/lib/file_ops"); const libUsers = require("@clusterio/lib/users"); /** @@ -62,7 +63,7 @@ class UserManager { users: serializedUsers, roles: serializedRoles, }; - await fs.outputFile(filePath, JSON.stringify(serialized, null, 4)); + await libFileOps.safeOutputFile(filePath, JSON.stringify(serialized, null, 4)); } /** diff --git a/packages/master/src/routes.js b/packages/master/src/routes.js index 749b2b55..c0217d20 100644 --- a/packages/master/src/routes.js +++ b/packages/master/src/routes.js @@ -2,13 +2,13 @@ const Busboy = require("busboy"); const crypto = require("crypto"); const events = require("events"); -const fs = require("fs-extra"); const JSZip = require("jszip"); const jwt = require("jsonwebtoken"); const path = require("path"); const util = require("util"); const libErrors = require("@clusterio/lib/errors"); +const libFileOps = require("@clusterio/lib/file_ops"); const libHelpers = require("@clusterio/lib/helpers"); const libLink = require("@clusterio/lib/link"); const libPlugin = require("@clusterio/lib/plugin"); @@ -187,7 +187,7 @@ async function uploadExport(req, res) { } let name = path.posix.basename(filePath); - await fs.outputFile(path.join("static", "export", name), await file.async("nodebuffer")); + await libFileOps.safeOutputFile(path.join("static", "export", name), await file.async("nodebuffer")); } res.sendStatus(200); diff --git a/packages/slave/slave.js b/packages/slave/slave.js index a4db4b7e..c7b4b456 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -376,7 +376,7 @@ class Instance extends libLink.Link { */ async writeServerSettings() { let serverSettings = await this.resolveServerSettings(this.config.get("factorio.settings")); - await fs.writeFile( + await libFileOps.safeOutputFile( this.server.writePath("server-settings.json"), JSON.stringify(serverSettings, null, 4) ); @@ -410,7 +410,7 @@ class Instance extends libLink.Link { if (this.config.get("factorio.sync_adminlist")) { this.logger.verbose("Writing server-adminlist.json"); - fs.outputFile( + libFileOps.safeOutputFile( this.server.writePath("server-adminlist.json"), JSON.stringify([...this._slave.adminlist], null, 4) ); @@ -418,7 +418,7 @@ class Instance extends libLink.Link { if (this.config.get("factorio.sync_banlist")) { this.logger.verbose("Writing server-banlist.json"); - fs.outputFile( + libFileOps.safeOutputFile( this.server.writePath("server-banlist.json"), JSON.stringify([...this._slave.banlist].map( ([username, reason]) => ({ username, reason }) @@ -428,7 +428,7 @@ class Instance extends libLink.Link { if (this.config.get("factorio.sync_whitelist")) { this.logger.verbose("Writing server-whitelist.json"); - fs.outputFile( + libFileOps.safeOutputFile( this.server.writePath("server-whitelist.json"), JSON.stringify([...this._slave.whitelist], null, 4) ); @@ -1299,7 +1299,7 @@ class Slave extends libLink.Link { _warning: "Changes to this file will be overwritten by the master server's copy.", ...instanceInfo.config.serialize(), }; - await fs.outputFile( + await libFileOps.safeOutputFile( path.join(instanceInfo.path, "instance.json"), JSON.stringify(warnedOutput, null, 4) ); diff --git a/plugins/inventory_sync/master.js b/plugins/inventory_sync/master.js index 907102a1..6ef37d28 100644 --- a/plugins/inventory_sync/master.js +++ b/plugins/inventory_sync/master.js @@ -2,6 +2,7 @@ const fs = require("fs-extra"); const path = require("path"); +const libFileOps = require("@clusterio/lib/file_ops"); const libPlugin = require("@clusterio/lib/plugin"); const libErrors = require("@clusterio/lib/errors"); @@ -26,7 +27,7 @@ async function saveDatabase(masterConfig, playerDatastore, logger) { let file = path.resolve(masterConfig.get("master.database_directory"), "inventories.json"); logger.verbose(`writing ${file}`); let content = JSON.stringify(Array.from(playerDatastore)); - await fs.outputFile(file, content); + await libFileOps.safeOutputFile(file, content); } } diff --git a/plugins/research_sync/master.js b/plugins/research_sync/master.js index cec2295e..461c3edc 100644 --- a/plugins/research_sync/master.js +++ b/plugins/research_sync/master.js @@ -2,6 +2,7 @@ const fs = require("fs-extra"); const path = require("path"); +const libFileOps = require("@clusterio/lib/file_ops"); const libPlugin = require("@clusterio/lib/plugin"); const RateLimiter = require("@clusterio/lib/RateLimiter"); @@ -25,7 +26,7 @@ async function loadTechnologies(masterConfig, logger) { async function saveTechnologies(masterConfig, technologies, logger) { let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); logger.verbose(`writing ${filePath}`); - await fs.outputFile(filePath, JSON.stringify([...technologies.entries()], null, 4)); + await libFileOps.safeOutputFile(filePath, JSON.stringify([...technologies.entries()], null, 4)); } class MasterPlugin extends libPlugin.BaseMasterPlugin { diff --git a/plugins/subspace_storage/master.js b/plugins/subspace_storage/master.js index 77a1ee1b..dc8e32d7 100644 --- a/plugins/subspace_storage/master.js +++ b/plugins/subspace_storage/master.js @@ -3,6 +3,7 @@ const fs = require("fs-extra"); const path = require("path"); const libDatabase = require("@clusterio/lib/database"); +const libFileOps = require("@clusterio/lib/file_ops"); const libPlugin = require("@clusterio/lib/plugin"); const { Counter, Gauge } = require("@clusterio/lib/prometheus"); const RateLimiter = require("@clusterio/lib/RateLimiter"); @@ -49,7 +50,7 @@ async function saveDatabase(masterConfig, items, logger) { let file = path.resolve(masterConfig.get("master.database_directory"), "items.json"); logger.verbose(`writing ${file}`); let content = JSON.stringify(items.serialize()); - await fs.outputFile(file, content); + await libFileOps.safeOutputFile(file, content); } else if (items) { logger.error(`Item database too large, not saving (${items.size})`); } diff --git a/test/lib/file_ops.js b/test/lib/file_ops.js index 5b02656d..21823231 100644 --- a/test/lib/file_ops.js +++ b/test/lib/file_ops.js @@ -10,6 +10,8 @@ describe("lib/file_ops", function() { async function setupTestingEnv() { await fs.ensureDir(path.join(baseDir, "test", "folder")); await fs.ensureDir(path.join(baseDir, "test", "another folder")); + await fs.remove(path.join(baseDir, "safe")); + await fs.ensureDir(path.join(baseDir, "safe")); await fs.outputFile(path.join(baseDir, "test", "file.txt"), "contents"); await fs.outputFile(path.join(baseDir, "test", "another file.txt"), "more contents"); @@ -66,4 +68,20 @@ describe("lib/file_ops", function() { } }); }); + + describe("safeOutputFile()", function() { + it("should write new target file", async function() { + let target = path.join(baseDir, "safe", "simple.txt"); + await libFileOps.safeOutputFile(target, "a text file", "utf8"); + assert(!await fs.pathExists(`${target}.tmp`), "temporary was left behind"); + assert.equal(await fs.readFile(target, "utf8"), "a text file"); + }); + it("should overwrite existing target file", async function() { + let target = path.join(baseDir, "safe", "exists.txt"); + await fs.outputFile(target, "previous", "utf8"); + await libFileOps.safeOutputFile(target, "current", "utf8"); + assert(!await fs.pathExists(`${target}.tmp`), "temporary was left behind"); + assert.equal(await fs.readFile(target, "utf8"), "current"); + }); + }); }); From c889d8f325aa57a9ae9a7736e2649983f1a3c728 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 3 Nov 2021 16:27:21 +0100 Subject: [PATCH 05/18] Ignore SessionLost from updateInstances (#422) Don't shutdown the slave if the connection session is lost during updateInstances. Resolves #418. --- packages/slave/slave.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/slave/slave.js b/packages/slave/slave.js index c7b4b456..ddf0cd32 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -1097,6 +1097,10 @@ class Slave extends libLink.Link { } this.updateInstances().catch((err) => { + if (err instanceof libErrors.SessionLost) { + return undefined; + } + logger.fatal(`Unexpected error updating instances:\n${err.stack}`); return this.shutdown(); }); From c3a1b420eacace9b1dcff1e241c48dcaf286982b Mon Sep 17 00:00:00 2001 From: bbassie Date: Thu, 4 Nov 2021 01:36:56 +0100 Subject: [PATCH 06/18] Added InstanceStatusTag react component - Modified InstanceList & InstanceViewPage to use it --- .../web_ui/src/components/InstanceList.jsx | 2 ++ .../src/components/InstanceStatusTag.jsx | 21 +++++++++++++++++++ .../src/components/InstanceViewPage.jsx | 3 ++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 packages/web_ui/src/components/InstanceStatusTag.jsx diff --git a/packages/web_ui/src/components/InstanceList.jsx b/packages/web_ui/src/components/InstanceList.jsx index 4a010ffa..39f77156 100644 --- a/packages/web_ui/src/components/InstanceList.jsx +++ b/packages/web_ui/src/components/InstanceList.jsx @@ -3,6 +3,7 @@ import { useHistory } from "react-router-dom"; import { Table } from "antd"; import { useSlaveList } from "../model/slave"; +import InstanceStatusTag from "./InstanceStatusTag"; const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare; @@ -35,6 +36,7 @@ export default function InstanceList(props) { { title: "Status", dataIndex: "status", + render: status => , sorter: (a, b) => strcmp(a["status"], b["status"]), }, ]; diff --git a/packages/web_ui/src/components/InstanceStatusTag.jsx b/packages/web_ui/src/components/InstanceStatusTag.jsx new file mode 100644 index 00000000..d3418fcf --- /dev/null +++ b/packages/web_ui/src/components/InstanceStatusTag.jsx @@ -0,0 +1,21 @@ +import { Tag } from "antd"; +import React from "react"; + +const statusColors = { + unknown: "#d46b08", + unassigned: "#d4380d", + stopped: "#cf1322", + starting: "#7cb305", + running: "#389e0d", + stopping: "#d48806", + creating_save: "#096dd9", + exporting_data: "#08979c", + deleted: "#cf1322", +}; + + +export default function InstanceStatusTag(props) { + return + {props.status} + ; +} diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index c5a27cb5..58384ab4 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -18,6 +18,7 @@ import SavesList from "./SavesList"; import { notifyErrorHandler } from "../util/notify"; import { useInstance } from "../model/instance"; import { useSlave } from "../model/slave"; +import InstanceStatusTag from "./InstanceStatusTag"; const { Title, Paragraph } = Typography; @@ -116,7 +117,7 @@ export default function InstanceViewPage(props) { buttonContent={assigned ? "Reassign" : "Assign"} />} - {instance["status"]} + { From 83b720eade77f6737f1bb9616304e035f815ba4a Mon Sep 17 00:00:00 2001 From: bbassie Date: Thu, 4 Nov 2021 01:39:28 +0100 Subject: [PATCH 07/18] Added public_address field to lists - InstanceList - SlavesPage Added public_address field to messages - slaveProperties - instanceProperties --- packages/lib/link/messages.js | 2 ++ packages/lib/schema.js | 1 + packages/master/src/ControlConnection.js | 4 ++++ packages/master/src/Master.js | 1 + packages/master/src/SlaveConnection.js | 1 + packages/slave/slave.js | 1 + packages/web_ui/src/components/InstanceList.jsx | 17 ++++++++++++++++- packages/web_ui/src/components/SlavesPage.jsx | 13 +++++++++++-- 8 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 5a13f6dd..b09e3134 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -337,6 +337,7 @@ let slaveProperties = { "name": { type: "string" }, "id": { type: "integer" }, "connected": { type: "boolean" }, + "public_address": { type: ["null", "string"] }, }; messages.listSlaves = new Request({ @@ -372,6 +373,7 @@ let instanceProperties = { "name": { type: "string" }, "id": { type: "integer" }, "assigned_slave": { type: ["null", "integer"] }, + "game_port": { type: ["null", "integer"] }, "status": { enum: [ "unknown", "unassigned", "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", "deleted", diff --git a/packages/lib/schema.js b/packages/lib/schema.js index da43a843..17157876 100644 --- a/packages/lib/schema.js +++ b/packages/lib/schema.js @@ -178,6 +178,7 @@ const clientHandshake = ajv.compile({ "version": { type: "string" }, "name": { type: "string" }, "id": { type: "integer" }, + "public_address": { type: "string" }, "plugins": { type: "object", additionalProperties: { type: "string" }, diff --git a/packages/master/src/ControlConnection.js b/packages/master/src/ControlConnection.js index 69e1f5e9..dfc07981 100644 --- a/packages/master/src/ControlConnection.js +++ b/packages/master/src/ControlConnection.js @@ -106,6 +106,7 @@ class ControlConnection extends BaseConnection { version: slave.version, id: slave.id, name: slave.name, + public_address: slave.public_address, connected: this._master.wsServer.slaveConnections.has(slave.id), }); } @@ -169,6 +170,7 @@ class ControlConnection extends BaseConnection { id, name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), + game_port: instance.config.get("factorio.game_port"), status: instance.status, }; } @@ -180,6 +182,7 @@ class ControlConnection extends BaseConnection { id: instance.config.get("instance.id"), name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), + game_port: instance.config.get("factorio.game_port"), status: instance.status, }); } @@ -199,6 +202,7 @@ class ControlConnection extends BaseConnection { id: instance.config.get("instance.id"), name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), + game_port: instance.config.get("factorio.game_port"), status: instance.status, }); } diff --git a/packages/master/src/Master.js b/packages/master/src/Master.js index c4f2ee6e..72b525a7 100644 --- a/packages/master/src/Master.js +++ b/packages/master/src/Master.js @@ -368,6 +368,7 @@ class Master { version: slave.version, id: slave.id, name: slave.name, + public_address: slave.public_address, connected: this.wsServer.slaveConnections.has(slave.id), }; diff --git a/packages/master/src/SlaveConnection.js b/packages/master/src/SlaveConnection.js index 6c2319dd..dde922d1 100644 --- a/packages/master/src/SlaveConnection.js +++ b/packages/master/src/SlaveConnection.js @@ -28,6 +28,7 @@ class SlaveConnection extends BaseConnection { id: this._id, name: this._name, version: this._version, + public_address: registerData.public_address, plugins: registerData.plugins, }); diff --git a/packages/slave/slave.js b/packages/slave/slave.js index ddf0cd32..222c6687 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -1004,6 +1004,7 @@ class SlaveConnector extends libLink.WebSocketClientConnector { version, id: this.slaveConfig.get("slave.id"), name: this.slaveConfig.get("slave.name"), + public_address: this.slaveConfig.get("slave.public_address"), plugins, }); } diff --git a/packages/web_ui/src/components/InstanceList.jsx b/packages/web_ui/src/components/InstanceList.jsx index 39f77156..c5eb002a 100644 --- a/packages/web_ui/src/components/InstanceList.jsx +++ b/packages/web_ui/src/components/InstanceList.jsx @@ -19,7 +19,13 @@ export default function InstanceList(props) { } return String(slaveId); } - + function slavePublicAddress(slaveId) { + let slave = slaveList.find(s => s.id === slaveId); + if (slave) { + return slave.public_address; + } + return null; + } let columns = [ { title: "Name", @@ -33,6 +39,15 @@ export default function InstanceList(props) { render: instance => slaveName(instance["assigned_slave"]), sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])), }, + { + title: "Public address", + key: "public_address", + render: instance => { + let address = slavePublicAddress(instance["assigned_slave"]); + return address ? `${address}:${instance["game_port"] || "unknown"}` : ""; + }, + sorter: (a, b) => strcmp(slavePublicAddress(a["assigned_slave"]), slavePublicAddress(b["assigned_slave"])), + }, { title: "Status", dataIndex: "status", diff --git a/packages/web_ui/src/components/SlavesPage.jsx b/packages/web_ui/src/components/SlavesPage.jsx index 9333752a..24cb17ed 100644 --- a/packages/web_ui/src/components/SlavesPage.jsx +++ b/packages/web_ui/src/components/SlavesPage.jsx @@ -1,6 +1,6 @@ import React, { useContext, useRef, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Button, Form, Input, Modal, PageHeader, Table } from "antd"; +import { Button, Form, Input, Modal, PageHeader, Table, Tag } from "antd"; import { libLink } from "@clusterio/lib"; @@ -104,10 +104,19 @@ export default function SlavesPage() { dataIndex: "version", sorter: (a, b) => strcmp(a["version"], b["version"]), }, + { + title: "Public address", + dataIndex: "public_address", + sorter: (a, b) => strcmp(a["public_address"], b["public_address"]), + }, { title: "Connected", key: "connected", - render: slave => slave["connected"] && "Yes", + render: slave => + {slave["connected"] ? "Connected" : "Disconnected"} + , sorter: (a, b) => a["connected"] - b["connected"], }, ]} From f930ed36a0f611fe291301781e467bc447a92cce Mon Sep 17 00:00:00 2001 From: bbassie Date: Thu, 4 Nov 2021 21:29:53 +0100 Subject: [PATCH 08/18] Changed to using the real factorio port --- packages/lib/link/messages.js | 1 + packages/master/src/ControlConnection.js | 6 +++--- packages/master/src/SlaveConnection.js | 1 + packages/slave/slave.js | 5 ++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index b09e3134..5aa971e8 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -1152,6 +1152,7 @@ messages.instanceStatusChanged = new Event({ "status": { enum: [ "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", ]}, + "game_port": { type: ["null", "integer"] }, }, }); diff --git a/packages/master/src/ControlConnection.js b/packages/master/src/ControlConnection.js index dfc07981..d00a1e2d 100644 --- a/packages/master/src/ControlConnection.js +++ b/packages/master/src/ControlConnection.js @@ -170,7 +170,7 @@ class ControlConnection extends BaseConnection { id, name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), - game_port: instance.config.get("factorio.game_port"), + game_port: instance.game_port, status: instance.status, }; } @@ -182,7 +182,7 @@ class ControlConnection extends BaseConnection { id: instance.config.get("instance.id"), name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), - game_port: instance.config.get("factorio.game_port"), + game_port: instance.game_port, status: instance.status, }); } @@ -202,7 +202,7 @@ class ControlConnection extends BaseConnection { id: instance.config.get("instance.id"), name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), - game_port: instance.config.get("factorio.game_port"), + game_port: instance.game_port, status: instance.status, }); } diff --git a/packages/master/src/SlaveConnection.js b/packages/master/src/SlaveConnection.js index dde922d1..a8e1e456 100644 --- a/packages/master/src/SlaveConnection.js +++ b/packages/master/src/SlaveConnection.js @@ -88,6 +88,7 @@ class SlaveConnection extends BaseConnection { let prev = instance.status; instance.status = message.data.status; + instance.game_port = message.data.game_port; logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`); this._master.instanceUpdated(instance); await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev); diff --git a/packages/slave/slave.js b/packages/slave/slave.js index 222c6687..99c9988c 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -260,7 +260,9 @@ class Instance extends libLink.Link { notifyStatus(status) { this._status = status; libLink.messages.instanceStatusChanged.send(this, { - instance_id: this.id, status, + instance_id: this.id, + status, + game_port: this.server ? this.server.gamePort : this.config.get("factorio.game_port") || null, }); } @@ -1297,6 +1299,7 @@ class Slave extends libLink.Link { libLink.messages.instanceStatusChanged.send(this, { instance_id, status: instanceConnection ? instanceConnection.status : "stopped", + game_port: null, }); // save a copy of the instance config From 2e5617c4b52a7938ad2bdc0060671749042f8885 Mon Sep 17 00:00:00 2001 From: bbassie Date: Thu, 4 Nov 2021 22:25:36 +0100 Subject: [PATCH 09/18] Fix instance port and status update Master was not recieving instance status after restart. Instead only a status from the slave but the slave doesn't store the full instance status. --- packages/master/src/ControlConnection.js | 6 +++--- packages/slave/slave.js | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/master/src/ControlConnection.js b/packages/master/src/ControlConnection.js index d00a1e2d..5ac4990f 100644 --- a/packages/master/src/ControlConnection.js +++ b/packages/master/src/ControlConnection.js @@ -170,7 +170,7 @@ class ControlConnection extends BaseConnection { id, name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), - game_port: instance.game_port, + game_port: instance.game_port || null, status: instance.status, }; } @@ -182,7 +182,7 @@ class ControlConnection extends BaseConnection { id: instance.config.get("instance.id"), name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), - game_port: instance.game_port, + game_port: instance.game_port || null, status: instance.status, }); } @@ -202,7 +202,7 @@ class ControlConnection extends BaseConnection { id: instance.config.get("instance.id"), name: instance.config.get("instance.name"), assigned_slave: instance.config.get("instance.assigned_slave"), - game_port: instance.game_port, + game_port: instance.game_port || null, status: instance.status, }); } diff --git a/packages/slave/slave.js b/packages/slave/slave.js index 99c9988c..b6f49f40 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -694,6 +694,8 @@ class Instance extends libLink.Link { } async masterConnectionEventEventHandler(message) { + this.notifyStatus(this._status); + await libPlugin.invokeHook(this.plugins, "onMasterConnectionEvent", message.data.event); } From 69d4a883ad63dbf8cd955dc5e331598560cd703f Mon Sep 17 00:00:00 2001 From: bbassie Date: Thu, 4 Nov 2021 22:25:53 +0100 Subject: [PATCH 10/18] Add copy public address button --- .../web_ui/src/components/InstanceList.jsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/web_ui/src/components/InstanceList.jsx b/packages/web_ui/src/components/InstanceList.jsx index c5eb002a..2a3e33fd 100644 --- a/packages/web_ui/src/components/InstanceList.jsx +++ b/packages/web_ui/src/components/InstanceList.jsx @@ -1,13 +1,13 @@ import React from "react"; import { useHistory } from "react-router-dom"; -import { Table } from "antd"; +import { notification, Button, Table } from "antd"; +import { CopyOutlined } from "@ant-design/icons"; import { useSlaveList } from "../model/slave"; import InstanceStatusTag from "./InstanceStatusTag"; const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare; - export default function InstanceList(props) { let history = useHistory(); let [slaveList] = useSlaveList(); @@ -44,7 +44,22 @@ export default function InstanceList(props) { key: "public_address", render: instance => { let address = slavePublicAddress(instance["assigned_slave"]); - return address ? `${address}:${instance["game_port"] || "unknown"}` : ""; + let full_address = address + (instance["game_port"] !== null ? `:${instance["game_port"]}` : ""); + return <> + {full_address} + + } + /> ; } From 15381ac260e5d2425bb02b9bce315bd98fbcfd58 Mon Sep 17 00:00:00 2001 From: bbassie Date: Tue, 16 Nov 2021 23:50:13 +0100 Subject: [PATCH 18/18] Final running status color update --- packages/web_ui/src/components/InstanceStatusTag.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web_ui/src/components/InstanceStatusTag.jsx b/packages/web_ui/src/components/InstanceStatusTag.jsx index e7ee06ad..cd37ca1b 100644 --- a/packages/web_ui/src/components/InstanceStatusTag.jsx +++ b/packages/web_ui/src/components/InstanceStatusTag.jsx @@ -6,7 +6,7 @@ const statusColors = { unassigned: "#eb2f96", stopped: "#cf1322", starting: "#ad8b00", - running: "#52c41a", + running: "#389e0d", stopping: "#d48806", creating_save: "#096dd9", exporting_data: "#08979c",