From ab52cc35bff2a5959448e7876a902ede4c22a9c8 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 30 Nov 2023 18:31:42 +0100 Subject: [PATCH] Provide controller resources at construction time Load hosts, instances, mod packs, mods and users before constructing the Controller instances. This allows these resources to be correctly typed as always available and prevents any possibility of save being initiated on these resources before they are loaded. --- packages/controller/controller.ts | 8 +- packages/controller/src/BaseConnection.ts | 4 +- packages/controller/src/ControlConnection.ts | 28 +++---- packages/controller/src/Controller.ts | 88 +++++++++++--------- packages/controller/src/ControllerRouter.ts | 2 +- packages/controller/src/HostConnection.ts | 18 ++-- packages/controller/src/WsServer.ts | 6 +- packages/controller/src/routes.ts | 2 +- plugins/inventory_sync/controller.ts | 8 +- plugins/player_auth/controller.ts | 2 +- plugins/subspace_storage/controller.ts | 2 +- 11 files changed, 91 insertions(+), 77 deletions(-) diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index e177a12d..5295740c 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -383,7 +383,13 @@ async function startup() { return; } - controller = new Controller(clusterLogger, pluginInfos, controllerConfigPath, controllerConfig); + controller = new Controller( + clusterLogger, + pluginInfos, + controllerConfigPath, + controllerConfig, + ...await Controller.bootstrap(controllerConfig) + ); let secondSigint = false; process.on("SIGINT", () => { diff --git a/packages/controller/src/BaseConnection.ts b/packages/controller/src/BaseConnection.ts index 4c55eb9a..b12c7e3c 100644 --- a/packages/controller/src/BaseConnection.ts +++ b/packages/controller/src/BaseConnection.ts @@ -50,7 +50,7 @@ export default class BaseConnection extends lib.Link { async handleModPackGetRequest(request: lib.ModPackGetRequest): Promise { let { id } = request; - let modPack = this._controller.modPacks!.get(id); + let modPack = this._controller.modPacks.get(id); if (!modPack) { throw new lib.RequestError(`Mod pack with ID ${id} does not exist`); } @@ -62,7 +62,7 @@ export default class BaseConnection extends lib.Link { if (id === null) { throw new lib.RequestError("Default mod pack not set on controller"); } - let modPack = this._controller.modPacks!.get(id); + let modPack = this._controller.modPacks.get(id); if (!modPack) { throw new lib.RequestError(`Default mod pack configured (${id}) does not exist`); } diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index eee48d8d..d4c8d779 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -185,7 +185,7 @@ export default class ControlConnection extends BaseConnection { } async handleHostRevokeTokensRequest(request: lib.HostRevokeTokensRequest) { - const host = this._controller.hosts?.get(request.hostId); + const host = this._controller.hosts.get(request.hostId); if (!host) { throw new Error(`Unknown host id (${request.hostId})`); } @@ -202,7 +202,7 @@ export default class ControlConnection extends BaseConnection { async handleHostListRequest(): Promise { let list = []; - for (let host of this._controller.hosts!.values()) { + for (let host of this._controller.hosts.values()) { list.push(new lib.HostDetails( host.agent, host.version, @@ -266,7 +266,7 @@ export default class ControlConnection extends BaseConnection { async handleInstanceDetailsListRequest() { let list = []; - for (let instance of this._controller.instances!.values()) { + for (let instance of this._controller.instances.values()) { let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host"); if (assigned_host === null) { assigned_host = undefined; @@ -424,7 +424,7 @@ export default class ControlConnection extends BaseConnection { } async handleModPackListRequest() { - return [...this._controller.modPacks!.values()]; + return [...this._controller.modPacks.values()]; } async handleModPackCreateRequest(request: lib.ModPackCreateRequest) { @@ -432,37 +432,37 @@ export default class ControlConnection extends BaseConnection { if (modPack.id === undefined) { throw new lib.RequestError("Mod pack need an ID to be created"); } - if (this._controller.modPacks!.has(modPack.id)) { + if (this._controller.modPacks.has(modPack.id)) { throw new lib.RequestError(`Mod pack with ID ${modPack.id} already exist`); } - this._controller.modPacks!.set(modPack.id, modPack); + this._controller.modPacks.set(modPack.id, modPack); this._controller.modPackUpdated(modPack); } async handleModPackUpdateRequest(request: lib.ModPackUpdateRequest) { let modPack = request.modPack; - if (modPack.id === undefined || !this._controller.modPacks!.has(modPack.id)) { + if (modPack.id === undefined || !this._controller.modPacks.has(modPack.id)) { throw new lib.RequestError(`Mod pack with ID ${modPack.id} does not exist`); } - this._controller.modPacks!.set(modPack.id, modPack); + this._controller.modPacks.set(modPack.id, modPack); this._controller.modPackUpdated(modPack); } async handleModPackDeleteRequest(request: lib.ModPackDeleteRequest) { let { id } = request; - let modPack = this._controller.modPacks!.get(id); + let modPack = this._controller.modPacks.get(id); if (!modPack) { throw new lib.RequestError(`Mod pack with ID ${id} does not exist`); } modPack.isDeleted = true; - this._controller.modPacks!.delete(id); + this._controller.modPacks.delete(id); this._controller.modPackUpdated(modPack); } async handleModGetRequest(request: lib.ModGetRequest) { let { name, version } = request; let filename = `${name}_${version}.zip`; - let mod = this._controller.mods!.get(filename); + let mod = this._controller.mods.get(filename); if (!mod) { throw new lib.RequestError(`Mod ${filename} does not exist`); } @@ -470,7 +470,7 @@ export default class ControlConnection extends BaseConnection { } async handleModListRequest() { - return [...this._controller.mods!.values()]; + return [...this._controller.mods.values()]; } static termsMatchesMod(terms: lib.ParsedTerm[], mod: lib.ModInfo) { @@ -510,7 +510,7 @@ export default class ControlConnection extends BaseConnection { type ModVersions = { name: string, versions: lib.ModInfo[] }; let results: Map = new Map(); - for (let mod of this._controller.mods!.values()) { + for (let mod of this._controller.mods.values()) { if ( mod.factorioVersion !== factorioVersion || !ControlConnection.termsMatchesMod(query.terms, mod) @@ -565,7 +565,7 @@ export default class ControlConnection extends BaseConnection { async handleModDownloadRequest(request: lib.ModDownloadRequest) { let { name, version } = request; let filename = `${name}_${version}.zip`; - let mod = this._controller.mods!.get(filename); + let mod = this._controller.mods.get(filename); if (!mod) { throw new lib.RequestError(`Mod ${filename} does not exist`); } diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 48cd20da..1ee2fb60 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -48,17 +48,6 @@ export default class Controller { config: lib.ControllerConfig; app: Application; - /** Mapping of host id to host info */ - hosts: Map | null = null; - /** Mapping of instance id to instance info */ - instances: Map | null = null; - /** Mapping of mod pack id to mod pack */ - modPacks: Map | null = null; - /** Mapping of mod names to mod infos */ - mods: Map | null = null; - /** User and roles manager for the cluster */ - userManager: UserManager; - /** True if {@link Controller.hosts} has changed since last time it was saved */ hostsDirty = false; /** True if {@link Controller.instances} has changed since last time it was saved */ @@ -98,11 +87,45 @@ export default class Controller { clusterLogIndex: lib.LogIndex | null = null; clusterLogBuildInterval: ReturnType | null = null; + static async bootstrap(config: lib.ControllerConfig) { + let databaseDirectory = config.get("controller.database_directory"); + await fs.ensureDir(databaseDirectory); + + const hosts = await Controller.loadHosts(path.join(databaseDirectory, "hosts.json")); + const instances = await Controller.loadInstances(path.join(databaseDirectory, "instances.json")); + const modPacks = await Controller.loadModPacks(path.join(databaseDirectory, "mod-packs.json")); + const userManager = new UserManager(config); + await userManager.load(path.join(databaseDirectory, "users.json")); + + let modsDirectory = config.get("controller.mods_directory"); + await fs.ensureDir(modsDirectory); + const mods = await Controller.loadModInfos(modsDirectory); + + return [ + hosts, + instances, + modPacks, + mods, + userManager, + ] as const; + } + constructor( clusterLogger: winston.Logger, pluginInfos: any[], configPath: string, - config: lib.ControllerConfig + config: lib.ControllerConfig, + + /** Mapping of host id to host info */ + public hosts = new Map(), + /** Mapping of instance id to instance info */ + public instances = new Map(), + /** Mapping of mod pack id to mod pack */ + public modPacks = new Map(), + /** Mapping of mod names to mod infos */ + public mods = new Map(), + /** User and roles manager for the cluster */ + public userManager = new UserManager(config), ) { this.clusterLogger = clusterLogger; this.pluginInfos = pluginInfos; @@ -114,7 +137,6 @@ export default class Controller { this.app.locals.streams = new Map(); this.wsServer = new WsServer(this); - this.userManager = new UserManager(this.config); this.subscriptions = new lib.SubscriptionController(this); // Handle subscriptions for all internal properties @@ -178,25 +200,13 @@ export default class Controller { this._startDevServer(args); } - let databaseDirectory = this.config.get("controller.database_directory"); - await fs.ensureDir(databaseDirectory); - - this.hosts = await Controller.loadHosts(path.join(databaseDirectory, "hosts.json")); - this.instances = await Controller.loadInstances(path.join(databaseDirectory, "instances.json")); - this.modPacks = await Controller.loadModPacks(path.join(databaseDirectory, "mod-packs.json")); - await this.userManager.load(path.join(databaseDirectory, "users.json")); - - let modsDirectory = this.config.get("controller.mods_directory"); - await fs.ensureDir(modsDirectory); - this.mods = await Controller.loadModInfos(modsDirectory); - this.config.on("fieldChanged", (group, field, prev) => { if (group.name === "controller" && field === "autosave_interval") { this.onAutosaveIntervalChanged(); } lib.invokeHook(this.plugins, "onControllerConfigFieldChanged", group, field, prev); }); - for (let instance of this.instances!.values()) { + for (let instance of this.instances.values()) { this.addInstanceHooks(instance); } @@ -346,9 +356,7 @@ export default class Controller { await lib.invokeHook(this.plugins, "onShutdown"); - if (this.wsServer) { - await this.wsServer.stop(); - } + await this.wsServer.stop(); let stopTasks = []; logger.info("Stopping HTTP(S) server"); @@ -401,22 +409,22 @@ export default class Controller { } let databaseDirectory = this.config.get("controller.database_directory"); - if (this.hosts && this.hostsDirty) { + if (this.hostsDirty) { this.hostsDirty = false; await Controller.saveHosts(path.join(databaseDirectory, "hosts.json"), this.hosts); } - if (this.instances && this.instancesDirty) { + if (this.instancesDirty) { this.instancesDirty = false; await Controller.saveInstances(path.join(databaseDirectory, "instances.json"), this.instances); } - if (this.modPacks && this.modPacksDirty) { + if (this.modPacksDirty) { this.modPacksDirty = false; await Controller.saveModPacks(path.join(databaseDirectory, "mod-packs.json"), this.modPacks); } - if (this.userManager && this.userManager.dirty) { + if (this.userManager.dirty) { await this.userManager.save(path.join(databaseDirectory, "users.json")); } @@ -548,14 +556,14 @@ export default class Controller { async deleteMod(name: string, version: string): Promise { let filename = `${name}_${version}.zip`; - let mod = this.mods!.get(filename); + let mod = this.mods.get(filename); if (!mod) { throw new Error(`Mod ${filename} does not exist`); } let modsDirectory = this.config.get("controller.mods_directory"); await fs.unlink(path.join(modsDirectory, filename)); - this.mods!.delete(filename); + this.mods.delete(filename); mod.isDeleted = true; this.modUpdated(mod); } @@ -662,7 +670,7 @@ export default class Controller { * @throws {module:lib.RequestError} if the instance does not exist. */ getRequestInstance(instanceId:number): InstanceInfo { - let instance = this.instances!.get(instanceId); + let instance = this.instances.get(instanceId); if (!instance) { throw new lib.RequestError(`Instance with ID ${instanceId} does not exist`); } @@ -688,7 +696,7 @@ export default class Controller { */ async instanceCreate(instanceConfig: lib.InstanceConfig): Promise { let instanceId = instanceConfig.get("instance.id"); - if (this.instances!.has(instanceId)) { + if (this.instances.has(instanceId)) { throw new lib.RequestError(`Instance with ID ${instanceId} already exists`); } @@ -719,7 +727,7 @@ export default class Controller { instanceConfig.set("factorio.settings", settings); let instance = new InstanceInfo({ config: instanceConfig, status: "unassigned" }); - this.instances!.set(instanceId, instance); + this.instances.set(instanceId, instance); this.instancesDirty = true; await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance); this.addInstanceHooks(instance); @@ -793,7 +801,7 @@ export default class Controller { if (hostId !== null) { await this.sendTo({ hostId }, new lib.InstanceDeleteInternalRequest(instanceId)); } - this.instances!.delete(instanceId); + this.instances.delete(instanceId); this.instancesDirty = true; let prev = instance.status; @@ -1173,7 +1181,7 @@ export default class Controller { connection = this.wsServer.controlConnections.get(dst.id); } else if (dst.type === lib.Address.instance) { - let instance = this.instances!.get(dst.id); + let instance = this.instances.get(dst.id); if (!instance) { return; } diff --git a/packages/controller/src/ControllerRouter.ts b/packages/controller/src/ControllerRouter.ts index 51e972df..84420595 100644 --- a/packages/controller/src/ControllerRouter.ts +++ b/packages/controller/src/ControllerRouter.ts @@ -40,7 +40,7 @@ export default class ControllerRouter { msg = `Host ${dst.id} is offline`; } } else if (dst.type === lib.Address.instance) { - let instance = this.controller.instances!.get(dst.id); + let instance = this.controller.instances.get(dst.id); if (!instance) { msg = `Instance ${dst.id} does not exist`; } else { diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index f04b03ff..8d893cd6 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -46,9 +46,9 @@ export default class HostConnection extends BaseConnection { this.plugins = new Map(Object.entries(registerData.plugins)); this._checkPluginVersions(); - let currentHostInfo = this._controller.hosts!.get(this._id); + let currentHostInfo = this._controller.hosts.get(this._id); - this._controller.hosts!.set(this._id, { + this._controller.hosts.set(this._id, { agent: this._agent, id: this._id, name: this._name, @@ -70,7 +70,7 @@ export default class HostConnection extends BaseConnection { this.connector.on("close", () => { // Update status to unknown for instances on this host. - for (let instance of this._controller.instances!.values()) { + for (let instance of this._controller.instances.values()) { if (instance.config.get("instance.assigned_host") !== this._id) { continue; } @@ -105,7 +105,7 @@ export default class HostConnection extends BaseConnection { break; case lib.Address.instance: - let instance = this._controller.instances!.get(message.src.id); + let instance = this._controller.instances.get(message.src.id); if (!instance || instance.config.get("instance.assigned_host") !== this._id) { throw new lib.InvalidMessage( `Received message with invalid src ${message.src} from ${origin}` @@ -157,7 +157,7 @@ export default class HostConnection extends BaseConnection { } async handleInstanceStatusChangedEvent(request: lib.InstanceStatusChangedEvent) { - let instance = this._controller.instances!.get(request.instanceId); + let instance = this._controller.instances.get(request.instanceId); // It's possible to get updates from an instance that does not exist // or is not assigned to the host it originated from if it was @@ -187,7 +187,7 @@ export default class HostConnection extends BaseConnection { async handleInstancesUpdateRequest(request: lib.InstancesUpdateRequest) { // Push updated instance configs - for (let instance of this._controller.instances!.values()) { + for (let instance of this._controller.instances.values()) { if (instance.config.get("instance.assigned_host") === this._id) { await this.send( new lib.InstanceAssignInternalRequest(instance.id, instance.config.serialize("host")) @@ -200,7 +200,7 @@ export default class HostConnection extends BaseConnection { let instanceConfig = new lib.InstanceConfig("controller"); await instanceConfig.load(instanceData.config as lib.SerializedConfig, "host"); - let controllerInstance = this._controller.instances!.get(instanceConfig.get("instance.id")); + let controllerInstance = this._controller.instances.get(instanceConfig.get("instance.id")); if (controllerInstance) { // Check if this instance is assigned somewhere else. if (controllerInstance.config.get("instance.assigned_host") !== this._id) { @@ -227,7 +227,7 @@ export default class HostConnection extends BaseConnection { let newInstance = new InstanceInfo( { config: instanceConfig, status: instanceData.status as InstanceStatus } ); - this._controller.instances!.set(instanceConfig.get("instance.id"), newInstance); + this._controller.instances.set(instanceConfig.get("instance.id"), newInstance); this._controller.instancesDirty = true; this._controller.addInstanceHooks(newInstance); await this.send( @@ -292,7 +292,7 @@ export default class HostConnection extends BaseConnection { user.recalculatePlayerStats(); this._controller.userUpdated(user); - let instance = this._controller.instances!.get(instanceId)!; + let instance = this._controller.instances.get(instanceId)!; await lib.invokeHook(this._controller.plugins, "onPlayerEvent", instance, { type: event.type, name: event.name, diff --git a/packages/controller/src/WsServer.ts b/packages/controller/src/WsServer.ts index 333c06fb..bc63a56f 100644 --- a/packages/controller/src/WsServer.ts +++ b/packages/controller/src/WsServer.ts @@ -242,7 +242,7 @@ ${err.stack}` throw new Error("missmatched host id"); } - let host = this.controller.hosts!.get(data.id); + let host = this.controller.hosts.get(data.id); if (!tokenPayload.iat || tokenPayload.iat < (host?.token_valid_after??0)) { throw new Error("invalid token"); } @@ -274,13 +274,13 @@ ${err.stack}` connector.on("close", () => { if (this.hostConnections.get(data.id) === connection) { this.hostConnections.delete(data.id); - this.controller.hostUpdated(this.controller.hosts!.get(data.id) as HostInfo); + this.controller.hostUpdated(this.controller.hosts.get(data.id) as HostInfo); } else { logger.warn("Unlisted HostConnection closed"); } }); this.hostConnections.set(data.id, connection); - this.controller.hostUpdated(this.controller.hosts!.get(data.id) as HostInfo); + this.controller.hostUpdated(this.controller.hosts.get(data.id) as HostInfo); let src = new lib.Address(lib.Address.host, data.id); connector.ready(socket, src, sessionToken, undefined); } diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index 45d5dbc4..708c090b 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -137,7 +137,7 @@ function validateHostToken(req: Request, res: Response, next: any) { if (typeof tokenPayload === "string") { throw new Error("unexpected JsonWebToken type"); } - let host = (req.app.locals.controller as Controller).hosts!.get(tokenPayload.host); + let host = (req.app.locals.controller as Controller).hosts.get(tokenPayload.host); if (!host) { throw new Error("invalid host"); } diff --git a/plugins/inventory_sync/controller.ts b/plugins/inventory_sync/controller.ts index e5d4d8ea..24e83643 100644 --- a/plugins/inventory_sync/controller.ts +++ b/plugins/inventory_sync/controller.ts @@ -84,7 +84,7 @@ export class ControllerPlugin extends BaseControllerPlugin { if ( !acquisitionRecord || acquisitionRecord.instanceId === instanceId - || !this.controller.instances!.has(acquisitionRecord.instanceId) + || !this.controller.instances.has(acquisitionRecord.instanceId) || acquisitionRecord.expires && acquisitionRecord.expires < Date.now() ) { this.acquiredPlayers.set(playerName, { instanceId }); @@ -98,7 +98,7 @@ export class ControllerPlugin extends BaseControllerPlugin { let { instanceId, playerName } = request; if (!this.acquire(instanceId, playerName)) { let acquisitionRecord = this.acquiredPlayers.get(playerName); - let instance = this.controller.instances!.get(acquisitionRecord!.instanceId)!; + let instance = this.controller.instances.get(acquisitionRecord!.instanceId)!; return { status: "busy", message: instance.config.get("instance.name"), @@ -127,7 +127,7 @@ export class ControllerPlugin extends BaseControllerPlugin { async handleUploadRequest(request: msg.UploadRequest) { let { instanceId, playerName, playerData } = request; - let instanceName = this.controller.instances!.get(instanceId)!.config.get("instance.name"); + let instanceName = this.controller.instances.get(instanceId)!.config.get("instance.name"); let store = true; let acquisitionRecord = this.acquiredPlayers.get(playerName); if (!acquisitionRecord) { @@ -162,7 +162,7 @@ export class ControllerPlugin extends BaseControllerPlugin { async handleDownloadRequest(request: msg.DownloadRequest) { let { instanceId, playerName } = request; - let instanceName = this.controller.instances!.get(instanceId)!.config.get("instance.name"); + let instanceName = this.controller.instances.get(instanceId)!.config.get("instance.name"); let acquisitionRecord = this.acquiredPlayers.get(playerName); if (!acquisitionRecord) { diff --git a/plugins/player_auth/controller.ts b/plugins/player_auth/controller.ts index f9863008..81ac1283 100644 --- a/plugins/player_auth/controller.ts +++ b/plugins/player_auth/controller.ts @@ -47,7 +47,7 @@ export class ControllerPlugin extends BaseControllerPlugin { this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => { let servers: string[] = []; - for (let instance of this.controller.instances!.values()) { + for (let instance of this.controller.instances.values()) { if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) { servers.push(instance.config.get("factorio.settings")["name"] as string || "unnamed server"); } diff --git a/plugins/subspace_storage/controller.ts b/plugins/subspace_storage/controller.ts index b08fe064..e0c36f8d 100644 --- a/plugins/subspace_storage/controller.ts +++ b/plugins/subspace_storage/controller.ts @@ -176,7 +176,7 @@ export class ControllerPlugin extends BaseControllerPlugin { } } else { - let instance = this.controller.instances!.get(instanceId); + let instance = this.controller.instances.get(instanceId); let instanceName = instance ? instance.config.get("instance.name") : "unkonwn"; // use fancy neural net to calculate a "fair" dole division rate.