Merge pull request #453 from Hornwitser/small-things-7

Small things 7
This commit is contained in:
Hornwitser authored and GitHub committed 2022-03-26 21:22:35 +01:00
commit 5e94198216
25 files changed
+363 -108

No files matched your search

+1 -1
View File
@@ -125,7 +125,7 @@ Otherwise see below for OS specific instructions.
**Ubuntu with Docker**
The Docker support for Clusterio is curently broken.
The Docker support for Clusterio is currently broken.
If you need it open an issue about it.
<!--
+9
View File
@@ -42,6 +42,15 @@ List instances.
Create a new instance. `name` is the instance name. `--id` is the optional id.
### Kill instance
ctl> instance kill <name>
Terminates the running Factorio server without giving it a chance to save or cleanup after itself.
Note: This may cause loss of data.
### Config management
#### List Configuration
+13
View File
@@ -755,6 +755,19 @@ instanceCommands.add(new libCommand.Command({
},
}));
instanceCommands.add(new libCommand.Command({
definition: ["kill <instance>", "Kill instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to kill", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await libCommand.resolveInstance(control, args.instance);
await control.setLogSubscriptions({ instance_ids: [instanceId] });
await libLink.messages.killInstance.send(control, {
instance_id: instanceId,
});
},
}));
instanceCommands.add(new libCommand.Command({
definition: ["delete <instance>", "Delete instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to delete", type: "string" });
+29 -17
View File
@@ -409,6 +409,7 @@ const outputHeuristics = [
* - game-ready - invoked when the server is finished starting up
* - autosave-start - invoked when the server starts an autosave
* - autosave-fnished - invoked when the autosave finished
* - save-finished - invoked when the server has finished a manual save
* - exit - invoked when the sterver has exited
* @extends events.EventEmitter
* @memberof module:lib/factorio
@@ -487,6 +488,7 @@ class FactorioServer extends events.EventEmitter {
// Array of possible causes for an unexpected shutdown
this._unexpected = [];
this._killed = false;
// Track autosaving
this._runningAutosave = null;
@@ -499,6 +501,8 @@ class FactorioServer extends events.EventEmitter {
if (this._runningAutosave) {
this.emit("autosave-finished", this._runningAutosave);
this._runningAutosave = null;
} else {
this.emit("save-finished");
}
});
}
@@ -694,28 +698,30 @@ class FactorioServer extends events.EventEmitter {
_watchExit() {
this._server.on("exit", (code, signal) => {
if (this._state !== "stopping") {
if (this._unexpected.length === 0) {
if (signal === "SIGKILL") {
if (signal === "SIGKILL") {
if (this._killed) {
this.emit("error", new libErrors.EnvironmentError("Factorio server was killed"));
} else {
this.emit("error", new libErrors.EnvironmentError(
"Factorio server was unexpectedly killed, is the system low on memory?"
));
} else {
let msg;
if (code !== null) {
msg = `Factorio server unexpectedly shut down with code ${code}`;
} else {
msg = `Factorio server was unexpectedly shut by down by signal ${signal}`;
}
this.emit("error", new Error(msg));
}
} else if (this._unexpected.length === 0) {
let msg;
if (code !== null) {
msg = `Factorio server unexpectedly shut down with code ${code}`;
} else {
msg = `Factorio server was unexpectedly shut by down by signal ${signal}`;
}
this.emit("error", new libErrors.EnvironmentError(msg));
} else if (this._unexpected.length === 1) {
this.emit("error", new Error(this._unexpected[0]));
this.emit("error", new libErrors.EnvironmentError(this._unexpected[0]));
} else {
this.emit("error", new Error(
this.emit("error", new libErrors.EnvironmentError(
"Factorio server unexpectedly shut down. Possible causes:"+
`\n- ${this._unexpected.join("\n- ")}`
));
@@ -1004,12 +1010,18 @@ class FactorioServer extends events.EventEmitter {
* Kill the server
*
* Terminates the server without any cleanup or saving.
* @param {boolean} [unexpected=false] -
* If true raise an error event as a result of killing the Factorio
* process.
*/
async kill() {
async kill(unexpected = false) {
this._check(["running", "stopping"]);
this._state = "stopping";
this._killed = true;
if (!unexpected) {
this._state = "stopping";
}
this._server.kill("SIGKILL");
await events.once(this._server, "exit");
await new Promise(resolve => this._server.once("exit", resolve));
}
/**
+7
View File
@@ -636,6 +636,13 @@ messages.stopInstance = new Request({
forwardTo: "instance",
});
messages.killInstance = new Request({
type: "kill_instance",
links: ["control-master", "master-slave", "slave-instance"],
permission: "core.instance.kill",
forwardTo: "instance",
});
messages.deleteInstance = new Request({
type: "delete_instance",
links: ["control-master", "master-slave"],
+55 -3
View File
@@ -78,7 +78,7 @@ class BaseInstancePlugin {
* changed.
*
* @param {module:lib/config.ConfigGroup} group -
* The group who's field got changed on.
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
*/
@@ -355,6 +355,19 @@ class BaseMasterPlugin {
*/
async onInstanceStatusChanged(instance, prev) { }
/**
* Called when the value of a master config field changed.
*
* Invoked after the value of the config field given by `field` has
* changed on the master.
*
* @param {module:lib/config.ConfigGroup} group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
*/
async onMasterConfigFieldChanged(group, field, prev) { }
/**
* Called when the value of an instance config field changed.
*
@@ -363,7 +376,7 @@ class BaseMasterPlugin {
*
* @param {Object} instance - The instance the config changed on.
* @param {module:lib/config.ConfigGroup} group -
* The group who's field got changed on.
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
*/
@@ -575,11 +588,14 @@ class BaseControlPlugin {
/**
* Plugin supplied pages
* @typedef {Object} module:Lib/plugin~Page
* @typedef {Object} module:lib/plugin~Page
* @property {string} path - URL path to this page.
* @property {string=} sidebarPath -
* If present and this path matches one of the pages in the sidebar it
* will cause that sidebar entry to be highlighted as active.
* @property {string=} sidebarGroup -
* If present group this entry under a group of the given name in the
* sidebar.
* @property {string=} sidebarName -
* If present creates an entry in the sidebar for this page with the
* given text.
@@ -640,6 +656,42 @@ class BaseWebPlugin {
* @type {Array<module:lib/plugin~Page>}
*/
this.pages = [];
/**
* Extra react component to add to core components
*
* Interface to augment core components of the web UI. Setting a
* component as one of the supported properties of this object will
* cause the web UI to render it when displaying that component,
* usually at the end. Each component will receive a `plugin` param
* which is the instance of the web plugin that contained the
* component extra.
*
* @type {object}
* @property {React.ComponentType} MasterPage -
* Placed at the end of the master page.
* @property {React.ComponentType} SlavesPage -
* Placed at the end of the slaves list page.
* @property {React.ComponentType} SlaveViewPage -
* Placed at the end of each slave page. Takes a `slave` param
* which is the slave the page is displayed for.
* @property {React.ComponentType} InstancesPage -
* Placed at the end of the instance list page.
* @property {React.ComponentType} InstanceViewPage -
* Placed at the end of each instance page. Takes an `instance`
* param which is the instance the page is displayed for.
* @property {React.ComponentType} UsersPage -
* Placed at the end of the users list page.
* @property {React.ComponentType} UserViewPage -
* Placed at the end of each user page. Takes a `user` param
* which is the user object the page is displayed for.
* @property {React.ComponentType} RolesPage -
* Placed at the end of the roles list page.
* @property {React.ComponentType} RoleViewPage -
* Placed at the end of each role page. Takes a `role` param
* which is the role object the page is displayed for.
*/
this.componentExtra = {};
}
/**
+5
View File
@@ -427,6 +427,11 @@ definePermission({
title: "Stop instance",
description: "Stop instances.",
});
definePermission({
name: "core.instance.kill",
title: "Kill instance",
description: "Terminate running instances without saving or cleanup.",
});
definePermission({
name: "core.instance.delete",
title: "Delete instance",
+3
View File
@@ -162,6 +162,9 @@ class Master {
this.userManager = new UserManager(this.config);
await this.userManager.load(path.join(databaseDirectory, "users.json"));
this.config.on("fieldChanged", (group, field, prev) => {
libPlugin.invokeHook(this.plugins, "onMasterConfigFieldChanged", group, field, prev);
});
for (let instance of this.instances.values()) {
this.addInstanceHooks(instance);
}
@@ -49,7 +49,7 @@ local has_create_grid = version_ge("1.1.7")
-- If the equipment is a burner the following is also present:
-- i: burner inventory
-- r: result inventory
-- b: curently burning (optional)
-- b: currently burning (optional)
-- f: remaining_burning_fuel (optional)
function serialize.serialize_equipment_grid(grid)
local serialized = {}
+22 -1
View File
@@ -186,7 +186,11 @@ class Instance extends libLink.Link {
});
this.server.on("error", err => {
this.logger.error(`Error in instance ${this.name}:\n${err.stack}`);
if (err instanceof libErrors.EnvironmentError) {
this.logger.error(err.message);
} else {
this.logger.error(`${this.name}:\n${err.stack}`);
}
});
this.server.on("autosave-finished", name => {
@@ -195,6 +199,12 @@ class Instance extends libLink.Link {
});
});
this.server.on("save-finished", () => {
this.sendSaveListUpdate().catch(err => {
this.logger.error(`Error handling save-finished in instance ${this.name}:\n${err.stack}`);
});
});
this.server.on("ipc-player_event", event => {
libLink.messages.playerEvent.send(this, {
instance_id: this.id,
@@ -697,6 +707,13 @@ class Instance extends libLink.Link {
}
}
async kill() {
if (this._status === "stopped") {
return;
}
await this.server.kill(true);
}
async masterConnectionEventEventHandler(message) {
await libPlugin.invokeHook(this.plugins, "onMasterConnectionEvent", message.data.event);
}
@@ -833,6 +850,10 @@ class Instance extends libLink.Link {
await this.stop();
}
async killInstanceRequestHandler() {
await this.kill();
}
async sendRconRequestHandler(message) {
let result = await this.sendRcon(message.data.command);
return { result };
@@ -1,10 +1,11 @@
import React from "react";
import { useHistory } from "react-router-dom";
import { message, Button, Table } from "antd";
import { message, Button, Space, Table } from "antd";
import CopyOutlined from "@ant-design/icons/CopyOutlined";
import { useSlaveList } from "../model/slave";
import InstanceStatusTag from "./InstanceStatusTag";
import StartStopInstanceButton from "./StartStopInstanceButton";
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
@@ -43,6 +44,7 @@ export default function InstanceList(props) {
key: "assigned_slave",
render: instance => slaveName(instance["assigned_slave"]),
sorter: (a, b) => strcmp(slaveName(a["assigned_slave"]), slaveName(b["assigned_slave"])),
responsive: ["sm"],
},
{
title: "Public address",
@@ -63,13 +65,24 @@ export default function InstanceList(props) {
</> : "";
},
sorter: (a, b) => strcmp(slavePublicAddress(a["assigned_slave"]), slavePublicAddress(b["assigned_slave"])),
responsive: ["lg"],
},
{
title: "Status",
dataIndex: "status",
render: status => <InstanceStatusTag status={status} />,
key: "status",
render: instance => <InstanceStatusTag status={instance["status"]} />,
sorter: (a, b) => strcmp(a["status"], b["status"]),
},
{
key: "action",
render: instance => <StartStopInstanceButton
buttonProps={{ size: "small" }}
instance={instance}
/>,
responsive: ["sm"],
align: "right",
width: 100,
},
];
if (props.hideAssignedSlave) {
@@ -1,13 +1,15 @@
import React, { useContext, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { Alert, Button, Descriptions, Popconfirm, Space, Spin, Typography } from "antd";
import { Alert, Button, Descriptions, Dropdown, Menu, Modal, PageHeader, Space, Spin, Typography } from "antd";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import DownOutlined from "@ant-design/icons/DownOutlined";
import { libLink } from "@clusterio/lib";
import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import InstanceConfigTree from "./InstanceConfigTree";
import LogConsole from "./LogConsole";
import InstanceRcon from "./InstanceRcon";
@@ -23,6 +25,36 @@ import InstanceStatusTag from "./InstanceStatusTag";
const { Title } = Typography;
function InstanceDescription(props) {
let account = useAccount();
const { slave, instance } = props;
let assigned = instance["assigned_slave"] !== null;
return <Descriptions
bordered
size="small"
>
<Descriptions.Item label="Slave">
{!assigned
? <em>Unassigned</em>
: slave["name"] || instance["assigned_slave"]
}
{account.hasPermission("core.instance.assign") && <AssignInstanceModal
id={instance["id"]}
slaveId={instance["assigned_slave"]}
buttonProps={{
size: "small",
style: { float: "Right" },
type: assigned ? "default" : "primary",
disabled: !["unknown", "unassigned", "stopped"].includes(instance["status"]),
}}
buttonContent={assigned ? "Reassign" : "Assign"}
/>}
</Descriptions.Item>
<Descriptions.Item label="Status"><InstanceStatusTag status={instance["status"]} /></Descriptions.Item>
</Descriptions>;
}
export default function InstanceViewPage(props) {
let params = useParams();
let instanceId = Number(params.id);
@@ -60,16 +92,9 @@ export default function InstanceViewPage(props) {
</PageLayout>;
}
let instanceButtons = <Space>
{
account.hasAnyPermission("core.instance.start", "core.instance.stop")
&& <StartStopInstanceButton instance={instance} />
}
{account.hasPermission("core.instance.load_scenario") && <LoadScenarioModal instance={instance} />}
{account.hasPermission("core.instance.export_data") && <Button
loading={exportingData}
disabled={instance.status !== "stopped"}
onClick={() => {
let instanceButtonsMenu = <Menu
onClick={({ key }) => {
if (key === "export") {
setExportingData(true);
libLink.messages.exportData.send(
control, { instance_id: instanceId }
@@ -78,59 +103,69 @@ export default function InstanceViewPage(props) {
).finally(() => {
setExportingData(false);
});
}}
>
Export data
</Button>}
{account.hasPermission("core.instance.delete") && <Popconfirm
title="Permanently delete instance and server saves?"
okText="Delete"
placement="bottomRight"
okButtonProps={{ danger: true }}
onConfirm={() => {
libLink.messages.deleteInstance.send(
} else if (key === "kill") {
libLink.messages.killInstance.send(
control, { instance_id: instanceId }
).then(() => {
history.push("/instances");
}).catch(notifyErrorHandler("Error deleting instance"));
}}
>
<Button
danger
).catch(notifyErrorHandler("Error killing instance"));
} else if (key === "delete") {
Modal.confirm({
autoFocusButton: "cancel",
content: "Permamently delete instance and server saves?",
okText: "Delete",
okButtonProps: { danger: true },
onOk: () => {
libLink.messages.deleteInstance.send(
control, { instance_id: instanceId }
).then(() => {
history.push("/instances");
}).catch(notifyErrorHandler("Error deleting instance"));
},
});
}
}}
>
{account.hasPermission("core.instance.export_data") && <Menu.Item
disabled={exportingData || instance.status !== "stopped"}
key="export"
>Export data</Menu.Item>}
{account.hasPermission("core.instance.kill") && <Menu.Item
disabled={["unknown", "unassigned", "stopped"].includes(instance["status"])}
key="kill"
>Kill process</Menu.Item>}
{account.hasPermission("core.instance.delete") && <>
<Menu.Divider />
<Menu.Item
disabled={!["unknown", "unassigned", "stopped"].includes(instance["status"])}
>
<DeleteOutlined />
</Button>
</Popconfirm>}
danger
key="delete"
icon={<DeleteOutlined />}
>Delete</Menu.Item>
</>}
</Menu>;
let instanceButtons = <Space>
{
account.hasAnyPermission("core.instance.start", "core.instance.stop")
&& <StartStopInstanceButton instance={instance} />
}
{account.hasPermission("core.instance.load_scenario") && <LoadScenarioModal instance={instance} />}
{account.hasAnyPermission(
"core.instance.export_data",
"core.instance.kill",
"core.instance.delete",
) && <Dropdown placement="bottomRight" trigger={["click"]} overlay={instanceButtonsMenu}>
<Button>More <DownOutlined /></Button>
</Dropdown>}
</Space>;
let assigned = instance["assigned_slave"] !== null;
return <PageLayout nav={nav}>
<Descriptions
bordered
size="small"
<PageHeader
className="site-page-header"
title={instance["name"]}
extra={instanceButtons}
>
<Descriptions.Item label="Slave">
{!assigned
? <em>Unassigned</em>
: slave["name"] || instance["assigned_slave"]
}
{account.hasPermission("core.instance.assign") && <AssignInstanceModal
id={instanceId}
slaveId={instance["assigned_slave"]}
buttonProps={{
size: "small",
style: { float: "Right" },
type: assigned ? "default" : "primary",
disabled: !["unknown", "unassigned", "stopped"].includes(instance["status"]),
}}
buttonContent={assigned ? "Reassign" : "Assign"}
/>}
</Descriptions.Item>
<Descriptions.Item label="Status"><InstanceStatusTag status={instance["status"]} /></Descriptions.Item>
</Descriptions>
/>
<InstanceDescription slave={slave} instance={instance} />
{
account.hasAllPermission("core.instance.save.list", "core.instance.save.list_subscribe")
@@ -147,5 +182,7 @@ export default function InstanceViewPage(props) {
}
{account.hasPermission("core.instance.get_config") && <InstanceConfigTree id={instanceId} />}
<PluginExtra component="InstanceViewPage" instance={instance} />
</PageLayout>;
}
@@ -7,6 +7,7 @@ import { libConfig, libLink } from "@clusterio/lib";
import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import { useInstanceList } from "../model/instance";
import InstanceList from "./InstanceList";
import { notifyErrorHandler } from "../util/notify";
@@ -94,5 +95,6 @@ export default function InstancesPage() {
/>
<InstanceList instances={instanceList} />
<PluginExtra component="InstancesPage" />
</PageLayout>;
}
@@ -1,6 +1,7 @@
import React from "react";
import { Typography } from "antd";
import PluginExtra from "./PluginExtra";
import LogConsole from "./LogConsole";
import { useAccount } from "../model/account";
import MasterConfigTree from "./MasterConfigTree";
@@ -9,7 +10,7 @@ import PageLayout from "./PageLayout";
const { Title } = Typography;
export default function SlavesPage() {
export default function MasterPage() {
let account = useAccount();
return <PageLayout nav={[{ name: "Master" }]}>
@@ -19,5 +20,6 @@ export default function SlavesPage() {
<LogConsole master={true} />
</>}
{account.hasPermission("core.master.get_config") && <MasterConfigTree />}
<PluginExtra component="MasterPage" />
</PageLayout>;
};
@@ -0,0 +1,17 @@
import React, { useContext } from "react";
import ControlContext from "./ControlContext";
export default function PluginExtra(params) {
let control = useContext(ControlContext);
let components = [];
for (let [name, plugin] of control.plugins) {
if (Object.prototype.hasOwnProperty.call(plugin.componentExtra, params.component)) {
let ComponentExtra = plugin.componentExtra[params.component];
components.push(<ComponentExtra key={name} plugin={plugin} {...params}/>);
}
}
return components;
}
@@ -8,6 +8,7 @@ import { libLink, libUsers } from "@clusterio/lib";
import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import { notifyErrorHandler } from "../util/notify";
@@ -149,5 +150,6 @@ export default function RoleViewPage() {
</Form.Item>
))}
</Form>
<PluginExtra component="RoleViewPage" role={role} />
</PageLayout>;
}
@@ -8,6 +8,7 @@ import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import { notifyErrorHandler } from "../util/notify";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
@@ -99,5 +100,6 @@ export default function RolesPage() {
},
})}
/>
<PluginExtra component="RolesPage" />
</PageLayout>;
}
+23 -6
View File
@@ -37,6 +37,27 @@ export default function SiteLayout(props) {
combinedPages.push(...plugin.pages);
}
let menuItems = [];
let menuGroups = new Map();
for (let { sidebarName, sidebarGroup, permission, path } of combinedPages) {
if (!sidebarName || permission && !account.hasPermission(permission)) {
continue;
}
if (sidebarGroup) {
let group = menuGroups.get(sidebarGroup);
if (!group) {
group = [];
menuGroups.set(sidebarGroup, group);
}
group.push(<Menu.Item key={path}>{sidebarName}</Menu.Item>);
} else {
menuItems.push(<Menu.Item key={path}>{sidebarName}</Menu.Item>);
}
}
for (let [name, group] of menuGroups) {
menuItems.push(<Menu.ItemGroup key={name} title={name}>{group}</Menu.ItemGroup>);
}
return <Layout style={{ minHeight: "100vh" }}>
<Header className="header">
<div className="site-logo" />
@@ -65,16 +86,12 @@ export default function SiteLayout(props) {
>
<Menu
mode="inline"
defaultOpenKeys={[...menuGroups.keys()]}
selectedKeys={[currentSidebarPath]}
style={{ height: "100%", borderRight: 0 }}
onClick={({ key }) => history.push(key)}
>
{combinedPages.map(({sidebarName, permission, path}) => {
if (!sidebarName || permission && !account.hasPermission(permission)) {
return null;
}
return <Menu.Item key={path}>{sidebarName}</Menu.Item>;
})}
{menuItems}
</Menu>
</Sider>
<Layout className="site-layout-content-container">
@@ -8,6 +8,7 @@ import { useAccount } from "../model/account";
import { useInstanceList } from "../model/instance";
import { useSlave } from "../model/slave";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
const { Title } = Typography;
@@ -60,5 +61,6 @@ export default function SlaveViewPage(props) {
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
<LogConsole slaves={[slaveId]} />
</>}
<PluginExtra component="SlaveViewPage" slave={slave} />
</PageLayout>;
}
@@ -7,6 +7,7 @@ import { libLink } from "@clusterio/lib";
import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import { useSlaveList } from "../model/slave";
import { notifyErrorHandler } from "../util/notify";
@@ -129,5 +130,6 @@ export default function SlavesPage() {
},
})}
/>
<PluginExtra component="SlavesPage" />
</PageLayout>;
};
@@ -11,7 +11,8 @@ export default function StartStopInstanceButton(props) {
let control = useContext(ControlContext);
let [switching, setSwitching] = useState(false);
function onClick() {
function onClick(event) {
event.stopPropagation();
setSwitching(true);
let action;
if (props.instance["status"] === "stopped") {
@@ -8,6 +8,7 @@ import { libLink } from "@clusterio/lib";
import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
import { notifyErrorHandler } from "../util/notify";
@@ -258,5 +259,6 @@ export default function UserViewPage() {
</Space>
</Form.Item>
</Form>
<PluginExtra component="UserViewPage" user={user} />
</PageLayout>;
}
@@ -8,6 +8,7 @@ import { useAccount } from "../model/account";
import ControlContext from "./ControlContext";
import { notifyErrorHandler } from "../util/notify";
import PageLayout from "./PageLayout";
import PluginExtra from "./PluginExtra";
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
@@ -121,5 +122,6 @@ export default function UsersPage() {
},
})}
/>
<PluginExtra component="UsersPage" />
</PageLayout>;
}
+47 -17
View File
@@ -6,6 +6,7 @@ const events = require("events");
const libLink = require("@clusterio/lib/link");
const libUsers = require("@clusterio/lib/users");
const { wait } = require("@clusterio/lib/helpers");
const testStrings = require("../lib/factorio/test_strings");
const {
@@ -252,6 +253,7 @@ describe("Integration of Clusterio", function() {
describe("instance create-save", function() {
it("creates a save", async function() {
slowTest(this);
getControl().saveListUpdates = [];
await execCtl("instance create-save test");
await checkInstanceStatus(44, "stopped");
});
@@ -325,8 +327,32 @@ describe("Integration of Clusterio", function() {
describe("instance send-rcon", function() {
it("sends the command", async function() {
slowTest(this);
await execCtl("instance send-rcon test test");
// TODO check that the command was received
await execCtl("instance send-rcon test technobabble");
let { log } = await libLink.messages.queryLog.send(getControl(), {
all: false,
master: false,
slave_ids: [],
instance_ids: [44],
max_level: null,
limit: 10,
order: "desc",
});
assert(log.some(info => /technobabble/.test(info.message)), "Command was not sent");
});
it("should trigger saveListUpdate on save", async function() {
slowTest(this);
getControl().saveListUpdates = [];
await execCtl("instance send-rcon test /server-save");
let received = false;
for (let x = 0; x < 10; x++) {
if (getControl().saveListUpdates.length) {
received = true;
break;
}
await wait(100);
}
assert(received, "saveListUpdate not sent");
});
});
@@ -481,22 +507,26 @@ describe("Integration of Clusterio", function() {
await execCtl("instance config set 44 research_sync.load_plugin false");
await execCtl("instance config set 44 statistics_exporter.load_plugin false");
await execCtl("instance config set 44 subspace_storage.load_plugin false");
try {
let exchangeString = testStrings.modified.replace(/[\n\r]+/g, "");
let args = `base/freeplay --seed 1234 --map-exchange-string "${exchangeString}"`;
await execCtl(`instance load-scenario test ${args}`);
await checkInstanceStatus(44, "running");
await sendRcon(44, '/c game.print("disable achievements")');
await sendRcon(44, '/c game.print("disable achievements")');
assert.equal(await sendRcon(44, "/c rcon.print(game.default_map_gen_settings.seed)"), "1234\n");
assert.equal(await sendRcon(44, "/c rcon.print(game.map_settings.pollution.ageing)"), "1.5\n");
assert.equal(
await sendRcon(44, "/c rcon.print(game.difficulty_settings.research_queue_setting)"), "never\n"
);
} finally {
await execCtl("instance stop test");
}
let exchangeString = testStrings.modified.replace(/[\n\r]+/g, "");
let args = `base/freeplay --seed 1234 --map-exchange-string "${exchangeString}"`;
await execCtl(`instance load-scenario test ${args}`);
await checkInstanceStatus(44, "running");
await sendRcon(44, '/c game.print("disable achievements")');
await sendRcon(44, '/c game.print("disable achievements")');
assert.equal(await sendRcon(44, "/c rcon.print(game.default_map_gen_settings.seed)"), "1234\n");
assert.equal(await sendRcon(44, "/c rcon.print(game.map_settings.pollution.ageing)"), "1.5\n");
assert.equal(
await sendRcon(44, "/c rcon.print(game.difficulty_settings.research_queue_setting)"), "never\n"
);
});
});
describe("instance kill", function() {
it("kills the instance", async function() {
slowTest(this);
await execCtl("instance kill test");
await checkInstanceStatus(44, "stopped");
});
});
+2
View File
@@ -62,6 +62,8 @@ describe("lib/plugin", function() {
});
it("should define defaults for hooks", async function() {
await masterPlugin.onInstanceStatusChanged({}, "running", "initialized");
await masterPlugin.onMasterConfigFieldChanged({}, "foo", null);
await masterPlugin.onInstanceConfigFieldChanged({}, {}, "foo", null);
await masterPlugin.onMetrics();
await masterPlugin.onShutdown();
masterPlugin.onSlaveConnectionEvent({}, "connect");