Merge pull request #783 from clusterio/cursor/configure-master-listen-address-53c7

Exclude instance from "start all" function
This commit is contained in:
Daniel authored and GitHub committed 2025-08-04 20:50:47 +02:00
commit 6004f861fe
8 files changed
+259 -2

No files matched your search

+8
View File
@@ -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.
+1
View File
@@ -45,6 +45,7 @@ export default class InstanceInfo {
this.status,
this.factorioVersion,
this.updatedAtMs,
this.config.get("instance.exclude_from_start_all"),
);
}
+90
View File
@@ -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" });
+6
View File
@@ -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,
+1 -1
View File
@@ -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),
+149
View File
@@ -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");
});
});
});