@node-red/runtime 4.0.8 → 4.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,7 +96,11 @@ var api = module.exports = {
96
96
  } else if (scope === 'node') {
97
97
  var node = runtime.nodes.getNode(id);
98
98
  if (node) {
99
- ctx = node.context();
99
+ if (/^subflow:/.test(node.type)) {
100
+ ctx = runtime.nodes.getContext(node.id);
101
+ } else {
102
+ ctx = node.context();
103
+ }
100
104
  }
101
105
  }
102
106
  if (ctx) {
@@ -104,13 +108,25 @@ var api = module.exports = {
104
108
  store = store || availableStores.default;
105
109
  ctx.get(key,store,function(err, v) {
106
110
  if (opts.keysOnly) {
111
+ const result = {}
107
112
  if (Array.isArray(v)) {
108
- resolve({ [store]: { format: `array[${v.length}]`}})
113
+ result.format = `array[${v.length}]`
109
114
  } else if (typeof v === 'object') {
110
- resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
115
+ result.keys = Object.keys(v).map(k => {
116
+ if (Array.isArray(v[k])) {
117
+ return { key: k, format: `array[${v[k].length}]`, length: v[k].length }
118
+ } else if (typeof v[k] === 'object') {
119
+ return { key: k, format: 'object' }
120
+ } else {
121
+ return { key: k }
122
+ }
123
+ })
124
+ result.format = 'object'
111
125
  } else {
112
- resolve({ [store]: { keys: [] }})
126
+ result.keys = []
113
127
  }
128
+ resolve({ [store]: result })
129
+ return
114
130
  }
115
131
  var encoded = util.encodeObject({msg:v});
116
132
  if (store !== availableStores.default) {
@@ -147,7 +163,7 @@ var api = module.exports = {
147
163
  }
148
164
  return
149
165
  }
150
- result[store] = { keys }
166
+ result[store] = { keys: keys.map(key => { return { key }}) }
151
167
  c--;
152
168
  if (c === 0) {
153
169
  if (!errorReported) {
@@ -225,7 +241,11 @@ var api = module.exports = {
225
241
  } else if (scope === 'node') {
226
242
  var node = runtime.nodes.getNode(id);
227
243
  if (node) {
228
- ctx = node.context();
244
+ if (/^subflow:/.test(node.type)) {
245
+ ctx = runtime.nodes.getContext(node.id);
246
+ } else {
247
+ ctx = node.context();
248
+ }
229
249
  }
230
250
  }
231
251
  if (ctx) {
@@ -161,6 +161,8 @@ var api = module.exports = {
161
161
  safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
162
162
  }
163
163
 
164
+ safeSettings.telemetryEnabled = runtime.telemetry.isEnabled()
165
+
164
166
  safeSettings.runtimeState = {
165
167
  //unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
166
168
  enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
@@ -213,7 +215,19 @@ var api = module.exports = {
213
215
  }
214
216
  var currentSettings = runtime.settings.getUserSettings(username)||{};
215
217
  currentSettings = extend(currentSettings, opts.settings);
218
+
216
219
  try {
220
+ if (currentSettings.hasOwnProperty("telemetryEnabled")) {
221
+ // This is a global setting that is being set by the user. It should
222
+ // not be stored per-user as it applies to the whole runtime.
223
+ const telemetryEnabled = currentSettings.telemetryEnabled;
224
+ delete currentSettings.telemetryEnabled;
225
+ if (telemetryEnabled) {
226
+ runtime.telemetry.enable()
227
+ } else {
228
+ runtime.telemetry.disable()
229
+ }
230
+ }
217
231
  return runtime.settings.setUserSettings(username, currentSettings).then(function() {
218
232
  runtime.log.audit({event: "settings.update",username:username}, opts.req);
219
233
  return;
package/lib/flows/Flow.js CHANGED
@@ -675,6 +675,9 @@ class Flow {
675
675
  count: count
676
676
  }
677
677
  };
678
+ if (logMessage.hasOwnProperty('code')) {
679
+ errorMessage.error.code = logMessage.code;
680
+ }
678
681
  if (logMessage.hasOwnProperty('stack')) {
679
682
  errorMessage.error.stack = logMessage.stack;
680
683
  }
@@ -719,6 +722,14 @@ class Flow {
719
722
  });
720
723
  }
721
724
 
725
+ getContext(scope) {
726
+ if (scope === 'flow') {
727
+ return this.context
728
+ } else if (scope === 'global') {
729
+ return context.get('global')
730
+ }
731
+ }
732
+
722
733
  dump() {
723
734
  console.log("==================")
724
735
  console.log(this.TYPE, this.id);
@@ -49,6 +49,14 @@ class Group {
49
49
  }
50
50
  return this.parent.getSetting(key);
51
51
  }
52
+
53
+ error(msg) {
54
+ this.parent.error(msg);
55
+ }
56
+
57
+ getContext(scope) {
58
+ return this.parent.getContext(scope);
59
+ }
52
60
  }
53
61
 
54
62
  module.exports = {
package/lib/flows/util.js CHANGED
@@ -100,7 +100,24 @@ async function evaluateEnvProperties(flow, env, credentials) {
100
100
  }
101
101
  } else if (type ==='jsonata') {
102
102
  pendingEvaluations.push(new Promise((resolve, _) => {
103
- redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => {
103
+ redUtil.evaluateNodeProperty(value, 'jsonata',{
104
+ // Fake a node object to provide access to _flow and context
105
+ _flow: flow,
106
+ context: () => {
107
+ return {
108
+ flow: {
109
+ get: (value, store, callback) => {
110
+ return flow.getContext('flow').get(value, store, callback)
111
+ }
112
+ },
113
+ global: {
114
+ get: (value, store, callback) => {
115
+ return flow.getContext('global').get(value, store, callback)
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }, null, (err, result) => {
104
121
  if (!err) {
105
122
  if (typeof result === 'object') {
106
123
  result = { value: result, __clone__: true}
package/lib/index.js CHANGED
@@ -23,6 +23,7 @@ var library = require("./library");
23
23
  var plugins = require("./plugins");
24
24
  var settings = require("./settings");
25
25
  const multiplayer = require("./multiplayer");
26
+ const telemetry = require("./telemetry");
26
27
 
27
28
  var express = require("express");
28
29
  var path = require('path');
@@ -135,6 +136,7 @@ function start() {
135
136
  return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json")
136
137
  .then(function() { return storage.init(runtime)})
137
138
  .then(function() { return settings.load(storage)})
139
+ .then(function() { return telemetry.init(runtime)})
138
140
  .then(function() { return library.init(runtime)})
139
141
  .then(function() { return multiplayer.init(runtime)})
140
142
  .then(function() {
@@ -235,8 +237,12 @@ function start() {
235
237
  }
236
238
  }
237
239
  return redNodes.loadContextsPlugin().then(function () {
238
- redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {});
239
240
  started = true;
241
+ redNodes.loadFlows().then(() => {
242
+ if (started) {
243
+ redNodes.startFlows()
244
+ }
245
+ }).catch(function(err) {});
240
246
  });
241
247
  });
242
248
  });
@@ -337,6 +343,7 @@ var runtime = {
337
343
  library: library,
338
344
  exec: exec,
339
345
  util: util,
346
+ telemetry: telemetry,
340
347
  get adminApi() { return adminApi },
341
348
  get adminApp() { return adminApp },
342
349
  get nodeApp() { return nodeApp },
@@ -51,6 +51,8 @@ function runGitCommand(args,cwd,env,emit) {
51
51
  err.code = "git_auth_failed";
52
52
  } else if(/Authentication failed/i.test(stderr)) {
53
53
  err.code = "git_auth_failed";
54
+ } else if (/The requested URL returned error: 403/i.test(stderr)) {
55
+ err.code = "git_auth_failed";
54
56
  } else if (/commit your changes or stash/i.test(stderr)) {
55
57
  err.code = "git_local_overwrite";
56
58
  } else if (/CONFLICT/.test(err.stdout)) {
@@ -0,0 +1,213 @@
1
+ const path = require('path')
2
+ const fs = require('fs/promises')
3
+ const semver = require('semver')
4
+ const cronosjs = require('cronosjs')
5
+
6
+ const METRICS_DIR = path.join(__dirname, 'metrics')
7
+ const INITIAL_PING_DELAY = 1000 * 60 * 30 // 30 minutes from startup
8
+
9
+ /** @type {import("got").Got | undefined} */
10
+ let got
11
+
12
+ let runtime
13
+
14
+ let scheduleTask
15
+
16
+ async function gather () {
17
+ let metricFiles = await fs.readdir(METRICS_DIR)
18
+ metricFiles = metricFiles.filter(name => /^\d+-.*\.js$/.test(name))
19
+ metricFiles.sort()
20
+
21
+ const metrics = {}
22
+
23
+ for (let i = 0, l = metricFiles.length; i < l; i++) {
24
+ const metricModule = require(path.join(METRICS_DIR, metricFiles[i]))
25
+ let result = metricModule(runtime)
26
+ if (!!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
27
+ result = await result
28
+ }
29
+ const keys = Object.keys(result)
30
+ keys.forEach(key => {
31
+ const keyParts = key.split('.')
32
+ let p = metrics
33
+ keyParts.forEach((part, index) => {
34
+ if (index < keyParts.length - 1) {
35
+ if (!p[part]) {
36
+ p[part] = {}
37
+ }
38
+ p = p[part]
39
+ } else {
40
+ p[part] = result[key]
41
+ }
42
+ })
43
+ })
44
+ }
45
+ return metrics
46
+ }
47
+
48
+ async function report () {
49
+ if (!isTelemetryEnabled()) {
50
+ return
51
+ }
52
+ // If enabled, gather metrics
53
+ const metrics = await gather()
54
+
55
+ // Post metrics to endpoint - handle any error silently
56
+
57
+ if (!got) {
58
+ got = (await import('got')).got
59
+ }
60
+
61
+ runtime.log.debug('Sending telemetry')
62
+ const response = await got.post('https://telemetry.nodered.org/ping', {
63
+ json: metrics,
64
+ responseType: 'json',
65
+ headers: {
66
+ 'User-Agent': `Node-RED/${runtime.settings.version}`
67
+ }
68
+ }).json().catch(err => {
69
+ // swallow errors
70
+ runtime.log.debug('Failed to send telemetry: ' + err.toString())
71
+ })
72
+ // Example response:
73
+ // { 'node-red': { latest: '4.0.9', next: '4.1.0-beta.1.9' } }
74
+ runtime.log.debug(`Telemetry response: ${JSON.stringify(response)}`)
75
+ // Get response from endpoint
76
+ if (response?.['node-red']) {
77
+ const currentVersion = metrics.env['node-red']
78
+ if (semver.valid(currentVersion)) {
79
+ const latest = response['node-red'].latest
80
+ const next = response['node-red'].next
81
+ let updatePayload
82
+ if (semver.lt(currentVersion, latest)) {
83
+ // Case one: current < latest
84
+ runtime.log.info(`A new version of Node-RED is available: ${latest}`)
85
+ updatePayload = { version: latest }
86
+ } else if (semver.gt(currentVersion, latest) && semver.lt(currentVersion, next)) {
87
+ // Case two: current > latest && current < next
88
+ runtime.log.info(`A new beta version of Node-RED is available: ${next}`)
89
+ updatePayload = { version: next }
90
+ }
91
+
92
+ if (updatePayload && isUpdateNotificationEnabled()) {
93
+ runtime.events.emit("runtime-event",{id:"update-available", payload: updatePayload, retain: true});
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ function isTelemetryEnabled () {
100
+ // If NODE_RED_DISABLE_TELEMETRY was set, or --no-telemetry was specified,
101
+ // the settings object will have been updated to disable telemetry explicitly
102
+
103
+ // If there are no telemetry settings then the user has not had a chance
104
+ // to opt out yet - so keep it disabled until they do
105
+
106
+ let telemetrySettings
107
+ try {
108
+ telemetrySettings = runtime.settings.get('telemetry')
109
+ } catch (err) {
110
+ // Settings not available
111
+ }
112
+ let runtimeTelemetryEnabled
113
+ try {
114
+ runtimeTelemetryEnabled = runtime.settings.get('telemetryEnabled')
115
+ } catch (err) {
116
+ // Settings not available
117
+ }
118
+
119
+ if (telemetrySettings === undefined && runtimeTelemetryEnabled === undefined) {
120
+ // No telemetry settings - so keep it disabled
121
+ return undefined
122
+ }
123
+
124
+ // User has made a choice; defer to that
125
+ if (runtimeTelemetryEnabled !== undefined) {
126
+ return runtimeTelemetryEnabled
127
+ }
128
+
129
+ // If there are telemetry settings, use what it says
130
+ if (telemetrySettings && telemetrySettings.enabled !== undefined) {
131
+ return telemetrySettings.enabled
132
+ }
133
+
134
+ // At this point, we have no sign the user has consented to telemetry, so
135
+ // keep disabled - but return undefined as a false-like value to distinguish
136
+ // it from the explicit disable above
137
+ return undefined
138
+ }
139
+
140
+ function isUpdateNotificationEnabled () {
141
+ const telemetrySettings = runtime.settings.get('telemetry') || {}
142
+ return telemetrySettings.updateNotification !== false
143
+ }
144
+ /**
145
+ * Start the telemetry schedule
146
+ */
147
+ function startTelemetry () {
148
+ if (scheduleTask) {
149
+ // Already scheduled - nothing left to do
150
+ return
151
+ }
152
+
153
+ const pingTime = new Date(Date.now() + INITIAL_PING_DELAY)
154
+ const pingMinutes = pingTime.getMinutes()
155
+ const pingHours = pingTime.getHours()
156
+ const pingSchedule = `${pingMinutes} ${pingHours} * * *`
157
+
158
+ runtime.log.debug(`Telemetry enabled. Schedule: ${pingSchedule}`)
159
+
160
+ scheduleTask = cronosjs.scheduleTask(pingSchedule, () => {
161
+ report()
162
+ })
163
+ }
164
+
165
+ function stopTelemetry () {
166
+ if (scheduleTask) {
167
+ runtime.log.debug(`Telemetry disabled`)
168
+ scheduleTask.stop()
169
+ scheduleTask = null
170
+ }
171
+ }
172
+
173
+ module.exports = {
174
+ init: (_runtime) => {
175
+ runtime = _runtime
176
+
177
+ if (isTelemetryEnabled()) {
178
+ startTelemetry()
179
+ }
180
+ },
181
+ /**
182
+ * Enable telemetry via user opt-in in the editor
183
+ */
184
+ enable: () => {
185
+ if (runtime.settings.available()) {
186
+ runtime.settings.set('telemetryEnabled', true)
187
+ }
188
+ startTelemetry()
189
+ },
190
+
191
+ /**
192
+ * Disable telemetry via user opt-in in the editor
193
+ */
194
+ disable: () => {
195
+ if (runtime.settings.available()) {
196
+ runtime.settings.set('telemetryEnabled', false)
197
+ }
198
+ stopTelemetry()
199
+ },
200
+
201
+ /**
202
+ * Get telemetry enabled status
203
+ * @returns {boolean} true if telemetry is enabled, false if disabled, undefined if not set
204
+ */
205
+ isEnabled: isTelemetryEnabled,
206
+
207
+ stop: () => {
208
+ if (scheduleTask) {
209
+ scheduleTask.stop()
210
+ scheduleTask = null
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = (runtime) => {
2
+ return {
3
+ instanceId: runtime.settings.get('instanceId')
4
+ }
5
+ }
@@ -0,0 +1,9 @@
1
+ const os = require('os')
2
+
3
+ module.exports = (_) => {
4
+ return {
5
+ 'os.type': os.type(),
6
+ 'os.release': os.release(),
7
+ 'os.arch': os.arch()
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ const process = require('process')
2
+
3
+ module.exports = (runtime) => {
4
+ return {
5
+ 'env.nodejs': process.version.replace(/^v/, ''),
6
+ 'env.node-red': runtime.settings.version
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-red/runtime",
3
- "version": "4.0.8",
3
+ "version": "4.1.0-beta.1",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./lib/index.js",
6
6
  "repository": {
@@ -16,13 +16,15 @@
16
16
  }
17
17
  ],
18
18
  "dependencies": {
19
- "@node-red/registry": "4.0.8",
20
- "@node-red/util": "4.0.8",
19
+ "@node-red/registry": "4.1.0-beta.1",
20
+ "@node-red/util": "4.1.0-beta.1",
21
21
  "async-mutex": "0.5.0",
22
22
  "clone": "2.1.2",
23
+ "cronosjs": "1.7.1",
23
24
  "express": "4.21.2",
24
- "fs-extra": "11.2.0",
25
+ "fs-extra": "11.3.0",
25
26
  "json-stringify-safe": "5.0.1",
26
- "rfdc": "^1.3.1"
27
+ "rfdc": "^1.3.1",
28
+ "semver": "7.7.1"
27
29
  }
28
30
  }