@flowfuse/device-agent
Advanced tools
Comparing version 1.15.0 to 2.0.0
@@ -0,1 +1,8 @@ | ||
#### 2.0.0: Release | ||
- Enable WebSocket Affinity in the device agent (#213) @hardillb | ||
- Fix WS readystate crash (#221) @Steve-Mcl | ||
- Permit Device Agent to automatically re-establish Editor Tunnel after a restart (#220) @Steve-Mcl | ||
- Fix console.log(JSON) (#219) @hardillb | ||
#### 1.15.0: Release | ||
@@ -2,0 +9,0 @@ |
@@ -34,2 +34,3 @@ const { IntervalJitter } = require('./IntervalJitter') | ||
this.currentState = 'unknown' | ||
this.editorToken = null | ||
// ensure licensed property is present (default to null) | ||
@@ -54,2 +55,3 @@ if (utils.hasProperty(this.config, 'licensed') === false) { | ||
this.currentApplication = null | ||
this.editorToken = null | ||
} else { | ||
@@ -63,2 +65,3 @@ // New format | ||
this.config.licensed = config.licensed || null | ||
this.editorToken = config.editorToken || null | ||
} | ||
@@ -109,3 +112,4 @@ this.printAgentStatus() | ||
mode: this.currentMode, | ||
licensed: this.config.licensed | ||
licensed: this.config.licensed, | ||
editorToken: this.editorToken | ||
})) | ||
@@ -256,2 +260,3 @@ } | ||
// exiting developer mode | ||
this.editorToken = null // clear the discarded token | ||
let _launcher = this.launcher | ||
@@ -353,3 +358,3 @@ if (!_launcher) { | ||
/** A flag to inhibit updates if we are in developer mode */ | ||
const inhibitUpdates = this.currentMode === 'developer' | ||
const developerMode = this.currentMode === 'developer' | ||
@@ -362,3 +367,3 @@ /** A flag to indicate execution should skip to the update step */ | ||
// Wipe the local configuration | ||
if (inhibitUpdates === false) { | ||
if (developerMode === false) { | ||
this.stop() | ||
@@ -370,2 +375,3 @@ this.currentSnapshot = null | ||
this.currentMode = null | ||
this.editorToken = null | ||
await this.saveProject() | ||
@@ -375,3 +381,3 @@ this.currentState = 'stopped' | ||
} | ||
} else if (!skipToUpdate && inhibitUpdates === false && newState.application === null && this.currentOwnerType === 'application') { | ||
} else if (!skipToUpdate && developerMode === false && newState.application === null && this.currentOwnerType === 'application') { | ||
if (this.currentApplication) { | ||
@@ -404,3 +410,3 @@ debug('Removed from application') | ||
this.updating = false | ||
} else if (!skipToUpdate && inhibitUpdates === false && newState.project === null && this.currentOwnerType === 'project') { | ||
} else if (!skipToUpdate && developerMode === false && newState.project === null && this.currentOwnerType === 'project') { | ||
if (this.currentProject) { | ||
@@ -433,3 +439,3 @@ debug('Removed from project') | ||
this.updating = false | ||
} else if (!skipToUpdate && inhibitUpdates === false && newState.snapshot === null) { | ||
} else if (!skipToUpdate && developerMode === false && newState.snapshot === null) { | ||
// Snapshot removed, but project/application still set | ||
@@ -486,3 +492,3 @@ if (this.currentSnapshot) { | ||
const applicationUpdatePending = !!(newState.ownerType === 'application' && !this.currentApplication && newState.application) | ||
if (unknownOrStopped && inhibitUpdates && snapShotUpdatePending && (projectUpdatePending || applicationUpdatePending)) { | ||
if (unknownOrStopped && developerMode && snapShotUpdatePending && (projectUpdatePending || applicationUpdatePending)) { | ||
info('Developer Mode: no flows found - updating to latest snapshot') | ||
@@ -493,3 +499,3 @@ this.currentProject = newState.project | ||
updateSettings = true | ||
} else if (inhibitUpdates === false) { | ||
} else if (developerMode === false) { | ||
if (utils.hasProperty(newState, 'project') && (!this.currentSnapshot || newState.project !== this.currentProject)) { | ||
@@ -544,2 +550,5 @@ info('New instance assigned') | ||
this.mqttClient.setApplication(this.currentApplication) | ||
if (developerMode && this.editorToken) { | ||
this.mqttClient.startTunnel(this.editorToken) | ||
} | ||
} | ||
@@ -590,2 +599,5 @@ this.checkIn(2) | ||
this.mqttClient.setApplication(this.currentApplication) | ||
if (developerMode && this.editorToken) { | ||
this.mqttClient.startTunnel(this.editorToken) | ||
} | ||
} | ||
@@ -694,2 +706,10 @@ this.checkIn(2) | ||
} | ||
async saveEditorToken (token) { | ||
const changed = this.editorToken !== token | ||
this.editorToken = token | ||
if (changed) { | ||
await this.saveProject() | ||
} | ||
} | ||
} | ||
@@ -696,0 +716,0 @@ |
@@ -25,2 +25,3 @@ const { info, warn, debug } = require('../logging/log') | ||
this.options = options || {} | ||
this.affinity = undefined | ||
@@ -54,2 +55,3 @@ // How long to wait before attempting to reconnect. Start at 500ms - back | ||
const thisTunnel = this | ||
let unexpectedPacketMonitor = 0 | ||
if (this.socket) { | ||
@@ -62,8 +64,31 @@ this.close() | ||
// * Enable Device Editor (Step 8) - (device->forge:WS) Initiate WS connection (with token) | ||
const headers = { | ||
'x-access-token': this.options.token | ||
} | ||
if (this.affinity) { | ||
headers.cookie = `FFSESSION=${this.affinity}` | ||
} | ||
const socket = newWsConnection(forgeWSEndpoint, { | ||
headers: { | ||
'x-access-token': this.options.token | ||
headers | ||
}) | ||
socket.on('upgrade', (evt) => { | ||
if (evt.headers && evt.headers['set-cookie']) { | ||
const cookies = evt.headers['set-cookie'] | ||
if (Array.isArray(cookies)) { | ||
cookies.forEach(cookie => { | ||
const parts = cookie.split(';')[0].split(['=']) | ||
if (parts === 'FFSESSION') { | ||
this.affinity = parts[1] | ||
} | ||
}) | ||
} else { | ||
const parts = cookies.split(';')[0].split(['=']) | ||
if (parts === 'FFSESSION') { | ||
this.affinity = parts[1] | ||
} | ||
} | ||
} | ||
}) | ||
socket.onopen = (evt) => { | ||
unexpectedPacketMonitor = 0 | ||
info('Editor tunnel connected') | ||
@@ -106,3 +131,5 @@ // Reset reconnectDelay | ||
// console.log(`[${request.id}] R>E`, sendData.body) | ||
this.socket?.send(JSON.stringify(sendData)) | ||
if (this.socket?.readyState === WebSocket.OPEN) { | ||
this.socket.send(JSON.stringify(sendData)) | ||
} | ||
}) | ||
@@ -122,7 +149,9 @@ // Now the local comms is connected, send anything | ||
// websocket to match | ||
this.socket?.send(JSON.stringify({ | ||
id: request.id, | ||
ws: true, | ||
closed: true | ||
})) | ||
if (this.socket?.readyState === WebSocket.OPEN) { | ||
this.socket.send(JSON.stringify({ | ||
id: request.id, | ||
ws: true, | ||
closed: true | ||
})) | ||
} | ||
thisTunnel.wsClients[request.id]?.removeAllListeners() | ||
@@ -160,4 +189,9 @@ thisTunnel.wsClients[request.id] = null | ||
} else { | ||
warn(`[${request.id}] Unexpected editor comms packet ${JSON.stringify(request, null, 4)}`) | ||
this.close(1006, 'Non-connect packet received for unknown connection id') // 1006 = Abnormal closure | ||
if (unexpectedPacketMonitor > 1) { | ||
unexpectedPacketMonitor = 0 | ||
warn(`[${request.id}] Unexpected editor comms packet ${JSON.stringify(request, null, 4)}`) | ||
this.close(1006, 'Non-connect packet received for unknown connection id') // 1006 = Abnormal closure | ||
} else { | ||
unexpectedPacketMonitor++ | ||
} | ||
} | ||
@@ -199,8 +233,10 @@ } else { | ||
// send response back to the forge | ||
this.socket?.send(JSON.stringify({ | ||
id: request.id, | ||
headers: response.headers, | ||
body: response.rawBody, | ||
status: response.statusCode | ||
})) | ||
if (this.socket?.readyState === WebSocket.OPEN) { | ||
this.socket.send(JSON.stringify({ | ||
id: request.id, | ||
headers: response.headers, | ||
body: response.rawBody, | ||
status: response.statusCode | ||
})) | ||
} | ||
}).catch(_err => { | ||
@@ -211,7 +247,9 @@ // debug(`proxy [${request.method}] ${fullUrl} : error ${_err.toString()}`) | ||
// console.log(JSON.stringify(request)) | ||
this.socket?.send(JSON.stringify({ | ||
id: request.id, | ||
body: undefined, | ||
status: 404 | ||
})) | ||
if (this.socket?.readyState === WebSocket.OPEN) { | ||
this.socket.send(JSON.stringify({ | ||
id: request.id, | ||
body: undefined, | ||
status: 404 | ||
})) | ||
} | ||
}) | ||
@@ -218,0 +256,0 @@ } |
@@ -27,2 +27,6 @@ const LogBuffer = require('./logBuffer') | ||
} | ||
if (!Object.hasOwn(jsMsg, 'ts') && !Object.hasOwn(jsMsg, 'level')) { | ||
// not a NR log message | ||
jsMsg = { ts: Date.now(), level: '', msg } | ||
} | ||
const date = new Date(jsMsg.ts) | ||
@@ -29,0 +33,0 @@ if (typeof jsMsg.msg !== 'string') { |
@@ -103,19 +103,3 @@ const mqtt = require('mqtt') | ||
} else if (msg.command === 'startEditor') { | ||
info('Enabling remote editor access') | ||
if (this.tunnel) { | ||
this.tunnel.close() | ||
this.tunnel = null | ||
} | ||
if (!this.agent.launcher) { | ||
info('No running Node-RED instance, not starting editor') | ||
this.sendCommandResponse(msg, { connected: false, token: msg?.payload?.token, error: 'noNRRunning' }) | ||
return | ||
} | ||
// * Enable Device Editor (Step 6) - (forge:MQTT->device) Create the tunnel on the device | ||
this.tunnel = EditorTunnel.create(this.config, { token: msg?.payload?.token }) | ||
// * Enable Device Editor (Step 7) - (device) Begin the device tunnel connect process | ||
const result = await this.tunnel.connect() | ||
// * Enable Device Editor (Step 10) - (device->forge:MQTT) Send a response to the platform | ||
this.sendCommandResponse(msg, { connected: result, token: msg?.payload?.token }) | ||
this.sendStatus() | ||
await this.startTunnel(msg.payload?.token, msg) | ||
return | ||
@@ -285,2 +269,43 @@ } else if (msg.command === 'stopEditor') { | ||
} | ||
async startTunnel (token, msg) { | ||
info('Enabling remote editor access') | ||
try { | ||
if (this.tunnel) { | ||
this.tunnel.close() | ||
this.tunnel = null | ||
} | ||
if (!this.agent.launcher) { | ||
info('No running Node-RED instance, not starting editor') | ||
if (msg) { | ||
this.sendCommandResponse(msg, { connected: false, token, error: 'noNRRunning' }) | ||
} | ||
return | ||
} | ||
// * Enable Device Editor (Step 6) - (forge:MQTT->device) Create the tunnel on the device | ||
this.tunnel = EditorTunnel.create(this.config, { token }) | ||
// * Enable Device Editor (Step 7) - (device) Begin the device tunnel connect process | ||
const result = await this.tunnel.connect() | ||
// store the token for later use (i.e. device agent is restarted) | ||
await this.saveEditorToken(result ? token : null) | ||
if (msg) { | ||
// * Enable Device Editor (Step 10) - (device->forge:MQTT) Send a response to the platform | ||
this.sendCommandResponse(msg, { connected: result, token }) | ||
} | ||
} catch (err) { | ||
warn(`Error starting editor tunnel: ${err}`) | ||
if (msg) { | ||
this.sendCommandResponse(msg, { connected: false, token, error: err.toString() }) | ||
} | ||
} | ||
this.sendStatus() | ||
} | ||
async saveEditorToken (token) { | ||
await this.agent?.saveEditorToken(token) | ||
} | ||
} | ||
@@ -287,0 +312,0 @@ |
{ | ||
"name": "@flowfuse/device-agent", | ||
"version": "1.15.0", | ||
"version": "2.0.0", | ||
"description": "An Edge Agent for running Node-RED instances deployed from the FlowFuse Platform", | ||
@@ -5,0 +5,0 @@ "exports": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
253104
3800