mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
Refactor user and instance manager (#837)
* Refactor UserManager to use Datastore * Rename lib.User to UserDetails also adds to/fromUserDetails * Fix existing tests * Add user class tests * Add tests for finaliseUsers * Move HostDetails to own file with tests * Move InstanceDetails to its own file with tests * Rename HostInfo and add tests * Rename InstanceInfo and add tests - Also changes the names of the host properties related to instances - Also removes the need to specify the location of InstanceInfo * Make json round trip test validate schema * Add InstanceManager * Fix lint errors * Extra tests * Add final test coverage * Add one last test to InstanceManager * Response to comments * Use generator rather than return anonumous generator Co-authored-by: Hornwitser <github@hornwitser.no> * Fix lint issue with generator --------- Co-authored-by: Hornwitser <github@hornwitser.no>
This commit is contained in:
1 parent
5bea930d05
commit
2235294c59
60 files changed
+3371
-1227
No files matched your search
@@ -0,0 +1,274 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import type Controller from "./Controller";
|
||||
import type HostConnection from "./HostConnection";
|
||||
import InstanceRecord from "./InstanceRecord";
|
||||
|
||||
export default class InstanceManager {
|
||||
private static readonly DefaultFactorioSettings = {
|
||||
tags: ["clusterio"],
|
||||
max_players: 0,
|
||||
visibility: { public: true, lan: true },
|
||||
game_password: "",
|
||||
require_user_verification: true,
|
||||
max_upload_in_kilobytes_per_second: 0,
|
||||
max_upload_slots: 5,
|
||||
ignore_player_limit_for_returning_players: false,
|
||||
allow_commands: "admins-only",
|
||||
autosave_interval: 10,
|
||||
autosave_slots: 5,
|
||||
afk_autokick_interval: 0,
|
||||
auto_pause: false,
|
||||
only_admins_can_pause_the_game: true,
|
||||
autosave_only_on_server: true,
|
||||
non_blocking_saving: false,
|
||||
} as const;
|
||||
|
||||
constructor(
|
||||
public readonly records: lib.SubscribableDatastore<InstanceRecord>,
|
||||
private readonly _controller: Controller,
|
||||
) {
|
||||
for (const instance of records.values()) {
|
||||
this.addInstanceHooks(instance);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Basic Accessors */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
has(id: InstanceRecord["id"]): boolean {
|
||||
return this.records.has(id);
|
||||
}
|
||||
|
||||
getMutable(id: InstanceRecord["id"]): InstanceRecord | undefined {
|
||||
return this.records.get(id);
|
||||
}
|
||||
|
||||
get(id: InstanceRecord["id"]): Readonly<InstanceRecord> | undefined {
|
||||
return this.getMutable(id);
|
||||
}
|
||||
|
||||
valuesMutable(): IterableIterator<InstanceRecord> {
|
||||
return this.records.values();
|
||||
}
|
||||
|
||||
values(): IterableIterator<Readonly<InstanceRecord>> {
|
||||
return this.valuesMutable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance by ID for a request.
|
||||
*
|
||||
* Convenience wrapper around {@link getMutable} that throws
|
||||
* if the instance does not exist.
|
||||
*
|
||||
* @param instanceId - ID of instance to get.
|
||||
* @returns The instance record.
|
||||
* @throws {module:lib.RequestError} if the instance does not exist.
|
||||
*/
|
||||
getForRequest(instanceId: number): InstanceRecord {
|
||||
const instance = this.records.get(instanceId);
|
||||
if (!instance) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Internal Helpers */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
private async _notifyStatusChanged(
|
||||
instance: InstanceRecord,
|
||||
prev?: lib.InstanceStatus,
|
||||
): Promise<void> {
|
||||
await lib.invokeHook(
|
||||
this._controller.plugins,
|
||||
"onInstanceStatusChanged",
|
||||
instance,
|
||||
prev,
|
||||
);
|
||||
}
|
||||
|
||||
private async _notifyConfigFieldChanged(
|
||||
instance: InstanceRecord,
|
||||
field: string,
|
||||
curr: unknown,
|
||||
prev: unknown,
|
||||
): Promise<void> {
|
||||
await lib.invokeHook(
|
||||
this._controller.plugins,
|
||||
"onInstanceConfigFieldChanged",
|
||||
instance,
|
||||
field,
|
||||
curr,
|
||||
prev,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach change listeners to an instance record.
|
||||
*
|
||||
* Hooks into configuration field changes and ensures the datastore
|
||||
* is updated and plugins are notified when a config field changes.
|
||||
*
|
||||
* @param instance - The instance record to attach hooks to.
|
||||
* @internal
|
||||
*/
|
||||
addInstanceHooks(instance: InstanceRecord): void {
|
||||
instance.config.on("fieldChanged", (field: string, curr: unknown, prev: unknown) => {
|
||||
this.records.set(instance);
|
||||
this._notifyConfigFieldChanged(instance, field, curr, prev);
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Lifecycle */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @example
|
||||
* const config = new lib.InstanceConfig("controller", { "instance.name": "My instance" });
|
||||
* const instance = await controller.instances.createInstance(config);
|
||||
*
|
||||
* @param instanceConfig - Config to base the newly created instance on.
|
||||
* @param _suppressChanges - When true, side effects are suppressed, caller takes ownership.
|
||||
* @returns The created instance record.
|
||||
* @throws {module:lib.RequestError} if an instance with the same ID already exists.
|
||||
*/
|
||||
async createInstance(
|
||||
instanceConfig: lib.InstanceConfig,
|
||||
_suppressChanges?: boolean,
|
||||
): Promise<InstanceRecord> {
|
||||
const instanceId = instanceConfig.get("instance.id");
|
||||
if (this.records.has(instanceId)) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} already exists`);
|
||||
}
|
||||
|
||||
const controllerName = this._controller.config.get("controller.name");
|
||||
instanceConfig.set("factorio.settings", {
|
||||
name: `${controllerName} - ${instanceConfig.get("instance.name")}`,
|
||||
description: `Clusterio instance for ${controllerName}`,
|
||||
...InstanceManager.DefaultFactorioSettings,
|
||||
...instanceConfig.get("factorio.settings"),
|
||||
});
|
||||
|
||||
const instance = new InstanceRecord(instanceConfig, "unassigned");
|
||||
this.addInstanceHooks(instance);
|
||||
|
||||
if (!_suppressChanges) {
|
||||
this.records.set(instance);
|
||||
await this._notifyStatusChanged(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the assigned host of an instance.
|
||||
*
|
||||
* Unassigns the instance from the currently assigned host (if connected)
|
||||
* and assigns it to the provided host.
|
||||
*
|
||||
* This is the only supported way of modifying the
|
||||
* `instance.assigned_host` configuration entry.
|
||||
*
|
||||
* Note:
|
||||
* - This does NOT transfer any files or saves between hosts.
|
||||
* - If the instance is already assigned to the specified host, no action is taken.
|
||||
*
|
||||
* @param instanceId - ID of instance to assign.
|
||||
* @param hostId - ID of host to assign the instance to. If undefined,
|
||||
* the instance will be unassigned.
|
||||
*
|
||||
* @throws {module:lib.RequestError} if the instance does not exist.
|
||||
* @throws {module:lib.RequestError} if the target host is not connected.
|
||||
*/
|
||||
async assignInstance(
|
||||
instanceId: number,
|
||||
hostId: number | null,
|
||||
): Promise<void> {
|
||||
const instance = this.getForRequest(instanceId);
|
||||
const hostConnections = this._controller.wsServer.hostConnections;
|
||||
|
||||
const currentAssignedHost = instance.config.get("instance.assigned_host");
|
||||
if (currentAssignedHost === hostId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newHostConnection: HostConnection | undefined;
|
||||
if (hostId !== null) {
|
||||
newHostConnection = hostConnections.get(hostId);
|
||||
if (!newHostConnection) {
|
||||
throw new lib.RequestError("Target host is not connected to the controller");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentAssignedHost !== null) {
|
||||
const oldHostConnection = hostConnections.get(currentAssignedHost);
|
||||
if (oldHostConnection && !oldHostConnection.connector.closing) {
|
||||
await oldHostConnection.send(
|
||||
new lib.InstanceUnassignInternalRequest(instanceId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._controller.clearSavesOfInstance(instanceId);
|
||||
instance.config.set("instance.assigned_host", hostId);
|
||||
|
||||
if (newHostConnection) {
|
||||
await newHostConnection.send(
|
||||
new lib.InstanceAssignInternalRequest(instanceId, instance.config.toRemote("host")),
|
||||
);
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
}
|
||||
|
||||
this.records.set(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign an instance from its current host.
|
||||
*
|
||||
* Convenience wrapper around {@link assignInstance} that removes
|
||||
* the assigned host.
|
||||
*
|
||||
* @param instanceId - ID of instance to unassign.
|
||||
*/
|
||||
async unassignInstance(instanceId: number): Promise<void> {
|
||||
return this.assignInstance(instanceId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an instance permanently.
|
||||
*
|
||||
* Sends a delete request to the assigned host (if any),
|
||||
* removes the instance from the datastore,
|
||||
* clears associated saves and user statistics.
|
||||
*
|
||||
* @param instanceId - ID of instance to delete.
|
||||
*
|
||||
* @throws {module:lib.RequestError} if the instance does not exist.
|
||||
*/
|
||||
async deleteInstance(instanceId: number): Promise<void> {
|
||||
const instance = this.getForRequest(instanceId);
|
||||
const hostId = instance.config.get("instance.assigned_host");
|
||||
|
||||
if (hostId !== null) {
|
||||
await this._controller.sendTo(
|
||||
{ hostId },
|
||||
new lib.InstanceDeleteInternalRequest(instanceId),
|
||||
);
|
||||
}
|
||||
|
||||
const prevStatus = instance.status;
|
||||
this.records.delete(instance);
|
||||
this._controller.clearSavesOfInstance(instanceId);
|
||||
this._controller.users.clearStatsOfInstance(instanceId);
|
||||
|
||||
await this._notifyStatusChanged(instance, prevStatus);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user