@flowfuse/device-agent
Advanced tools
Comparing version 2.1.0 to 2.2.0
@@ -0,1 +1,7 @@ | ||
#### 2.2.0: Release | ||
- Wire up Node-RED instance audit events to FF (#232) @Steve-Mcl | ||
- Implement deferred stop like nr-launcher (#236) @Steve-Mcl | ||
- Fix theme following org name change (#235) @Steve-Mcl | ||
#### 2.1.0: Release | ||
@@ -2,0 +8,0 @@ |
@@ -5,3 +5,4 @@ const childProcess = require('child_process') | ||
const path = require('path') | ||
const { info, debug, warn, NRlog } = require('./logging/log') | ||
const { log, info, debug, warn, NRlog } = require('./logging/log') | ||
const { copyDir } = require('./utils') | ||
@@ -11,2 +12,5 @@ const MIN_RESTART_TIME = 10000 // 10 seconds | ||
/** How long wait for Node-RED to cleanly stop before killing */ | ||
const NODE_RED_STOP_TIMEOUT = 10000 | ||
const packageJSONTemplate = { | ||
@@ -34,2 +38,8 @@ name: 'flowfuse-project', | ||
this.auditLogURL = `${this.config.forgeURL}/logging/device/${this.config.deviceId}/audit` | ||
// A callback function that will be set if the launcher is waiting | ||
// for Node-RED to exit | ||
this.exitCallback = null | ||
this.projectDir = path.join(this.config.dir, 'project') | ||
@@ -43,3 +53,3 @@ | ||
userSettings: path.join(this.projectDir, 'settings.json'), | ||
themeDir: path.join(this.projectDir, 'node_modules/@flowforge/nr-theme'), | ||
themeDir: path.join(this.projectDir, 'node_modules', '@flowfuse', 'nr-theme'), | ||
npmrc: path.join(this.projectDir, '.npmrc') | ||
@@ -183,2 +193,8 @@ } | ||
teamID, | ||
deviceId: this.config.deviceId, | ||
auditLogger: { | ||
url: this.auditLogURL, | ||
token: this.config.token, | ||
bin: path.join(__dirname, 'auditLogger', 'index.js') | ||
}, | ||
projectLink | ||
@@ -282,6 +298,6 @@ } | ||
info('Updating theme files') | ||
const sourceDir1 = path.join(__dirname, '..', 'node_modules', '@flowforge', 'nr-theme') | ||
const sourceDir1 = path.join(__dirname, '..', 'node_modules', '@flowfuse', 'nr-theme') | ||
const sourceDir2 = path.join(__dirname, '..', '..', 'nr-theme') | ||
const sourceDir = existsSync(sourceDir1) ? sourceDir1 : sourceDir2 | ||
const targetDir = path.join(this.projectDir, 'node_modules', '@flowforge', 'nr-theme') | ||
const targetDir = path.join(this.projectDir, 'node_modules', '@flowfuse', 'nr-theme') | ||
try { | ||
@@ -295,3 +311,3 @@ if (!existsSync(sourceDir)) { | ||
} | ||
await fs.cp(sourceDir, targetDir, { recursive: true }) | ||
await copyDir(sourceDir, targetDir, { recursive: true }) | ||
} catch (error) { | ||
@@ -303,2 +319,5 @@ info(`Could not write theme files to disk: '${targetDir}'`) | ||
async start () { | ||
if (this.deferredStop) { | ||
await this.deferredStop | ||
} | ||
this.state = 'starting' | ||
@@ -368,2 +387,3 @@ if (!existsSync(this.projectDir) || | ||
debug(`CMD: ${execPath} ${processArgs.join(' ')}`) | ||
/** @type {childProcess.ChildProcess} */ | ||
this.proc = childProcess.spawn( | ||
@@ -404,2 +424,8 @@ execPath, | ||
} | ||
} else { | ||
// is really shutting down (i.e. was commanded by the | ||
// agent, so we won't be doing an auto restart) | ||
if (this.exitCallback) { | ||
this.exitCallback() | ||
} | ||
} | ||
@@ -428,18 +454,51 @@ }) | ||
info('Stopping Node-RED') | ||
// something wrong here, want to wait until child is dead | ||
if (this.deferredStop) { | ||
// A stop request is already inflight - return the existing deferred object | ||
return this.deferredStop | ||
} | ||
/** Operations that should be performed after the process has exited */ | ||
const postShutdownOps = async () => { | ||
if (clean) { | ||
info('Cleaning instance directory') | ||
try { | ||
await fs.rm(this.projectDir, { force: true, recursive: true }) | ||
} catch (err) { | ||
warn('Error cleaning instance directory', err) | ||
} | ||
} | ||
} | ||
if (this.proc) { | ||
this.shuttingDown = true | ||
const exitPromise = new Promise(resolve => { | ||
this.proc.on('exit', resolve) | ||
// Setup a promise that will resolve once the process has really exited | ||
this.deferredStop = new Promise((resolve, reject) => { | ||
// Setup a timeout so we can more forcefully kill Node-RED | ||
this.exitTimeout = setTimeout(async () => { | ||
log('Node-RED stop timed-out. Sending SIGKILL', 'system') | ||
if (this.proc) { | ||
this.proc.kill('SIGKILL') | ||
} | ||
}, NODE_RED_STOP_TIMEOUT) | ||
// Setup a callback for when the process has actually exited | ||
this.exitCallback = async () => { | ||
clearTimeout(this.exitTimeout) | ||
this.exitCallback = null | ||
this.deferredStop = null | ||
this.exitTimeout = null | ||
this.proc && this.proc.unref() | ||
this.proc = undefined | ||
await postShutdownOps() | ||
resolve() | ||
} | ||
// Send a kill signal. On Linux this will be a SIGTERM and | ||
// allow Node-RED to shutdown cleanly. Windows looks like it does | ||
// it more forcefully by default. | ||
this.proc.kill() | ||
this.state = 'stopped' | ||
}) | ||
this.proc.kill('SIGINT') | ||
await exitPromise | ||
info('Stopped Node-RED') | ||
return this.deferredStop | ||
} else { | ||
this.state = 'stopped' | ||
await postShutdownOps() | ||
} | ||
this.state = 'stopped' | ||
if (clean) { | ||
info('Cleaning instance directory') | ||
await fs.rm(this.projectDir, { force: true, recursive: true }) | ||
} | ||
} | ||
@@ -446,0 +505,0 @@ } |
@@ -122,2 +122,16 @@ const settings = require('./settings.json') | ||
if (settings.flowforge.auditLogger?.bin && settings.flowforge.auditLogger?.url) { | ||
try { | ||
runtimeSettings.logging.auditLogger = { | ||
level: 'off', | ||
audit: true, | ||
handler: require(settings.flowforge.auditLogger.bin), | ||
loggingURL: settings.flowforge.auditLogger.url, | ||
token: settings.flowforge.auditLogger.token | ||
} | ||
} catch (e) { | ||
console.warn('Could not initialise device audit logging. Audit events will not be logged to the platform') | ||
} | ||
} | ||
if (settings.https) { | ||
@@ -124,0 +138,0 @@ ;['key', 'ca', 'cert'].forEach(key => { |
@@ -0,1 +1,4 @@ | ||
const path = require('path') | ||
const fs = require('fs').promises | ||
module.exports = { | ||
@@ -5,3 +8,4 @@ compareNodeRedData, | ||
isObject, | ||
hasProperty | ||
hasProperty, | ||
copyDir | ||
} | ||
@@ -81,1 +85,34 @@ | ||
} | ||
/** | ||
* Copy a directory from one location to another | ||
* @param {string} src - source directory | ||
* @param {string} dest - destination directory | ||
* @param {Object} [options] - options | ||
* @param {boolean} [options.recursive=true] - whether to copy recursively (default: true) | ||
*/ | ||
async function copyDir (src, dest, { recursive = true } = {}) { | ||
// for nodejs v 16.7.0 and later, fs.cp will be available | ||
if (fs.cp && typeof fs.cp === 'function') { | ||
await fs.cp(src, dest, { recursive }) | ||
return | ||
} | ||
// fallback to own implementation of recursive copy (for Node.js 14) | ||
// TODO: remove this when Node.js 14 is no longer supported by the device agent | ||
const cp = async (src, dest) => { | ||
const lstat = await fs.lstat(src).catch(_err => { }) | ||
if (!lstat) { | ||
// do nothing | ||
} else if (lstat.isFile()) { | ||
await fs.copyFile(src, dest) | ||
} else if (lstat.isDirectory()) { | ||
await fs.mkdir(dest).catch(_err => { }) | ||
if (recursive) { | ||
for (const f of await fs.readdir(src)) { | ||
await cp(path.join(src, f), path.join(dest, f)) | ||
} | ||
} | ||
} | ||
} | ||
await cp(src, dest) | ||
} |
{ | ||
"name": "@flowfuse/device-agent", | ||
"version": "2.1.0", | ||
"version": "2.2.0", | ||
"description": "An Edge Agent for running Node-RED instances deployed from the FlowFuse Platform", | ||
"exports": { | ||
"./libraryPlugin": "./lib/plugins/libraryPlugin.js" | ||
"./libraryPlugin": "./lib/plugins/libraryPlugin.js", | ||
"./auditLogger": "./lib/auditLogger/index.js" | ||
}, | ||
@@ -8,0 +9,0 @@ "main": "index.js", |
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
265523
35
4081
18