Merge pull request #780 from Cooldude2606/feature/instance-restart

Add instance restart
This commit is contained in:
Daniel authored and GitHub committed 2025-07-31 00:18:41 +02:00
commit f966c5b965
17 files changed
+425 -219

No files matched your search

+17 -2
View File
@@ -1038,17 +1038,32 @@ instanceCommands.add(new lib.Command({
definition: ["start <instance>", "Start instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to start", type: "string" });
yargs.options({
"save": { describe: "Save load, defaults to latest", nargs: 1, type: "string" },
"save": { describe: "Save to load, defaults to latest", nargs: 1, type: "string" },
"keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false },
});
}],
handler: async function(args: { instance: string, save?: string, keepOpen: boolean }, control: Control) {
let instanceId = await lib.resolveInstance(control, args.instance);
const instanceId = await lib.resolveInstance(control, args.instance);
await control.setLogSubscriptions({ instanceIds: [instanceId] });
await control.sendTo({ instanceId }, new lib.InstanceStartRequest(args.save));
control.keepOpen = args.keepOpen;
},
}));
instanceCommands.add(new lib.Command({
definition: ["restart <instance>", "Restart instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to restart", type: "string" });
yargs.options({
"save": { describe: "Save to load, defaults to latest", nargs: 1, type: "string" },
"keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false },
});
}],
handler: async function(args: { instance: string, save?: string, keepOpen: boolean }, control: Control) {
const instanceId = await lib.resolveInstance(control, args.instance);
await control.setLogSubscriptions({ instanceIds: [instanceId] });
await control.sendTo({ instanceId }, new lib.InstanceRestartRequest(args.save));
control.keepOpen = args.keepOpen;
},
}));
instanceCommands.add(new lib.Command({
definition: ["load-scenario <instance> <scenario>", "Start instance by loading a scenario", (yargs) => {
+9
View File
@@ -289,6 +289,7 @@ export default class Instance extends lib.Link {
this.handle(lib.InstanceSaveDetailsListRequest, this.handleInstanceSaveDetailsListRequest.bind(this));
this.handle(lib.InstanceCreateSaveRequest, this.handleInstanceCreateSaveRequest.bind(this));
this.handle(lib.InstanceExportDataRequest, this.handleInstanceExportDataRequest.bind(this));
this.handle(lib.InstanceRestartRequest, this.handleInstanceRestartRequest.bind(this));
this.handle(lib.InstanceStopRequest, this.handleInstanceStopRequest.bind(this));
this.handle(lib.InstanceKillRequest, this.handleInstanceKillRequest.bind(this));
this.handle(lib.InstanceSendRconRequest, this.handleInstanceSendRconRequest.bind(this));
@@ -1300,6 +1301,14 @@ end`.replace(/\r?\n/g, " ");
}
}
async handleInstanceRestartRequest(request: lib.InstanceRestartRequest) {
await this.stop();
// Stopping invalidates this instance object, a new one must be created by the host
// To do this we simulate a waking an instance and sending a start request
const instanceConnection = await this._host._connectInstance(this.id);
await instanceConnection.instance.handleInstanceStartRequest(new lib.InstanceStartRequest(request.save));
}
async handleInstanceStopRequest() {
await this.stop();
}
@@ -284,6 +284,26 @@ export class InstanceStartRequest {
}
}
export class InstanceRestartRequest {
declare ["constructor"]: typeof InstanceRestartRequest;
static type = "request" as const;
static src = ["control", "controller"] as const;
static dst = "instance" as const;
static permission = "core.instance.restart" as const;
constructor(
public save?: string,
) { }
static jsonSchema = Type.Object({
"save": Type.Optional(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.save);
}
}
export class SaveDetails {
constructor(
public instanceId: number,
+1
View File
@@ -63,6 +63,7 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
instance.InstanceAssignRequest,
instance.InstanceMetricsRequest,
instance.InstanceStartRequest,
instance.InstanceRestartRequest,
instance.InstanceSaveDetailsListRequest,
instance.InstanceSaveDetailsUpdatesEvent,
instance.InstanceCreateSaveRequest,
+5
View File
@@ -307,6 +307,11 @@ definePermission({
title: "Start instance",
description: "Start instances.",
});
definePermission({
name: "core.instance.restart",
title: "Restart instance",
description: "Restart instances.",
});
definePermission({
name: "core.instance.load_scenario",
title: "Load scenario",
@@ -1,5 +1,5 @@
import React, {useContext, useState} from "react";
import { Button, Descriptions, Flex, Popconfirm, Space, Tooltip, Typography } from "antd";
import { Descriptions, Flex, Popconfirm, Space, Tooltip, Typography } from "antd";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import * as lib from "@clusterio/lib";
@@ -19,13 +19,65 @@ import PageLayout from "./PageLayout";
import { notifyErrorHandler } from "../util/notify";
import webUiPackage from "../../package.json";
import { hasNpmButtonPermission, NpmButton } from "./NpmButton";
import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton";
const { Title } = Typography;
type ControllerControlButtonProps = { canRestart?: boolean, restartRequired?: boolean };
function ControllerControlButton({ canRestart, restartRequired }: ControllerControlButtonProps) {
const account = useAccount();
const control = useContext(ControlContext);
const actions: VariableDropdownButtonProps["actions"] = [];
if (account.hasPermission("core.controller.restart") && canRestart) {
actions.push({
key: "restart",
label: (
<Tooltip title={restartRequired ? "Restart Required" : null}>
Restart {restartRequired ? <ExclamationCircleOutlined style={{ color: "yellow" }}/> : undefined}
</Tooltip>
),
onClick: () => {
control.send(
new lib.ControllerRestartRequest()
).catch(notifyErrorHandler("Error restarting controller"));
},
});
}
if (account.hasPermission("core.controller.stop")) {
actions.push({
key: "stop",
danger: true,
label: (
<Popconfirm
title={<>
Stopping the controller will leave the cluster inoperable until someone with
access to the system it runs on manually starts it again.<br />
Are you sure you want to stop the controller?
</>}
placement="bottomRight"
okText="Stop"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.send(
new lib.ControllerStopRequest()
).catch(notifyErrorHandler("Error stopping controller"));
}}
>
Stop
</Popconfirm>
),
});
}
return <VariableDropdownButton actions={actions} />;
}
export default function ControllerPage() {
let account = useAccount();
const control = useContext(ControlContext);
const account = useAccount();
const [systems] = useSystems();
const system = systems.get("controller");
const [maxLevel, setMaxLevel] = useState<keyof typeof lib.levels>("info");
@@ -34,43 +86,7 @@ export default function ControllerPage() {
<PageHeader
title="Controller"
extra={<Space>
{
account.hasPermission("core.controller.stop")
&& <Popconfirm
title={<>
Stopping the controller will leave the cluster inoperable until someone with
access to the system it runs on manually starts it again.<br />
Are you sure you want to stop the controller?
</>}
placement="bottomRight"
okText="Stop"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.send(
new lib.ControllerStopRequest()
).catch(notifyErrorHandler("Error stopping controller"));
}}
>
<Button danger>Stop</Button>
</Popconfirm>
}
{
account.hasPermission("core.controller.restart")
&& <Tooltip title={system?.restartRequired ? "Restart Required" : null}>
<Button
disabled={system?.canRestart === false}
onClick={() => {
control.send(
new lib.ControllerRestartRequest()
).catch(notifyErrorHandler("Error restarting controller"));
}}
>
Restart
{!system?.restartRequired ? undefined
: <ExclamationCircleOutlined style={{ color: "yellow" }}/>}
</Button>
</Tooltip>
}
<ControllerControlButton canRestart={system?.canRestart} restartRequired={system?.restartRequired}/>
{
hasNpmButtonPermission(true)
&& <NpmButton target="controller" canRestart={system?.canRestart}/>
+56 -39
View File
@@ -22,9 +22,64 @@ import {
import { formatTimestamp } from "../util/time_format";
import { useSystems } from "../model/system";
import { hasNpmButtonPermission, NpmButton } from "./NpmButton";
import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton";
const { Title } = Typography;
type HostControlButtonProps = { hostId: number, canRestart?: boolean, restartRequired?: boolean };
function HostControlButton({ hostId, canRestart, restartRequired }: HostControlButtonProps) {
const account = useAccount();
const control = useContext(ControlContext);
const actions: VariableDropdownButtonProps["actions"] = [];
if (account.hasPermission("core.host.restart") && canRestart) {
actions.push({
key: "restart",
label: (
<Tooltip title={restartRequired ? "Restart Required" : null}>
Restart {restartRequired ? <ExclamationCircleOutlined style={{ color: "yellow" }}/> : undefined}
</Tooltip>
),
onClick: () => {
control.sendTo(
{ hostId },
new lib.HostRestartRequest()
).catch(notifyErrorHandler("Error restarting host"));
},
});
}
if (account.hasPermission("core.host.stop")) {
actions.push({
key: "stop",
danger: true,
label: (
<Popconfirm
title={<>
Stopping this host will leave it inaccessible until someone with
access to the system it runs on manually starts it again.<br />
Are you sure you want to stop the host?
</>}
placement="bottomRight"
okText="Stop"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.sendTo(
{ hostId },
new lib.HostStopRequest()
).catch(notifyErrorHandler("Error stopping host"));
}}
>
Stop
</Popconfirm>
),
});
}
return <VariableDropdownButton actions={actions} />;
}
export default function HostViewPage() {
let params = useParams();
let hostId = Number(params.id);
@@ -50,6 +105,7 @@ export default function HostViewPage() {
}
let hostButtons = <Space>
<HostControlButton hostId={hostId} canRestart={system?.canRestart} restartRequired={system?.restartRequired}/>
{
account.hasPermission("core.host.revoke_token")
&& <Popconfirm
@@ -68,45 +124,6 @@ export default function HostViewPage() {
</Button>
</Popconfirm>
}
{
account.hasPermission("core.host.stop")
&& <Popconfirm
title={<>
Stopping this host will leave it inaccessible until someone with
access to the system it runs on manually starts it again.<br />
Are you sure you want to stop the host?
</>}
placement="bottomRight"
okText="Stop"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.sendTo(
{ hostId },
new lib.HostStopRequest()
).catch(notifyErrorHandler("Error stopping host"));
}}
>
<Button danger>Stop</Button>
</Popconfirm>
}
{
account.hasPermission("core.host.restart")
&& <Tooltip title={system?.restartRequired ? "Restart Required" : null}>
<Button
disabled={system?.canRestart === false}
onClick={() => {
control.sendTo(
{ hostId },
new lib.HostRestartRequest()
).catch(notifyErrorHandler("Error restarting host"));
}}
>
Restart
{!system?.restartRequired ? undefined
: <ExclamationCircleOutlined style={{ color: "yellow" }} />}
</Button>
</Tooltip>
}
{
hasNpmButtonPermission(false)
&& <NpmButton target={{ hostId }} canRestart={system?.canRestart} disabled={!host["connected"]}/>
@@ -0,0 +1,122 @@
import React, { useContext, useState } from "react";
import { Popconfirm } from "antd";
import * as lib from "@clusterio/lib";
import ControlContext from "./ControlContext";
import { useAccount } from "../model/account";
import { notifyErrorHandler } from "../util/notify";
import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton";
type InstanceControlButtonProps = Omit<VariableDropdownButtonProps, "actions"> & {
instance: lib.InstanceDetails;
onFinish?: () => void;
};
export const InstanceControlButtonPermissions = [
"core.instance.start", "core.instance.restart", "core.instance.stop", "core.instance.kill",
] as const;
export default function InstanceControlButton(props: InstanceControlButtonProps) {
const account = useAccount();
const control = useContext(ControlContext);
const [switching, setSwitching] = useState<boolean>(false);
const instanceId = props.instance.id;
const instanceStatus = props.instance.status;
const showStart = ["stopped", "stopping"].includes(instanceStatus);
const showStop = ["running", "starting"].includes(instanceStatus);
function actionFinish() {
setSwitching(false);
if (props.onFinish) {
props.onFinish();
}
}
const actions: VariableDropdownButtonProps["actions"] = [];
if (account.hasPermission("core.instance.start") && showStart !== switching) {
actions.push({
key: "start",
label: "Start",
onClick: () => {
setSwitching(true);
control.sendTo(
{ instanceId },
new lib.InstanceStartRequest(undefined),
).catch(
notifyErrorHandler("Error starting instance")
).finally(actionFinish);
},
});
} else if (account.hasPermission("core.instance.stop")) { // else if because it is stop or start
actions.push({
key: "stop",
label: "Stop",
danger: true,
onClick: () => {
setSwitching(true);
control.sendTo(
{ instanceId },
new lib.InstanceStopRequest(),
).catch(
notifyErrorHandler("Error stopping instance")
).finally(actionFinish);
},
});
}
if (account.hasPermission("core.instance.restart")) {
actions.push({
key: "restart",
label: "Restart",
disabled: instanceStatus !== "running",
onClick: () => {
setSwitching(true);
control.sendTo(
{ instanceId },
new lib.InstanceRestartRequest(undefined),
).catch(
notifyErrorHandler("Error restarting instance")
).finally(actionFinish);
},
});
}
if (account.hasPermission("core.instance.kill")) {
actions.push({
key: "kill",
danger: true,
disabled: instanceStatus === "stopped",
label: (<Popconfirm
title={<>
Killing this instance may leave you save and player data corrupted.
Are you sure you want to kill the instance?
</>}
placement="bottomRight"
okText="Kill"
okButtonProps={{ danger: true }}
onConfirm={() => {
control.sendTo(
{ instanceId },
new lib.InstanceKillRequest(),
).catch(
notifyErrorHandler("Error killing instance")
).finally(actionFinish);
}}
>
Kill
</Popconfirm>),
});
}
return <VariableDropdownButton
{...props}
type="primary"
actions={actions}
loading={switching}
disabled={!(showStart || showStop)}
/>;
}
@@ -8,7 +8,7 @@ import type { ColumnsType } from "antd/es/table";
import { useAccount } from "../model/account";
import { useHosts } from "../model/host";
import InstanceStatusTag from "./InstanceStatusTag";
import StartStopInstanceButton from "./StartStopInstanceButton";
import InstanceControlButton, { InstanceControlButtonPermissions } from "./InstanceControlButton";
import * as lib from "@clusterio/lib";
import Link from "./Link";
import { instancePublicAddress } from "../util/instance";
@@ -98,13 +98,10 @@ export default function InstanceList(props: InstanceListProps) {
},
];
if (account.hasAnyPermission("core.instance.start", "core.instance.stop")) {
if (account.hasAnyPermission(...InstanceControlButtonPermissions)) {
columns.push({
key: "action",
render: (_, instance) => <StartStopInstanceButton
buttonProps={{ size: "small" }}
instance={instance}
/>,
render: (_, instance) => <InstanceControlButton size="small" instance={instance} />,
responsive: ["sm"],
align: "right",
width: 100,
@@ -15,7 +15,7 @@ import InstanceConfigTree from "./InstanceConfigTree";
import LogConsole, { SelectMaxLogLevel } from "./LogConsole";
import InstanceRcon from "./InstanceRcon";
import AssignInstanceModal from "./AssignInstanceModal";
import StartStopInstanceButton from "./StartStopInstanceButton";
import InstanceControlButton, { InstanceControlButtonPermissions } from "./InstanceControlButton";
import LoadScenarioModal from "./LoadScenarioModal";
import SavesList from "./SavesList";
import { notifyErrorHandler } from "../util/notify";
@@ -91,13 +91,6 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
label: "Extract players from save",
});
}
if (account.hasPermission("core.instance.kill")) {
instanceButtonMenuItems.push({
disabled: ["unknown", "unassigned", "stopped"].includes(instance.status!),
key: "kill",
label: "Kill process",
});
}
if (account.hasPermission("core.instance.delete")) {
if (instanceButtonMenuItems.length) {
instanceButtonMenuItems.push({ type: "divider" });
@@ -130,12 +123,6 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
new lib.InstanceExtractPlayersRequest(),
).catch(notifyErrorHandler("Error extracting player data"));
} else if (key === "kill") {
control.sendTo(
{ instanceId },
new lib.InstanceKillRequest(),
).catch(notifyErrorHandler("Error killing instance"));
} else if (key === "delete") {
Modal.confirm({
autoFocusButton: "cancel",
@@ -155,8 +142,8 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
};
return <Space>
{
account.hasAnyPermission("core.instance.start", "core.instance.stop")
&& <StartStopInstanceButton instance={instance} />
account.hasAnyPermission(...InstanceControlButtonPermissions)
&& <InstanceControlButton instance={instance} />
}
<Button
onClick={() => {
@@ -173,7 +160,6 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
{account.hasAnyPermission(
"core.instance.export_data",
"core.instance.extract_players",
"core.instance.kill",
"core.instance.delete",
)
&& <Dropdown placement="bottomRight" trigger={["click"]} menu={instanceButtonsMenuProps}>
@@ -1,6 +1,6 @@
import React, { useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Form, Input, Modal, Select, Tooltip } from "antd";
import { Button, Form, Input, Modal, Select, Space, Tooltip } from "antd";
import * as lib from "@clusterio/lib";
@@ -82,7 +82,7 @@ export default function InstancesPage() {
return <PageLayout nav={[{ name: "Instances" }]}>
<PageHeader
title="Instances"
extra={<>
extra={<Space>
{account.hasPermission("core.instance.create") && <CreateInstanceButton instances={instances}/>}
{account.hasPermission("core.instance.start")
&& <Button onClick={e => instances.forEach(instance => {
@@ -108,7 +108,7 @@ export default function InstancesPage() {
}>
Stop all
</Button>}
</>}
</Space>}
/>
<InstanceList instances={instances} />
@@ -1,61 +0,0 @@
import React, { useContext, useState } from "react";
import { Button } from "antd";
import * as lib from "@clusterio/lib";
import ControlContext from "./ControlContext";
import { notifyErrorHandler } from "../util/notify";
import { ButtonProps } from "antd/es/button";
type StartStopInstanceButtonProps = {
instance: lib.InstanceDetails;
onFinish?: () => void;
buttonProps?: ButtonProps;
};
export default function StartStopInstanceButton(props: StartStopInstanceButtonProps) {
let control = useContext(ControlContext);
let [switching, setSwitching] = useState<boolean>(false);
function onClick(event: React.MouseEvent<HTMLElement>) {
event.stopPropagation();
setSwitching(true);
let action;
if (props.instance.status === "stopped") {
action = control.sendTo(
{ instanceId: props.instance.id },
new lib.InstanceStartRequest(undefined),
).catch(
notifyErrorHandler("Error starting instance")
);
} else if (["starting", "running"].includes(props.instance.status)) {
action = control.sendTo(
{ instanceId: props.instance.id },
new lib.InstanceStopRequest(),
).catch(
notifyErrorHandler("Error stopping instance")
);
} else {
setSwitching(false);
return;
}
action.finally(() => {
setSwitching(false);
if (props.onFinish) {
props.onFinish();
}
});
};
return <Button
{...(props.buttonProps || {})}
loading={switching}
type="primary"
disabled={!["starting", "running", "stopped"].includes(props.instance.status)}
onClick={onClick}
>
{props.instance.status === "stopped" ? "Start" : "Stop"}
</Button>;
}
+2 -2
View File
@@ -382,11 +382,11 @@ export default function UsersPage() {
return <PageLayout nav={[{ name: "Users" }]}>
<PageHeader
title="Users"
extra={<>
extra={<Space>
{account.hasPermission("core.user.create") ? <CreateUserButton /> : undefined}
{account.hasAnyPermission("core.user.bulk_import", "core.user.bulk_export")
? <BulkUserActionButton /> : undefined}
</>}
</Space>}
/>
<Table
columns={[
@@ -0,0 +1,43 @@
import React from "react";
import { Button, Dropdown } from "antd";
import { MenuItemType } from "antd/es/menu/interface";
import { DropdownButtonProps } from "antd/es/dropdown";
export type VariableDropdownButtonProps = DropdownButtonProps & {
actions: (MenuItemType & { onClick?: () => void })[],
};
export default function VariableDropdownButton(props: VariableDropdownButtonProps) {
const menuActions = [...props.actions];
const mainAction = menuActions.shift();
if (!mainAction) {
return <></>;
}
const buttonProps: DropdownButtonProps = {
...props,
danger: mainAction.danger,
onClick: (event) => {
event.stopPropagation();
if (mainAction.onClick) {
mainAction.onClick();
}
},
};
return menuActions.length === 0
? <Button {...buttonProps}>
{mainAction.label}
</Button>
: <Dropdown.Button
{...buttonProps}
// stopPropagation is needed to prevent propagation to table rows
menu={{ items: menuActions, onClick: (e: any) => e.domEvent.stopPropagation() }}
buttonsRender={([left, right]) => [
left, React.cloneElement(right as any, { onClick: (e: any) => e.stopPropagation() }),
]}
>
{mainAction.label}
</Dropdown.Button>;
}
+1 -1
View File
@@ -22,7 +22,7 @@ export { default as AssignInstanceModal } from "./components/AssignInstanceModal
export { default as InstanceList } from "./components/InstanceList";
export { default as InstanceRcon } from "./components/InstanceRcon";
export { default as InstanceStatusTag, statusColors } from "./components/InstanceStatusTag";
export { default as StartStopInstanceButton } from "./components/StartStopInstanceButton";
export { default as InstanceControlButton, InstanceControlButtonPermissions } from "./components/InstanceControlButton";
export { default as PageHeader } from "./components/PageHeader";
export { default as PageLayout } from "./components/PageLayout";
+83 -47
View File
@@ -529,56 +529,92 @@ describe("Integration of Clusterio", function() {
});
});
describe("instance start", function() {
it("should not hang if factorio version does not exist", async function() {
slowTest(this);
try {
await execCtl("instance config set 44 factorio.version 0.1.2");
await assert.rejects(execCtl("instance start test"));
for (let cmd of ["start", "restart"]) {
describe(`instance ${cmd}`, function() {
async function prepareToStart() {
if (cmd === "restart") {
await execCtl("instance start test");
}
}
after(async function() {
// It is expected that after these tests the instance is running, I dislike this dependency
// So this is here to make sure the other tests can continue any of these ones fail
const instances = await getInstances();
if (instances.get(44).status !== "running") {
await execCtl("instance start test");
}
});
it("should not hang if factorio version does not exist", async function() {
slowTest(this);
try {
await execCtl("instance config set 44 factorio.version 0.1.2");
await assert.rejects(execCtl(`instance ${cmd} test`));
} finally {
await execCtl("instance config set 44 factorio.version latest");
} finally {
await execCtl("instance config set 44 factorio.version latest");
}
});
it("should not leave the instance in the stopping state if it fails", async function() {
slowTest(this);
try {
await execCtl("instance config set 44 factorio.game_port 100000");
await assert.rejects(execCtl(`instance ${cmd} test`));
await checkInstanceStatus(44, "stopped");
} finally {
await execCtl("instance config set 44 factorio.game_port");
}
});
it("starts the given save", async function() {
slowTest(this);
await prepareToStart();
await execCtl(`instance ${cmd} test --save world.zip`);
await checkInstanceStatus(44, "running");
});
it("allows having a separate console log", async function() {
slowTest(this);
await execCtl("instance stop 44");
await execCtl("instance config set 44 factorio.console_logging true");
await prepareToStart();
await execCtl(`instance ${cmd} test`);
await checkInstanceStatus(44, "running");
const checkMessage = `check${Date.now()}`;
await sendRcon(44, checkMessage);
const consoleLog = await fs.readFile(path.join("temp", "test", "instances", "test", "console.log"));
assert(consoleLog.includes(checkMessage));
await execCtl("instance config set 44 factorio.console_logging false");
});
it("copies the save if an autosave is the target", async function() {
slowTest(this);
await execCtl("instance stop 44");
let savesDir = path.join("temp", "test", "instances", "test", "saves");
await fs.copy(path.join(savesDir, "world.zip"), path.join(savesDir, "_autosave1.zip"));
await prepareToStart();
await execCtl(`instance ${cmd} test`);
let saves = await getControl().sendTo({ instanceId: 44 }, new lib.InstanceSaveDetailsListRequest());
let running = saves.find(s => s.loaded);
assert(running.name !== "_autosave1.zip");
});
if (cmd === "restart") {
it("fails if the server is not running", async function() {
await execCtl("instance stop test");
await checkInstanceStatus(44, "stopped");
await assert.rejects(
execCtl("instance restart test"),
/Instance is not running/,
);
await execCtl("instance start test");
});
} else {
it("fails if the server is running", async function() {
await assert.rejects(
execCtl("instance start test"),
/Instance is already running./,
);
});
}
});
it("should not leave the instance in the stopping state if it fails", async function() {
slowTest(this);
try {
await execCtl("instance config set 44 factorio.game_port 100000");
await assert.rejects(execCtl("instance start test"));
await checkInstanceStatus(44, "stopped");
} finally {
await execCtl("instance config set 44 factorio.game_port");
}
});
it("starts the given save", async function() {
slowTest(this);
await execCtl("instance start test --save world.zip");
await checkInstanceStatus(44, "running");
});
it("allows having a separate console log", async function() {
slowTest(this);
await execCtl("instance stop 44");
await execCtl("instance config set 44 factorio.console_logging true");
await execCtl("instance start test");
await checkInstanceStatus(44, "running");
const checkMessage = `check${Date.now()}`;
await sendRcon(44, checkMessage);
const consoleLog = await fs.readFile(path.join("temp", "test", "instances", "test", "console.log"));
assert(consoleLog.includes(checkMessage));
await execCtl("instance config set 44 factorio.console_logging false");
});
it("copies the save if an autosave is the target", async function() {
slowTest(this);
await execCtl("instance stop 44");
let savesDir = path.join("temp", "test", "instances", "test", "saves");
await fs.copy(path.join(savesDir, "world.zip"), path.join(savesDir, "_autosave1.zip"));
await execCtl("instance start test");
let saves = await getControl().sendTo({ instanceId: 44 }, new lib.InstanceSaveDetailsListRequest());
let running = saves.find(s => s.loaded);
assert(running.name !== "_autosave1.zip");
});
});
}
describe("instance send-rcon", function() {
this.afterAll(async function() {
+1 -1
View File
@@ -4,7 +4,7 @@ const lib = require("@clusterio/lib");
const { Host } = require("@clusterio/host");
describe("messages/controller", function() {
describe("messages/host", function() {
/** @type {Host} */
let host;