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" &&