From f33848e16b6fa0ebaf0432e1dbd7fa3d1cded965 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 29 Jun 2022 10:27:44 +0100 Subject: [PATCH] Rework start/stop api to use runtime-event notification message --- .../@node-red/editor-api/lib/admin/flows.js | 2 +- .../editor-client/locales/en-US/editor.json | 10 +- .../@node-red/editor-client/src/js/nodes.js | 2 +- .../@node-red/editor-client/src/js/red.js | 7 +- .../@node-red/editor-client/src/js/runtime.js | 67 +++++-------- .../editor-client/src/js/ui/deploy.js | 23 ++--- .../@node-red/editor-client/src/js/ui/view.js | 2 +- .../@node-red/runtime/lib/api/flows.js | 31 +++--- .../@node-red/runtime/lib/flows/index.js | 96 ++++++++++--------- .../@node-red/runtime/lib/index.js | 6 +- .../runtime/locales/en-US/runtime.json | 1 + 11 files changed, 107 insertions(+), 140 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js index 2ad233f8f..4d8679aac 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/flows.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/flows.js @@ -83,7 +83,7 @@ module.exports = { postState: function(req,res) { const opts = { user: req.user, - requestedState: req.body.state||"", + state: req.body.state || "", req: apiUtils.getRequestLogObject(req) } runtimeAPI.flows.setState(opts).then(function(result) { diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index b25885e21..ab2fba982 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -169,6 +169,10 @@ } }, "notification": { + "state": { + "flowsStopped": "Flows stopped", + "flowsStarted": "Flows started" + }, "warning": "Warning: __message__", "warnings": { "undeployedChanges": "node has undeployed changes", @@ -291,12 +295,6 @@ "stopstart":{ "status": { "state_changed": "Flows runtime has been changed to '__state__' state" - }, - "errors": { - "notAllowed": "Method not allowed", - "notAuthorized": "Not authorized", - "notFound": "Not found", - "noResponse": "No response from server" } }, "deploy": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 052fad558..b390dab53 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -14,7 +14,7 @@ * limitations under the License. **/ -/** +/** * An Interface to nodes and utility functions for creating/adding/deleting nodes and links * @namespace RED.nodes */ diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 939c32f2e..55446418b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -336,7 +336,6 @@ var RED = (function() { id: notificationId } if (notificationId === "runtime-state") { - RED.events.emit("runtime-state",msg); if (msg.error === "safe-mode") { options.buttons = [ { @@ -477,9 +476,9 @@ var RED = (function() { } else if (persistentNotifications.hasOwnProperty(notificationId)) { persistentNotifications[notificationId].close(); delete persistentNotifications[notificationId]; - if (notificationId === 'runtime-state') { - RED.events.emit("runtime-state",msg); - } + } + if (notificationId === 'runtime-state') { + RED.events.emit("runtime-state",msg); } }); RED.comms.subscribe("status/#",function(topic,msg) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/runtime.js b/packages/node_modules/@node-red/editor-client/src/js/runtime.js index 49960e382..eac985467 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/runtime.js +++ b/packages/node_modules/@node-red/editor-client/src/js/runtime.js @@ -1,53 +1,36 @@ RED.runtime = (function() { let state = "" - let settings = {ui: false, enabled: false}; - const STOPPED = "stopped" - const STARTED = "started" + let settings = { ui: false, enabled: false }; + const STOPPED = "stop" + const STARTED = "start" + const SAFE = "safe" + return { init: function() { // refresh the current runtime status from server settings = Object.assign({}, settings, RED.settings.runtimeState); - RED.runtime.requestState() - - // {id:"flows-run-state", started: false, state: "stopped", retain:true} - RED.comms.subscribe("notification/flows-run-state",function(topic,msg) { - RED.events.emit("flows-run-state",msg); - RED.runtime.updateState(msg.state); - }); - }, - get state() { - return state - }, - get started() { - return state === STARTED - }, - get states() { - return { STOPPED, STARTED } - }, - updateState: function(newState) { - state = newState; - // disable pointer events on node buttons (e.g. inject/debug nodes) - $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state === STOPPED) - // show/hide Start/Stop based on current state - if(settings.enabled === true && settings.ui === true) { - RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED) - RED.menu.setVisible("deploymenu-item-runtime-start", state === STOPPED) - } - }, - requestState: function(callback) { - $.ajax({ - headers: { - "Accept":"application/json" - }, - cache: false, - url: 'flows/state', - success: function(data) { - RED.runtime.updateState(data.state) - if(callback) { - callback(data.state) + RED.events.on("runtime-state", function(msg) { + if (msg.state) { + const currentState = state + state = msg.state + $(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state !== STARTED) + if(settings.enabled === true && settings.ui === true) { + RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED) + RED.menu.setVisible("deploymenu-item-runtime-start", state !== STARTED) + } + // Do not notify the user about this event if: + // - This is the very first event we've received after loading the editor (currentState = '') + // - The state matches what we already thought was the case (state === currentState) + // - The event was triggered by a deploy (msg.deploy === true) + // - The event is a safe mode event - that gets notified separately + if (currentState !== '' && state !== currentState && !msg.deploy && state !== SAFE) { + RED.notify(RED._("notification.state.flows"+(state === STOPPED?'Stopped':'Started'), msg), "success") } } }); + }, + get started() { + return state === STARTED } } -})() \ No newline at end of file +})() diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index 46408cb6c..809202c99 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -69,7 +69,7 @@ RED.deploy = (function() { {id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}}, null ] - if(RED.settings.runtimeState && RED.settings.runtimeState.ui === true) { + if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) { mainMenuItems.push({id:"deploymenu-item-runtime-start", icon:"red/images/start.svg",label:"Start"/*RED._("deploy.startFlows")*/,sublabel:"Start Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:start-flows", visible:false}) mainMenuItems.push({id:"deploymenu-item-runtime-stop", icon:"red/images/stop.svg",label:"Stop"/*RED._("deploy.startFlows")*/,sublabel:"Stop Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:stop-flows", visible:false}) } @@ -302,7 +302,6 @@ RED.deploy = (function() { deployInflight = true deployButtonSetBusy() shadeShow() - RED.runtime.updateState(state) $.ajax({ url:"flows/state", type: "POST", @@ -311,30 +310,23 @@ RED.deploy = (function() { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled") } - RED.runtime.updateState((data && data.state) || "unknown") - RED.notify(RED._("stopstart.status.state_changed", data), "success") }).fail(function(xhr,textStatus,err) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled") } if (xhr.status === 401) { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notAuthorized") }), "error") - } else if (xhr.status === 404) { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notFound") }), "error") - } else if (xhr.status === 405) { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.notAllowed") }), "error") + RED.notify(RED._("notification.error", { message: RED._("user.notAuthorized") }), "error") } else if (xhr.responseText) { const errorDetail = { message: err ? (err + "") : "" } try { errorDetail.message = JSON.parse(xhr.responseText).message - } finally { + } finally { errorDetail.message = errorDetail.message || xhr.responseText } RED.notify(RED._("notification.error", errorDetail), "error") } else { - RED.notify(RED._("notification.error", { message: RED._("stopstart.errors.noResponse") }), "error") + RED.notify(RED._("notification.error", { message: RED._("deploy.errors.noResponse") }), "error") } - RED.runtime.requestState() }).always(function() { const delta = Math.max(0, 300 - (Date.now() - startTime)) setTimeout(function () { @@ -514,13 +506,10 @@ RED.deploy = (function() { deployButtonSetBusy(); const data = { flows: nns }; - data.runtimeState = RED.runtime.state; - if (data.runtimeState === RED.runtime.states.STOPPED || force) { - data._rev = RED.nodes.version(); - } else { + if (!force) { data.rev = RED.nodes.version(); } - + deployInflight = true; shadeShow(); $.ajax({ diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index eca5c01ef..02b8df5d8 100755 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -4877,7 +4877,7 @@ RED.view = (function() { if (d._def.button) { var buttonEnabled = isButtonEnabled(d); this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled); - if(RED.runtime && Object.hasOwn(RED.runtime,'started')) { + if (RED.runtime && Object.hasOwn(RED.runtime,'started')) { this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started); } diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index 2e71cbdca..b91635201 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -73,10 +73,6 @@ var api = module.exports = { if (deploymentType === 'reload') { apiPromise = runtime.flows.loadFlows(true); } else { - //ensure the runtime running/stopped state matches the deploying editor. If not, then copy the _rev number to flows.rev - if(flows.hasOwnProperty('_rev') && !flows.hasOwnProperty('rev') && (flows.runtimeState !== "stopped" || runtime.flows.started)) { - flows.rev = flows._rev - } if (flows.hasOwnProperty('rev')) { var currentVersion = runtime.flows.getFlows().rev; if (currentVersion !== flows.rev) { @@ -271,9 +267,7 @@ var api = module.exports = { getState: async function(opts) { runtime.log.audit({event: "flows.getState"}, opts.req); const result = { - state: runtime.flows.started ? "started" : "stopped", - started: !!runtime.flows.started, - rev: runtime.flows.getFlows().rev + state: runtime.flows.state() } return result; }, @@ -282,7 +276,7 @@ var api = module.exports = { * @param {Object} opts * @param {Object} opts.req - the request to log (optional) * @param {User} opts.user - the user calling the api - * @param {string} opts.requestedState - the requested state. Valid values are "start" and "stop". + * @param {string} opts.state - the requested state. Valid values are "start" and "stop". * @return {Promise} - the active flow configuration * @memberof @node-red/runtime_flows */ @@ -295,7 +289,7 @@ var api = module.exports = { err.code = err.code || errcode || "unexpected_error" runtime.log.audit({ event: "flows.setState", - state: opts.requestedState || "", + state: opts.state || "", error: errcode || "unexpected_error", message: err.code }, opts.req); @@ -304,21 +298,22 @@ var api = module.exports = { const getState = () => { return { - state: runtime.flows.started ? "started" : "stopped", - started: !!runtime.flows.started, - rev: runtime.flows.getFlows().rev, + state: runtime.flows.state() } } if(!runtime.settings.runtimeState || runtime.settings.runtimeState.enabled !== true) { throw (makeError("Method Not Allowed", "not_allowed", 405)) } - switch (opts.requestedState) { + switch (opts.state) { case "start": try { try { - runtime.settings.set('flowsRunStateRequested', opts.requestedState); - } catch(err) { } + runtime.settings.set('runtimeFlowState', opts.state); + } catch(err) {} + if (runtime.settings.safeMode) { + delete runtime.settings.safeMode + } await runtime.flows.startFlows("full") return getState() } catch (err) { @@ -327,15 +322,15 @@ var api = module.exports = { case "stop": try { try { - runtime.settings.set('flowsRunStateRequested', opts.requestedState); - } catch(err) { } + runtime.settings.set('runtimeFlowState', opts.state); + } catch(err) {} await runtime.flows.stopFlows("full") return getState() } catch (err) { throw (makeError(err, err.code, 500)) } default: - throw (makeError(`Cannot change flows runtime state to '${opts.requestedState}'}`, "invalid_run_state", 400)) + throw (makeError(`Cannot change flows runtime state to '${opts.state}'}`, "invalid_run_state", 400)) } }, } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index b707b4aaf..b2de8ea73 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -36,6 +36,8 @@ var activeFlowConfig = null; var activeFlows = {}; var started = false; +var state = 'stop' + var credentialsPendingReset = false; var activeNodesToFlow = {}; @@ -50,6 +52,7 @@ function init(runtime) { storage = runtime.storage; log = runtime.log; started = false; + state = 'stop'; if (!typeEventRegistered) { events.on('type-registered',function(type) { if (activeFlowConfig && activeFlowConfig.missingTypes.length > 0) { @@ -214,19 +217,26 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) { // Flows are running (or should be) // Stop the active flows (according to deploy type and the diff) - return stop(type,diff,muteLog).then(() => { + return stop(type,diff,muteLog,true).then(() => { // Once stopped, allow context to remove anything no longer needed return context.clean(activeFlowConfig) }).then(() => { + if (!isLoad) { + log.info(log._("nodes.flows.updated-flows")); + } // Start the active flows - start(type,diff,muteLog).then(() => { + start(type,diff,muteLog,true).then(() => { events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); }); // Return the new revision asynchronously to the actual start return flowRevision; }).catch(function(err) { }) } else { + if (!isLoad) { + log.info(log._("nodes.flows.updated-flows")); + } events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); + return flowRevision; } }); } @@ -259,10 +269,10 @@ function getFlows() { return activeConfig; } -async function start(type,diff,muteLog) { - type = type||"full"; - let reallyStarted = started +async function start(type,diff,muteLog,isDeploy) { + type = type || "full"; started = true; + state = 'start' var i; // If there are missing types, report them, emit the necessary runtime event and return if (activeFlowConfig.missingTypes.length > 0) { @@ -284,7 +294,7 @@ async function start(type,diff,muteLog) { log.info(log._("nodes.flows.missing-type-install-2")); log.info(" "+settings.userDir); } - events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true}); + events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true}); return; } @@ -298,7 +308,7 @@ async function start(type,diff,muteLog) { missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()}) log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`); } - events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true}); + events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true}); return; } @@ -307,10 +317,20 @@ async function start(type,diff,muteLog) { log.info("*****************************************************************") log.info(log._("nodes.flows.safe-mode")); log.info("*****************************************************************") - events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true}); + state = 'safe' + events.emit("runtime-event",{id:"runtime-state",payload:{state: 'safe', error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true}); return; } + const runtimeState = settings.get('runtimeFlowState') || 'start' + if (runtimeState === 'stop') { + log.info(log._("nodes.flows.stopped-flows")); + events.emit("runtime-event",{id:"runtime-state",payload:{ state: 'stop', deploy:isDeploy },retain:true}); + state = 'stop' + started = false + return + } + if (!muteLog) { if (type !== "full") { log.info(log._("nodes.flows.starting-modified-"+type)); @@ -365,51 +385,31 @@ async function start(type,diff,muteLog) { } } } - // Having created or updated all flows, now start them. - let startFlows = true - try { - startFlows = settings.get('flowsRunStateRequested'); - } catch(err) { - } - startFlows = (startFlows !== "stop"); - - if (startFlows) { - for (id in activeFlows) { - if (activeFlows.hasOwnProperty(id)) { - try { - activeFlows[id].start(diff); - // Create a map of node id to flow id and also a subflowInstance lookup map - var activeNodes = activeFlows[id].getActiveNodes(); - Object.keys(activeNodes).forEach(function(nid) { - activeNodesToFlow[nid] = id; - }); - } catch(err) { - console.log(err.stack); - } + for (id in activeFlows) { + if (activeFlows.hasOwnProperty(id)) { + try { + activeFlows[id].start(diff); + // Create a map of node id to flow id and also a subflowInstance lookup map + var activeNodes = activeFlows[id].getActiveNodes(); + Object.keys(activeNodes).forEach(function(nid) { + activeNodesToFlow[nid] = id; + }); + } catch(err) { + console.log(err.stack); } } - reallyStarted = true; - events.emit("flows:started", {config: activeConfig, type: type, diff: diff}); - // Deprecated event - events.emit("nodes-started"); - } else { - started = false; } - - const state = { - started: reallyStarted, - state: reallyStarted ? "started" : "stopped", - } - events.emit("runtime-event",{id:"flows-run-state", payload: state, retain:true}); - + events.emit("flows:started", {config: activeConfig, type: type, diff: diff}); + // Deprecated event + events.emit("nodes-started"); if (credentialsPendingReset === true) { credentialsPendingReset = false; } else { - events.emit("runtime-event",{id:"runtime-state",retain:true}); + events.emit("runtime-event",{id:"runtime-state", payload:{ state: 'start', deploy:isDeploy}, retain:true}); } - if (!muteLog && reallyStarted) { + if (!muteLog) { if (type !== "full") { log.info(log._("nodes.flows.started-modified-"+type)); } else { @@ -419,7 +419,7 @@ async function start(type,diff,muteLog) { return; } -function stop(type,diff,muteLog) { +function stop(type,diff,muteLog,isDeploy) { if (!started) { return Promise.resolve(); } @@ -439,6 +439,7 @@ function stop(type,diff,muteLog) { } } started = false; + state = 'stop' var promises = []; var stopList; var removedList = diff.removed; @@ -490,7 +491,8 @@ function stop(type,diff,muteLog) { } } events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff}); - events.emit("runtime-event",{id:"flows-run-state", payload: {started: false, state: "stopped"}, retain:true}); + + events.emit("runtime-event",{ id:"runtime-state", payload:{ state: 'stop', deploy:isDeploy }, retain:true }); // Deprecated event events.emit("nodes-stopped"); }); @@ -810,7 +812,7 @@ module.exports = { stopFlows: stop, get started() { return started }, - + state: () => { return state }, // handleError: handleError, // handleStatus: handleStatus, diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 8e1d2b487..88b3b8293 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -215,7 +215,7 @@ function start() { } } return redNodes.loadContextsPlugin().then(function () { - redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {}); + redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {}); started = true; }); }); @@ -399,12 +399,12 @@ module.exports = { * @memberof @node-red/runtime */ version: externalAPI.version, - + /** * @memberof @node-red/diagnostics */ diagnostics:externalAPI.diagnostics, - + storage: storage, events: events, hooks: hooks, diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index 3b46d0c9f..ecd010abb 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -122,6 +122,7 @@ "stopped-flows": "Stopped flows", "stopped": "Stopped", "stopping-error": "Error stopping node: __message__", + "updated-flows": "Updated flows", "added-flow": "Adding flow: __label__", "updated-flow": "Updated flow: __label__", "removed-flow": "Removed flow: __label__",