From 6ffcb8c4513b613040773ee468c58614a05088fc Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 20 May 2021 21:22:01 +0200 Subject: [PATCH] Add instance update subscription Provide the control link with the ability to subscribe to updates when instance status and properties changes. This allows the web interface to respond in real time to instance starts and stops that are triggered from somewhere else. --- packages/ctl/ctl.js | 2 + packages/lib/link/messages.js | 49 +++++++++++++--- packages/lib/users.js | 12 ++++ packages/master/src/ControlConnection.js | 41 ++++++++++++- packages/master/src/Master.js | 20 ++++++- packages/master/src/SlaveConnection.js | 4 +- .../src/components/InstanceViewPage.jsx | 11 +--- packages/web_ui/src/model/instance.jsx | 24 +++++--- packages/web_ui/src/util/websocket.jsx | 58 +++++++++++++++++++ test/integration/clusterio.js | 16 +++++ test/integration/index.js | 11 ++++ 11 files changed, 219 insertions(+), 29 deletions(-) diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 20243a75..b4051db2 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -748,6 +748,8 @@ class Control extends libLink.Link { this.keepOpen = false; } + async instanceUpdateEventHandler() { } + async setLogSubscriptions({ all = false, master = false, diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index e4aedcb6..8586ce39 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -353,6 +353,26 @@ messages.listSlaves = new Request({ }, }); +let instanceProperties = { + "name": { type: "string" }, + "id": { type: "integer" }, + "assigned_slave": { type: ["null", "integer"] }, + "status": { enum: [ + "unknown", "unassigned", "stopped", "starting", "running", "stopping", + "creating_save", "exporting_data", + ]}, +}; + +messages.getInstance = new Request({ + type: "get_instance", + links: ["control-master"], + permission: "core.instance.get", + requestProperties: { + "id": { type: "integer" }, + }, + responseProperties: instanceProperties, +}); + messages.listInstances = new Request({ type: "list_instances", links: ["control-master"], @@ -363,20 +383,25 @@ messages.listInstances = new Request({ items: { additionalProperties: false, required: ["name", "id", "assigned_slave", "status"], - properties: { - "name": { type: "string" }, - "id": { type: "integer" }, - "assigned_slave": { type: ["null", "integer"] }, - "status": { enum: [ - "unknown", "unassigned", "stopped", "starting", "running", "stopping", - "creating_save", "exporting_data", - ]}, - }, + properties: instanceProperties, }, }, }, }); +messages.setInstanceSubscriptions = new Request({ + type: "set_instance_subscriptions", + links: ["control-master"], + permission: "core.instance.subscribe", + requestProperties: { + "all": { type: "boolean" }, + "instance_ids": { + type: "array", + items: { type: "integer" }, + }, + }, +}); + messages.generateSlaveToken = new Request({ type: "generate_slave_token", links: ["control-master"], @@ -985,6 +1010,12 @@ messages.instanceStatusChanged = new Event({ }, }); +messages.instanceUpdate = new Event({ + type: "instance_update", + links: ["master-control"], + eventProperties: instanceProperties, +}); + messages.masterConnectionEvent = new Event({ type: "master_connection_event", links: ["slave-instance"], diff --git a/packages/lib/users.js b/packages/lib/users.js index dd9e0930..3ba6f39e 100644 --- a/packages/lib/users.js +++ b/packages/lib/users.js @@ -333,12 +333,24 @@ definePermission({ description: "Create slave configs via the master server.", }); +definePermission({ + name: "core.instance.get", + title: "Get instance", + description: "Get the details of an instance in the cluster.", + grantByDefault: true, +}); definePermission({ name: "core.instance.list", title: "List instances", description: "Get the full list of instances in the cluster.", grantByDefault: true, }); +definePermission({ + name: "core.instance.subscribe", + title: "Subscribe to instance updates", + description: "Subscribe to be notified on updates on the details and status of instances.", + grantByDefault: true, +}); definePermission({ name: "core.instance.create", title: "Create instance", diff --git a/packages/master/src/ControlConnection.js b/packages/master/src/ControlConnection.js index a592a6ce..c9ad29b3 100644 --- a/packages/master/src/ControlConnection.js +++ b/packages/master/src/ControlConnection.js @@ -38,6 +38,11 @@ class ControlConnection extends BaseConnection { */ this.user = user; + this.instanceSubscriptions = { + all: false, + instance_ids: [], + }; + this.logTransport = null; this.logSubscriptions = { all: false, @@ -122,6 +127,21 @@ class ControlConnection extends BaseConnection { return { serialized_config: slaveConfig.serialize() }; } + async getInstanceRequestHandler(message) { + let id = message.data.id; + let instance = this._master.instances.get(id); + if (!instance) { + throw new libErrors.RequestError(`Instance with ID ${id} does not exist`); + } + + return { + id, + name: instance.config.get("instance.name"), + assigned_slave: instance.config.get("instance.assigned_slave"), + status: instance.status, + }; + } + async listInstancesRequestHandler(message) { let list = []; for (let instance of this._master.instances.values()) { @@ -135,6 +155,24 @@ class ControlConnection extends BaseConnection { return { list }; } + async setInstanceSubscriptionsRequestHandler(message) { + this.instanceSubscriptions = message.data; + } + + instanceUpdated(instance) { + if ( + this.instanceSubscriptions.all + || this.instanceSubscriptions.instance_ids.includes(instance.config.get("instance.id")) + ) { + libLink.messages.instanceUpdate.send(this, { + id: instance.config.get("instance.id"), + name: instance.config.get("instance.name"), + assigned_slave: instance.config.get("instance.assigned_slave"), + status: instance.status, + }); + } + } + // XXX should probably add a hook for slave reuqests? async createInstanceRequestHandler(message) { let instanceConfig = new libConfig.InstanceConfig("master"); @@ -174,7 +212,7 @@ class ControlConnection extends BaseConnection { let instance = { config: instanceConfig, status: "unassigned" }; this._master.instances.set(instanceId, instance); await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, null); - this._master.addInstancePluginHooks(instance); + this._master.addInstanceHooks(instance); } async deleteInstanceRequestHandler(message, request) { @@ -190,6 +228,7 @@ class ControlConnection extends BaseConnection { let prev = instance.status; instance.status = "deleted"; + this.instanceUpdated(instance); await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev); } diff --git a/packages/master/src/Master.js b/packages/master/src/Master.js index 412fb76b..644b404b 100644 --- a/packages/master/src/Master.js +++ b/packages/master/src/Master.js @@ -152,7 +152,7 @@ class Master { await this.userManager.load(path.join(databaseDirectory, "users.json")); for (let instance of this.instances.values()) { - this.addInstancePluginHooks(instance); + this.addInstanceHooks(instance); } // Make sure we're actually going to listen on a port @@ -361,10 +361,26 @@ class Master { } } - addInstancePluginHooks(instance) { + addInstanceHooks(instance) { instance.config.on("fieldChanged", (group, field, prev) => { + if (group.name === "instance" && field === "name") { + this.instanceUpdated(instance); + } + libPlugin.invokeHook(this.plugins, "onInstanceConfigFieldChanged", instance, group, field, prev); }); + + this.instanceUpdated(instance); + } + + instanceUpdated(instance) { + for (let controlConnection of this.wsServer.controlConnections) { + if (controlConnection.connector.closing) { + continue; + } + + controlConnection.instanceUpdated(instance); + } } async loadPlugins() { diff --git a/packages/master/src/SlaveConnection.js b/packages/master/src/SlaveConnection.js index 9bbdfd4e..19829d0f 100644 --- a/packages/master/src/SlaveConnection.js +++ b/packages/master/src/SlaveConnection.js @@ -65,6 +65,7 @@ class SlaveConnection extends BaseConnection { let prev = instance.status; instance.status = message.data.status; logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`); + this._master.instanceUpdated(instance); await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", instance, prev); } @@ -99,6 +100,7 @@ class SlaveConnection extends BaseConnection { let prev = masterInstance.status; masterInstance.status = instanceData.status; logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`); + this._master.instanceUpdated(instance); await libPlugin.invokeHook(this._master.plugins, "onInstanceStatusChanged", masterInstance, prev); } continue; @@ -107,7 +109,7 @@ class SlaveConnection extends BaseConnection { instanceConfig.set("instance.assigned_slave", this._id); let newInstance = { config: instanceConfig, status: instanceData.status }; this._master.instances.set(instanceConfig.get("instance.id"), newInstance); - this._master.addInstancePluginHooks(newInstance); + this._master.addInstanceHooks(newInstance); await libLink.messages.assignInstance.send(this, { instance_id: instanceConfig.get("instance.id"), serialized_config: instanceConfig.serialize("slave"), diff --git a/packages/web_ui/src/components/InstanceViewPage.jsx b/packages/web_ui/src/components/InstanceViewPage.jsx index f44b5c34..cc0a55d2 100644 --- a/packages/web_ui/src/components/InstanceViewPage.jsx +++ b/packages/web_ui/src/components/InstanceViewPage.jsx @@ -26,7 +26,7 @@ export default function InstanceViewPage(props) { let history = useHistory(); let control = useContext(ControlContext); - let [instance, updateInstance] = useInstance(instanceId); + let [instance] = useInstance(instanceId); let [slave] = useSlave(Number(instance["assigned_slave"])); let [creatingSave, setCreatingSave] = useState(false); @@ -44,10 +44,7 @@ export default function InstanceViewPage(props) { } let instanceButtons = - + {instance.status === "stopped" &&