mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Revoking a host now unassigns its instances instead of being refused while it has any, and removes its system metrics. Approving and revoking no longer force a save. Hosts are saved by autosave and on exit like everything else, which also removes the approving state from pairing requests. The approval code and link are logged as warnings so they show up in the systemd service, which only prints warnings and above. The pairing limits are now the controller.host_pairing_timeout and controller.host_pairing_max_pending config values, 0 removes the limit. Subscribers take an optional permitted function, used for the pairing requests instead of overrides in a subclass.
285 lines
6.7 KiB
JavaScript
285 lines
6.7 KiB
JavaScript
import events from "node:events";
|
|
import http from "node:http";
|
|
import express from "express";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import { User, UserManager, UserRecord, InstanceManager, ControllerHooks } from "@clusterio/controller";
|
|
import { HostHooks, InstanceHooks } from "@clusterio/host";
|
|
|
|
const addr = lib.Address.fromShorthand;
|
|
|
|
export class MockLogger {
|
|
child() { return this; }
|
|
|
|
fatal() { }
|
|
|
|
error() { }
|
|
|
|
warn() { }
|
|
|
|
audit() { }
|
|
|
|
info() { }
|
|
|
|
server() { }
|
|
|
|
verbose() { }
|
|
}
|
|
|
|
export class MockSocket {
|
|
constructor() {
|
|
this.sentMessages = [];
|
|
this.events = new Map();
|
|
}
|
|
|
|
send(message) {
|
|
this.sentMessages.push(message);
|
|
}
|
|
|
|
on(event, fn) {
|
|
this.events.set(event, fn);
|
|
}
|
|
|
|
/* eslint-disable accessor-pairs */
|
|
set onclose(fn) {
|
|
this.on("close", (code, reason) => fn({ code, reason }));
|
|
}
|
|
|
|
set onerror(fn) {
|
|
this.on("error", fn);
|
|
}
|
|
|
|
set onopen(fn) {
|
|
this.on("open", fn);
|
|
}
|
|
|
|
set onmessage(fn) {
|
|
this.on("message", (data) => fn({ data }));
|
|
}
|
|
|
|
terminate() {
|
|
this.terminateCalled = true;
|
|
}
|
|
|
|
close() {
|
|
this.closeCalled = true;
|
|
}
|
|
}
|
|
|
|
export class MockConnector extends lib.BaseConnector {
|
|
constructor(src, dst) {
|
|
super(src, dst);
|
|
|
|
this.valid = true;
|
|
this.connected = true;
|
|
this.hasSession = true;
|
|
this.sentMessages = [];
|
|
}
|
|
|
|
send(message) {
|
|
this.sentMessages.push(message);
|
|
setImmediate(() => this.emit("send", message));
|
|
}
|
|
|
|
sendDisconnectReady() {
|
|
this.send(new lib.MessageDisconnect("ready"));
|
|
}
|
|
}
|
|
|
|
export class MockServer extends events.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
this.rconCommands = [];
|
|
this.rconCommandResults = new Map();
|
|
this.udpMessages = [];
|
|
}
|
|
|
|
async sendUdp(message) {
|
|
this.udpMessages.push(message);
|
|
}
|
|
|
|
async sendRcon(command) {
|
|
this.rconCommands.push(command);
|
|
let result = this.rconCommandResults.get(command);
|
|
if (!result) {
|
|
result = { response: "" };
|
|
}
|
|
if (result instanceof Error || typeof result === "string") {
|
|
result = { response: result };
|
|
}
|
|
if (result.time) {
|
|
await lib.wait(result.time);
|
|
}
|
|
if (result.response instanceof Error) {
|
|
throw result.response;
|
|
}
|
|
return result.response || "";
|
|
}
|
|
}
|
|
|
|
export class MockInstance extends lib.Link {
|
|
constructor() {
|
|
super(new MockConnector(addr({ instanceId: 7357 }), addr({ hostId: 1 })));
|
|
this.logger = new MockLogger();
|
|
this.server = new MockServer();
|
|
this.name = "test";
|
|
this.id = 7357;
|
|
this.status = "running";
|
|
this.autoPaused = false;
|
|
this.hooks = new InstanceHooks(this.logger);
|
|
this.loadedPlugins = new Set();
|
|
this.mockConfigEntries = new Map([
|
|
["instance.id", 7357],
|
|
["factorio.enable_save_patching", true],
|
|
["factorio.enable_script_commands", true],
|
|
]);
|
|
this.config = {
|
|
get: (name) => {
|
|
if (this.mockConfigEntries.has(name)) {
|
|
return this.mockConfigEntries.get(name);
|
|
}
|
|
throw Error(`mock for field ${name} is not implemented`);
|
|
},
|
|
};
|
|
}
|
|
|
|
async sendRcon(command, expectEmpty, plugin) {
|
|
return await this.server.sendRcon(command);
|
|
}
|
|
|
|
async sendUdp(message) {
|
|
await this.server.sendUdp(message);
|
|
}
|
|
}
|
|
|
|
export class MockHost extends lib.Link {
|
|
constructor() {
|
|
super(new MockConnector(addr({ hostId: 1 }), addr("controller")));
|
|
this.hooks = new HostHooks(new MockLogger());
|
|
this.loadedPlugins = new Set();
|
|
}
|
|
}
|
|
|
|
export class MockControl extends lib.Link { }
|
|
|
|
export class MockController {
|
|
constructor() {
|
|
this.app = express();
|
|
this.app.locals.controller = this;
|
|
this.app.locals.streams = new Map();
|
|
this.mockConfigEntries = new Map([
|
|
["controller.public_url", "test"],
|
|
["controller.auth_secret", "TestSecretDoNotUse"],
|
|
["controller.proxy_stream_timeout", 1],
|
|
["controller.metrics_timeout", 1],
|
|
["controller.host_pairing_timeout", 3600],
|
|
["controller.host_pairing_max_pending", 50],
|
|
]);
|
|
this.config = {
|
|
get: (name) => {
|
|
if (this.mockConfigEntries.has(name)) {
|
|
return this.mockConfigEntries.get(name);
|
|
}
|
|
throw Error(`mock for field ${name} is not implemented`);
|
|
},
|
|
};
|
|
|
|
const roleProvider = new lib.MemoryDatastoreProvider(new Map([
|
|
[0, lib.Role.fromJSON({ id: 0, name: "Admin", description: "admin", permissions: ["core.admin"] })],
|
|
[1, lib.Role.fromJSON({ id: 1, name: "Player", description: "player", permissions: [] })],
|
|
]));
|
|
|
|
this.roles = new lib.SubscribableDatastore(roleProvider, roleProvider.value);
|
|
this.roles.get(1).grantDefaultPermissions(); // Todo replace "core.admin" above with grantAdminPermissions
|
|
|
|
const userProvider = new lib.MemoryDatastoreProvider(new Map([
|
|
["test", UserRecord.fromJSON({ name: "test", roles: [0, 1] })],
|
|
["player", UserRecord.fromJSON({ name: "player", roles: [1] })],
|
|
]));
|
|
|
|
this.users = new UserManager(
|
|
new lib.SubscribableDatastore(userProvider, userProvider.value),
|
|
this.roles, this.config
|
|
);
|
|
|
|
this.instances = new InstanceManager(new lib.KeyValueDatastore(), this);
|
|
|
|
this.hosts = new lib.KeyValueDatastore();
|
|
this.handles = new Map();
|
|
this.hooks = new ControllerHooks(new MockLogger());
|
|
this.loadedPlugins = new Set();
|
|
this.wsServer = { hostConnections: new Map() };
|
|
}
|
|
|
|
get authSecret() {
|
|
return Buffer.from(this.config.get("controller.auth_secret"), "base64");
|
|
}
|
|
|
|
handle(eventClass, handler) {
|
|
this.handles.set(eventClass, handler);
|
|
}
|
|
|
|
deleteNote() { }
|
|
|
|
getControllerUrl() {
|
|
return "http://controller.example/";
|
|
}
|
|
|
|
async startServer() {
|
|
this.server = http.createServer(this.app);
|
|
await new Promise(resolve => {
|
|
this.server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
let address = this.server.address();
|
|
return `http://127.0.0.1:${address.port}`;
|
|
}
|
|
|
|
async stopServer() {
|
|
if (this.server) {
|
|
this.server.close();
|
|
this.server.unref();
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createControllerPlugin(ControllerPluginClass, info) {
|
|
let controller = new MockController();
|
|
let metrics = {};
|
|
let logger = new MockLogger();
|
|
let plugin = new ControllerPluginClass(info, controller, metrics, logger);
|
|
await plugin.init();
|
|
return plugin;
|
|
}
|
|
|
|
export async function createInstancePlugin(InstancePluginClass, info) {
|
|
let instance = new MockInstance();
|
|
let host = new MockHost();
|
|
let plugin = new InstancePluginClass(info, instance, host, new MockLogger());
|
|
await plugin.init();
|
|
return plugin;
|
|
}
|
|
|
|
export async function loadControllerPlugin(entrypoint, info) {
|
|
const controller = new MockController();
|
|
await entrypoint({ controller, metrics: {}, logger: new MockLogger(), plugin: info });
|
|
return controller;
|
|
}
|
|
|
|
export async function loadInstancePlugin(entrypoint, info) {
|
|
const instance = new MockInstance();
|
|
const host = new MockHost();
|
|
await entrypoint({ instance, host, logger: new MockLogger(), plugin: info });
|
|
return { instance, host };
|
|
}
|
|
|
|
export function getHandler(link, Class) {
|
|
if (link instanceof MockController) {
|
|
return link.handles.get(Class);
|
|
}
|
|
return link._requestHandlers.get(Class) ?? link._eventHandlers.get(Class);
|
|
}
|