mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 23:24:52 +09:00
Merge pull request #783 from clusterio/cursor/configure-master-listen-address-53c7
Exclude instance from "start all" function
This commit is contained in:
8 files changed
+259
-2
No files matched your search
@@ -239,6 +239,14 @@ Does not affect start or stop of the instance while the host is running.
|
||||
Defaults to false.
|
||||
|
||||
|
||||
### instance.exclude_from_start_all
|
||||
|
||||
If enabled this instance will be excluded from the "Start all" button operation in the web interface.
|
||||
This allows you to prevent certain instances from being started when using the bulk start operation.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
|
||||
### factorio.version
|
||||
|
||||
Version of Factorio to use for this instance.
|
||||
|
||||
@@ -45,6 +45,7 @@ export default class InstanceInfo {
|
||||
this.status,
|
||||
this.factorioVersion,
|
||||
this.updatedAtMs,
|
||||
this.config.get("instance.exclude_from_start_all"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1065,6 +1065,61 @@ instanceCommands.add(new lib.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new lib.Command({
|
||||
definition: ["start-all", "Start all stopped instances (respects exclude_from_start_all setting)", (yargs) => {
|
||||
yargs.options({
|
||||
"force": {
|
||||
describe: "Start instances even if they have exclude_from_start_all set",
|
||||
nargs: 0,
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
}],
|
||||
handler: async function(args: { force: boolean }, control: Control) {
|
||||
// Get all instances
|
||||
const instances = await control.send(new lib.InstanceDetailsListRequest());
|
||||
|
||||
// Filter instances that should be started
|
||||
const instancesToStart = instances.filter((instance: lib.InstanceDetails) => {
|
||||
// Only start instances that are currently stopped
|
||||
if (instance.status !== "stopped") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Respect exclude_from_start_all setting unless --force is used
|
||||
if (!args.force && instance.excludeFromStartAll) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (instancesToStart.length === 0) {
|
||||
print("No instances to start.");
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Starting ${instancesToStart.length} instance(s):`);
|
||||
for (const instance of instancesToStart) {
|
||||
print(` - ${instance.name} (ID: ${instance.id})`);
|
||||
}
|
||||
|
||||
// Start all filtered instances
|
||||
const startPromises = instancesToStart.map(async (instance: lib.InstanceDetails) => {
|
||||
try {
|
||||
await control.sendTo({ instanceId: instance.id }, new lib.InstanceStartRequest());
|
||||
print(`✓ Started ${instance.name}`);
|
||||
} catch (error) {
|
||||
print(`✗ Failed to start ${instance.name}: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(startPromises);
|
||||
print("Start all operation completed.");
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new lib.Command({
|
||||
definition: ["load-scenario <instance> <scenario>", "Start instance by loading a scenario", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to start", type: "string" });
|
||||
@@ -1110,6 +1165,41 @@ instanceCommands.add(new lib.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new lib.Command({
|
||||
definition: ["stop-all", "Stop all running instances", () => {}],
|
||||
handler: async function(_args, control: Control) {
|
||||
// Get all instances
|
||||
const instances = await control.send(new lib.InstanceDetailsListRequest());
|
||||
|
||||
// Filter instances that should be stopped
|
||||
const instancesToStop = instances
|
||||
.filter((instance: lib.InstanceDetails) => ["starting", "running"].includes(instance.status));
|
||||
|
||||
if (instancesToStop.length === 0) {
|
||||
print("No instances to stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Stopping ${instancesToStop.length} instance(s):`);
|
||||
for (const instance of instancesToStop) {
|
||||
print(` - ${instance.name} (ID: ${instance.id})`);
|
||||
}
|
||||
|
||||
// Stop all filtered instances
|
||||
const stopPromises = instancesToStop.map(async (instance: lib.InstanceDetails) => {
|
||||
try {
|
||||
await control.sendTo({ instanceId: instance.id }, new lib.InstanceStopRequest());
|
||||
print(`✓ Stopped ${instance.name}`);
|
||||
} catch (error) {
|
||||
print(`✗ Failed to stop ${instance.name}: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(stopPromises);
|
||||
print("Stop all operation completed.");
|
||||
},
|
||||
}));
|
||||
|
||||
instanceCommands.add(new lib.Command({
|
||||
definition: ["kill <instance>", "Kill instance", (yargs) => {
|
||||
yargs.positional("instance", { describe: "Instance to kill", type: "string" });
|
||||
|
||||
@@ -398,6 +398,7 @@ export interface InstanceConfigFields {
|
||||
"instance.id": number;
|
||||
"instance.assigned_host": number | null;
|
||||
"instance.auto_start": boolean;
|
||||
"instance.exclude_from_start_all": boolean;
|
||||
|
||||
"factorio.version": string;
|
||||
"factorio.executable_path": string | null;
|
||||
@@ -465,6 +466,11 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
"instance.exclude_from_start_all": {
|
||||
description: "Exclude this instance from the 'Start all' button operation",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
"factorio.version": {
|
||||
description: "Version of the game to run, use latest to run the latest installed version",
|
||||
restartRequired: true,
|
||||
|
||||
@@ -161,7 +161,7 @@ export default class ModPack {
|
||||
if (json.name) { modPack.name = json.name; }
|
||||
if (json.description) { modPack.description = json.description; }
|
||||
if (json.factorio_version) { modPack.factorioVersion = json.factorio_version; }
|
||||
if (json.mods) { modPack.mods = new Map(json.mods.map(m => [m.name, m])); }
|
||||
if (json.mods) { modPack.mods = new Map(json.mods.map((m: ModRecord) => [m.name, m])); }
|
||||
if (json.settings) {
|
||||
modPack.settings = {
|
||||
"startup": new Map(Object.entries(json.settings["startup"])),
|
||||
|
||||
@@ -45,6 +45,7 @@ export class InstanceDetails {
|
||||
public factorioVersion: string | undefined,
|
||||
/** Millisecond Unix timestamp this entry was last updated at */
|
||||
public updatedAtMs = 0,
|
||||
public excludeFromStartAll = false,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
@@ -58,6 +59,7 @@ export class InstanceDetails {
|
||||
]),
|
||||
"factorioVersion": Type.Optional(Type.String()),
|
||||
"updatedAtMs": Type.Optional(Type.Number()),
|
||||
"excludeFromStartAll": Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
@@ -69,6 +71,7 @@ export class InstanceDetails {
|
||||
json.status,
|
||||
json.factorioVersion,
|
||||
json.updatedAtMs,
|
||||
json.excludeFromStartAll,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function InstancesPage() {
|
||||
{account.hasPermission("core.instance.create") && <CreateInstanceButton instances={instances}/>}
|
||||
{account.hasPermission("core.instance.start")
|
||||
&& <Button onClick={e => instances.forEach(instance => {
|
||||
if (instance.status === "stopped") {
|
||||
if (instance.status === "stopped" && !instance.excludeFromStartAll) {
|
||||
control.sendTo(
|
||||
{ instanceId: instance.id },
|
||||
new lib.InstanceStartRequest(undefined),
|
||||
|
||||
@@ -34,6 +34,8 @@ describe("Clusterio Instance", function() {
|
||||
await execCtlProcess(`instance config set-prop ${instName} factorio.settings visibility "${visibility}"`);
|
||||
await execCtl(`instance config set-prop ${instName} factorio.settings require_user_verification false`);
|
||||
await execCtl(`instance assign ${instName} 4`);
|
||||
// Exclude main test instance from start-all to prevent interference
|
||||
await execCtl(`instance config set ${instName} instance.exclude_from_start_all true`);
|
||||
|
||||
for (const plugin of [
|
||||
"global_chat", "research_sync", "statistics_exporter", "subspace_storage", "player_auth", "inventory_sync",
|
||||
@@ -47,6 +49,8 @@ describe("Clusterio Instance", function() {
|
||||
await execCtlProcess(`instance config set-prop ${instAltName} factorio.settings visibility "${visibility}"`);
|
||||
await execCtl(`instance config set-prop ${instAltName} factorio.settings require_user_verification false`);
|
||||
await execCtl(`instance assign ${instAltName} 4`);
|
||||
// Exclude alt test instance from start-all to prevent interference
|
||||
await execCtl(`instance config set ${instAltName} instance.exclude_from_start_all true`);
|
||||
await execCtl(`instance start ${instAltName}`);
|
||||
await sendRcon(instAltId, "/sc disable achievements");
|
||||
});
|
||||
@@ -162,3 +166,148 @@ describe("Clusterio Instance", function() {
|
||||
});
|
||||
}
|
||||
});
|
||||
describe("start-all and stop-all commands", function() {
|
||||
// Test instance IDs for start-all/stop-all tests
|
||||
const testInstId1 = 50;
|
||||
const testInstId2 = 51;
|
||||
const testInstId3 = 52;
|
||||
const testInstName1 = "StartAllTest1";
|
||||
const testInstName2 = "StartAllTest2";
|
||||
const testInstName3 = "StartAllTest3";
|
||||
|
||||
before(async function() {
|
||||
this.timeout(30000);
|
||||
|
||||
// Create test instances for start-all/stop-all testing
|
||||
await execCtl(`instance create ${testInstName1} --id ${testInstId1}`);
|
||||
await execCtl(`instance create ${testInstName2} --id ${testInstId2}`);
|
||||
await execCtl(`instance create ${testInstName3} --id ${testInstId3}`);
|
||||
|
||||
// Configure instances
|
||||
await execCtl(`instance assign ${testInstName1} 4`);
|
||||
await execCtl(`instance assign ${testInstName2} 4`);
|
||||
await execCtl(`instance assign ${testInstName3} 4`);
|
||||
|
||||
// Exclude testInstName3 from start-all by default
|
||||
await execCtl(`instance config set ${testInstName3} instance.exclude_from_start_all true`);
|
||||
|
||||
// Disable plugins to speed up testing
|
||||
for (const instanceName of [testInstName1, testInstName2, testInstName3]) {
|
||||
for (const plugin of [
|
||||
"global_chat", "research_sync", "statistics_exporter",
|
||||
"subspace_storage", "player_auth", "inventory_sync",
|
||||
]) {
|
||||
await execCtl(`instance config set ${instanceName} ${plugin}.load_plugin false`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
after(async function() {
|
||||
this.timeout(30000);
|
||||
// Clean up test instances - ensure they are stopped first
|
||||
await execCtl(`instance stop ${testInstName1}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName2}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName3}`).catch(() => {});
|
||||
|
||||
await execCtl(`instance delete ${testInstName1}`).catch(() => {});
|
||||
await execCtl(`instance delete ${testInstName2}`).catch(() => {});
|
||||
await execCtl(`instance delete ${testInstName3}`).catch(() => {});
|
||||
});
|
||||
|
||||
async function getInstanceStatus(id) {
|
||||
const instances = await getControl().send(new lib.InstanceDetailsListRequest());
|
||||
const instanceMap = new Map(instances.map(instance => [instance.id, instance]));
|
||||
return instanceMap.get(id)?.status;
|
||||
}
|
||||
|
||||
describe("start-all command", function() {
|
||||
beforeEach(async function() {
|
||||
this.timeout(15000);
|
||||
// Ensure all test instances are stopped before each test
|
||||
await execCtl(`instance stop ${testInstName1}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName2}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName3}`).catch(() => {});
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
this.timeout(15000);
|
||||
// Clean up: stop any instances that were started during tests
|
||||
await execCtl(`instance stop ${testInstName1}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName2}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName3}`).catch(() => {});
|
||||
});
|
||||
|
||||
it("starts only stopped instances that are not excluded by default", async function() {
|
||||
this.timeout(20000);
|
||||
// Start one instance to verify it's not started again
|
||||
await execCtl(`instance start ${testInstName2}`);
|
||||
|
||||
// Verify initial state
|
||||
assert.equal(await getInstanceStatus(testInstId1), "stopped");
|
||||
assert.equal(await getInstanceStatus(testInstId2), "running");
|
||||
assert.equal(await getInstanceStatus(testInstId3), "stopped");
|
||||
|
||||
// Run start-all without force
|
||||
await execCtl("instance start-all");
|
||||
|
||||
// Verify results - only testInstName1 should be started
|
||||
// testInstName2 was already running, testInstName3 is excluded
|
||||
assert.equal(await getInstanceStatus(testInstId1), "running");
|
||||
assert.equal(await getInstanceStatus(testInstId2), "running");
|
||||
assert.equal(await getInstanceStatus(testInstId3), "stopped");
|
||||
});
|
||||
|
||||
it("starts excluded instances when --force flag is provided", async function() {
|
||||
this.timeout(20000);
|
||||
// Verify initial state - all stopped
|
||||
assert.equal(await getInstanceStatus(testInstId1), "stopped");
|
||||
assert.equal(await getInstanceStatus(testInstId2), "stopped");
|
||||
assert.equal(await getInstanceStatus(testInstId3), "stopped");
|
||||
|
||||
// Run start-all with force
|
||||
await execCtl("instance start-all --force");
|
||||
|
||||
// Verify results - all instances should be started including excluded one
|
||||
assert.equal(await getInstanceStatus(testInstId1), "running");
|
||||
assert.equal(await getInstanceStatus(testInstId2), "running");
|
||||
assert.equal(await getInstanceStatus(testInstId3), "running");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop-all command", function() {
|
||||
beforeEach(async function() {
|
||||
this.timeout(15000);
|
||||
// Ensure a known state before each test
|
||||
await execCtl(`instance stop ${testInstName1}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName2}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName3}`).catch(() => {});
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
this.timeout(15000);
|
||||
// Clean up: stop any instances that were started during tests
|
||||
await execCtl(`instance stop ${testInstName1}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName2}`).catch(() => {});
|
||||
await execCtl(`instance stop ${testInstName3}`).catch(() => {});
|
||||
});
|
||||
|
||||
it("stops only running or starting instances", async function() {
|
||||
this.timeout(20000);
|
||||
// Start some instances to test stop-all
|
||||
await execCtl(`instance start ${testInstName1}`);
|
||||
await execCtl(`instance start ${testInstName3}`);
|
||||
|
||||
// Verify initial state
|
||||
assert.equal(await getInstanceStatus(testInstId1), "running");
|
||||
assert.equal(await getInstanceStatus(testInstId2), "stopped");
|
||||
assert.equal(await getInstanceStatus(testInstId3), "running");
|
||||
|
||||
await execCtl("instance stop-all");
|
||||
|
||||
// Verify results - running instances should be stopped, stopped ones unchanged
|
||||
assert.equal(await getInstanceStatus(testInstId1), "stopped");
|
||||
assert.equal(await getInstanceStatus(testInstId2), "stopped");
|
||||
assert.equal(await getInstanceStatus(testInstId3), "stopped");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user