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.