mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Implement kill instance
Add ability to kill running instances in case it is desirable to stop the server without saving over the current save file.
This commit is contained in:
1 parent
a93f048814
commit
1e4d709a06
8 files changed
+186
-90
No files matched your search
@@ -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
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -487,6 +487,7 @@ class FactorioServer extends events.EventEmitter {
|
||||
|
||||
// Array of possible causes for an unexpected shutdown
|
||||
this._unexpected = [];
|
||||
this._killed = false;
|
||||
|
||||
// Track autosaving
|
||||
this._runningAutosave = null;
|
||||
@@ -694,28 +695,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 +1007,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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
+16
-1
@@ -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 => {
|
||||
@@ -697,6 +701,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 +844,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,7 +1,8 @@
|
||||
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";
|
||||
|
||||
@@ -24,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);
|
||||
@@ -61,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 }
|
||||
@@ -79,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")
|
||||
|
||||
@@ -481,22 +481,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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user