@node-red/runtime 4.0.9 → 4.1.0-beta.2
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.
- package/lib/api/context.js +5 -1
- package/lib/api/settings.js +14 -0
- package/lib/flows/Flow.js +3 -0
- package/lib/index.js +8 -1
- package/lib/storage/localfilesystem/library.js +1 -1
- package/lib/storage/localfilesystem/projects/git/index.js +2 -0
- package/lib/telemetry/index.js +213 -0
- package/lib/telemetry/metrics/01-core.js +5 -0
- package/lib/telemetry/metrics/02-os.js +9 -0
- package/lib/telemetry/metrics/03-env.js +8 -0
- package/package.json +7 -5
package/lib/api/context.js
CHANGED
|
@@ -241,7 +241,11 @@ var api = module.exports = {
|
|
|
241
241
|
} else if (scope === 'node') {
|
|
242
242
|
var node = runtime.nodes.getNode(id);
|
|
243
243
|
if (node) {
|
|
244
|
-
|
|
244
|
+
if (/^subflow:/.test(node.type)) {
|
|
245
|
+
ctx = runtime.nodes.getContext(node.id);
|
|
246
|
+
} else {
|
|
247
|
+
ctx = node.context();
|
|
248
|
+
}
|
|
245
249
|
}
|
|
246
250
|
}
|
|
247
251
|
if (ctx) {
|
package/lib/api/settings.js
CHANGED
|
@@ -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
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node-red/runtime",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.1.0-beta.2",
|
|
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.
|
|
20
|
-
"@node-red/util": "4.0.
|
|
19
|
+
"@node-red/registry": "4.1.0-beta.2",
|
|
20
|
+
"@node-red/util": "4.1.0-beta.2",
|
|
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.
|
|
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
|
}
|