mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
Merge branch 'master' into user-editable-config
This commit is contained in:
30 files changed
+423
-118
No files matched your search
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,16 @@ async function findUnusedName(directory, name, extension = "") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Warning: this function should not be relied upon to be accurate for
|
||||
* security sensitive applications. The selection process is inherently
|
||||
* racy and a file or folder may have been created in the folder by the time
|
||||
* this function returns.
|
||||
*
|
||||
* @param {string} prefix - Prefix for file
|
||||
* @param {string} suffix - Suffix for file
|
||||
* @param {string} tmpdir - Directory for temp file
|
||||
*/
|
||||
async function getTempFile(prefix, suffix, tmpdir) {
|
||||
prefix = (typeof prefix !== "undefined") ? prefix : "tmp.";
|
||||
suffix = (typeof suffix !== "undefined") ? suffix : "";
|
||||
@@ -77,10 +86,31 @@ async function getTempFile(prefix, suffix, tmpdir) {
|
||||
let freeFile = await findUnusedName(tmpdir, fileName);
|
||||
let fullPath = path.join(tmpdir, freeFile);
|
||||
return fullPath;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
getTempFile,
|
||||
safeOutputFile,
|
||||
};
|
||||
@@ -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",
|
||||
@@ -1145,11 +1147,13 @@ messages.instanceInitialized = new Event({
|
||||
messages.instanceStatusChanged = new Event({
|
||||
type: "instance_status_changed",
|
||||
links: ["instance-slave", "slave-master"],
|
||||
eventRequired: ["instance_id", "status"],
|
||||
eventProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"status": { enum: [
|
||||
"stopped", "starting", "running", "stopping", "creating_save", "exporting_data",
|
||||
]},
|
||||
"game_port": { type: ["null", "integer"] },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -106,6 +106,7 @@ class ControlConnection extends BaseConnection {
|
||||
version: slave.version,
|
||||
id: slave.id,
|
||||
name: slave.name,
|
||||
public_address: slave.public_address || null,
|
||||
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.game_port || null,
|
||||
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.game_port || null,
|
||||
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.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -367,6 +368,7 @@ class Master {
|
||||
version: slave.version,
|
||||
id: slave.id,
|
||||
name: slave.name,
|
||||
public_address: slave.public_address || null,
|
||||
connected: this.wsServer.slaveConnections.has(slave.id),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -87,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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
+18
-6
@@ -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?.gamePort || this.config.get("factorio.game_port") || null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -376,7 +378,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 +412,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 +420,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 +430,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)
|
||||
);
|
||||
@@ -886,6 +888,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"), {
|
||||
@@ -999,6 +1006,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,
|
||||
});
|
||||
}
|
||||
@@ -1092,6 +1100,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();
|
||||
});
|
||||
@@ -1294,7 +1306,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)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { message, Button, Table } from "antd";
|
||||
import CopyOutlined from "@ant-design/icons/CopyOutlined";
|
||||
|
||||
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();
|
||||
|
||||
function slaveName(slaveId) {
|
||||
let slave = slaveList.find(s => s.id === slaveId);
|
||||
if (slave) {
|
||||
return slave.name;
|
||||
}
|
||||
return String(slaveId);
|
||||
}
|
||||
|
||||
function instancePublicAddress(instance) {
|
||||
let slave = slaveList.find(s => s.id === instance.assigned_slave);
|
||||
if (slave && slave.public_address) {
|
||||
if (instance.game_port) {
|
||||
return `${slave.public_address}:${instance.game_port}`;
|
||||
}
|
||||
return slave.public_address;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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: "Public address",
|
||||
key: "public_address",
|
||||
render: instance => {
|
||||
let public_address = instancePublicAddress(instance);
|
||||
return public_address ? <>
|
||||
{public_address}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined/>}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(public_address);
|
||||
message.success("Copied public address!");
|
||||
}}
|
||||
/>
|
||||
</> : "";
|
||||
},
|
||||
sorter: (a, b) => strcmp(slavePublicAddress(a["assigned_slave"]), slavePublicAddress(b["assigned_slave"])),
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
render: status => <InstanceStatusTag status={status} />,
|
||||
sorter: (a, b) => strcmp(a["status"], b["status"]),
|
||||
},
|
||||
];
|
||||
|
||||
if (props.hideAssignedSlave) {
|
||||
columns.splice(1, 1);
|
||||
}
|
||||
|
||||
return <Table
|
||||
size={props.size || "large"}
|
||||
columns={columns}
|
||||
dataSource={props.instances}
|
||||
rowKey={instance => instance["id"]}
|
||||
pagination={false}
|
||||
onRow={(record, rowIndex) => ({
|
||||
onClick: event => {
|
||||
history.push(`/instances/${record.id}/view`);
|
||||
},
|
||||
})}
|
||||
/>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Tag } from "antd";
|
||||
import React from "react";
|
||||
|
||||
const statusColors = {
|
||||
unknown: "#8c8c8c",
|
||||
unassigned: "#eb2f96",
|
||||
stopped: "#cf1322",
|
||||
starting: "#ad8b00",
|
||||
running: "#389e0d",
|
||||
stopping: "#d48806",
|
||||
creating_save: "#096dd9",
|
||||
exporting_data: "#08979c",
|
||||
deleted: "#262626",
|
||||
};
|
||||
|
||||
|
||||
export default function InstanceStatusTag(props) {
|
||||
return <Tag color={statusColors[props.status]}>
|
||||
{props.status.replace("_", " ")}
|
||||
</Tag>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { Button, Descriptions, Popconfirm, Space, Spin, Typography } from "antd";
|
||||
import { Alert, Button, Descriptions, Popconfirm, Space, Spin, Typography } from "antd";
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
|
||||
@@ -40,10 +41,22 @@ export default function InstanceViewPage(props) {
|
||||
return <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
|
||||
}
|
||||
|
||||
if (instance.missing) {
|
||||
if (instance.missing || instance["status"] === "deleted") {
|
||||
return <PageLayout nav={nav}>
|
||||
<h2>Instance not found</h2>
|
||||
<p>Instance with id {instanceId} was not found on the master server.</p>
|
||||
<Alert
|
||||
message={instance["status"] === "deleted" ? "Instance has been deleted" : "Instance not found" }
|
||||
showIcon
|
||||
description={<>Instance with id {instanceId} was not found on the master server.</>}
|
||||
type="warning"
|
||||
action={
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => { history.push("/instances"); }}
|
||||
>
|
||||
Go back to instances list
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
@@ -116,7 +129,7 @@ export default function InstanceViewPage(props) {
|
||||
buttonContent={assigned ? "Reassign" : "Assign"}
|
||||
/>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">{instance["status"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status"><InstanceStatusTag status={instance["status"]} /></Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{
|
||||
@@ -127,7 +140,7 @@ export default function InstanceViewPage(props) {
|
||||
account.hasAnyPermission("core.log.follow", "core.instance.send_rcon")
|
||||
&& <Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||
}
|
||||
{account.hasPermission("core.log.follow") && <InstanceConsole id={instanceId} />}
|
||||
{account.hasPermission("core.log.follow") && <LogConsole instances={[instanceId]} />}
|
||||
{
|
||||
account.hasPermission("core.instance.send_rcon")
|
||||
&& <InstanceRcon id={instanceId} disabled={instance["status"] !== "running"} />
|
||||
|
||||
@@ -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 <PageLayout nav={[{ name: "Instances" }]}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
@@ -81,34 +69,6 @@ export default function InstancesPage() {
|
||||
extra={account.hasPermission("core.instance.create") && <CreateInstanceButton />}
|
||||
/>
|
||||
|
||||
<Table
|
||||
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"]),
|
||||
},
|
||||
]}
|
||||
dataSource={instanceList}
|
||||
rowKey={instance => instance["id"]}
|
||||
pagination={false}
|
||||
onRow={(record, rowIndex) => ({
|
||||
onClick: event => {
|
||||
history.push(`/instances/${record.id}/view`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<InstanceList instances={instanceList} />
|
||||
</PageLayout>;
|
||||
}
|
||||
+12
-8
@@ -49,7 +49,7 @@ function formatLog(info, key) {
|
||||
return <span key={key}>[{level}] {info.message}<br/></span>;
|
||||
}
|
||||
|
||||
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 <>
|
||||
<Paragraph code className="instance-console">
|
||||
@@ -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 <PageLayout nav={[{ name: "Master" }]}>
|
||||
<h2>Master</h2>
|
||||
{account.hasPermission("core.log.follow") && <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||
<LogConsole master={true} />
|
||||
</>}
|
||||
{account.hasPermission("core.master.get_config") && <MasterConfigTree />}
|
||||
</PageLayout>;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Descriptions, PageHeader, Spin, Tag, 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";
|
||||
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 <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
|
||||
}
|
||||
|
||||
if (slave.missing) {
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
title={slaveId}
|
||||
/>
|
||||
<p>Slave with id {slaveId} was not found on the master server.</p>
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
title={slave.name || slaveId}
|
||||
/>
|
||||
|
||||
<Descriptions bordered size="small" column={{ xs: 1, sm: 2, xl: 4 }}>
|
||||
<Descriptions.Item label="Name">{slave["name"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Connected">
|
||||
<Tag color={slave["connected"] ? "#389e0d" : "#cf1322"}>
|
||||
{slave["connected"] ? "Connected" : "Disconnected"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Agent">{slave["agent"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Version">{slave["version"]}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{account.hasPermission("core.instance.list") && <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Instances</Title>
|
||||
<InstanceList instances={instanceList} size="small" hideAssignedSlave />
|
||||
</>}
|
||||
{account.hasPermission("core.log.follow") && <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||
<LogConsole slaves={[slaveId]} />
|
||||
</>}
|
||||
</PageLayout>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext, useRef, useState } from "react";
|
||||
import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, PageHeader, Table, Tag } 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 <PageLayout nav={[{ name: "Slaves" }]}>
|
||||
@@ -102,16 +104,30 @@ 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 => <Tag
|
||||
color={slave["connected"] ? "#389e0d" : "#cf1322"}
|
||||
>
|
||||
{slave["connected"] ? "Connected" : "Disconnected"}
|
||||
</Tag>,
|
||||
sorter: (a, b) => a["connected"] - b["connected"],
|
||||
},
|
||||
]}
|
||||
dataSource={slaveList}
|
||||
rowKey={slave => slave["id"]}
|
||||
pagination={false}
|
||||
onRow={(record, rowIndex) => ({
|
||||
onClick: event => {
|
||||
history.push(`/slaves/${record.id}/view`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</PageLayout>;
|
||||
};
|
||||
@@ -13,8 +13,10 @@ 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";
|
||||
|
||||
export { default as PageLayout } from "./components/PageLayout";
|
||||
|
||||
@@ -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: <SlavesPage />,
|
||||
},
|
||||
{
|
||||
path: "/slaves/:id/view",
|
||||
sidebarPath: "/slaves",
|
||||
content: <SlaveViewPage />,
|
||||
},
|
||||
{
|
||||
path: "/instances",
|
||||
sidebarName: "Instances",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})`);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user