@inteli.city/node-red-contrib-exec-collection 1.0.4 → 1.0.5
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/README.md +836 -5
- package/exec.queue.html +228 -38
- package/exec.queue.js +553 -537
- package/exec.service.html +342 -229
- package/exec.service.js +325 -487
- package/node.queue.html +359 -0
- package/node.queue.js +569 -0
- package/package.json +19 -19
- package/python.config.html +55 -0
- package/python.config.js +24 -0
- package/python.queue.html +360 -0
- package/python.queue.js +555 -0
- package/utils/context.js +54 -0
- package/async.gpt.html +0 -327
- package/async.gpt.js +0 -615
- package/async.latex.html +0 -319
- package/async.latex.js +0 -618
- package/module.njk.html +0 -45
- package/module.njk.js +0 -12
- package/template.njk.html +0 -201
- package/template.njk.js +0 -138
- package/thrd.function.html +0 -312
- package/thrd.function.js +0 -586
- package/thread.queue.html +0 -311
- package/thread.queue.js +0 -586
package/exec.service.js
CHANGED
|
@@ -1,548 +1,386 @@
|
|
|
1
|
-
|
|
2
|
-
* Copyright JS Foundation and other contributors, http://js.foundation
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
**///
|
|
16
|
-
module.exports = function(RED) {
|
|
17
|
-
//* libraries
|
|
18
|
-
"use strict";
|
|
19
|
-
var mustache = require("mustache");
|
|
20
|
-
var fs = require('fs');
|
|
21
|
-
var tmp = require('tmp-promise');
|
|
22
|
-
var fsPromises = require('fs').promises;
|
|
23
|
-
var terminate = require("terminate");
|
|
24
|
-
var yaml = require("js-yaml");
|
|
25
|
-
var convertXML = require('xml-js');
|
|
26
|
-
var exec = require('child_process').exec;
|
|
27
|
-
var spawn = require('child_process').spawn;
|
|
28
|
-
var Queue = require('queue');
|
|
29
|
-
var pm2 = require('pm2');
|
|
30
|
-
//* auxiliary functions
|
|
31
|
-
//** function: extractTokens
|
|
32
|
-
function extractTokens(tokens, set) {
|
|
33
|
-
set = set || new Set();
|
|
34
|
-
tokens.forEach(function(token) {
|
|
35
|
-
if (token[0] !== 'text') {
|
|
36
|
-
set.add(token[1]);
|
|
37
|
-
if (token.length > 4) {
|
|
38
|
-
extractTokens(token[4], set);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
return set;
|
|
43
|
-
}
|
|
1
|
+
'use strict';
|
|
44
2
|
|
|
45
|
-
|
|
46
|
-
function parseContext(key) {
|
|
47
|
-
var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key);
|
|
48
|
-
if (match) {
|
|
49
|
-
var parts = {};
|
|
50
|
-
parts.type = match[1];
|
|
51
|
-
parts.store = (match[3] === '') ? "default" : match[3];
|
|
52
|
-
parts.field = match[4];
|
|
53
|
-
return parts;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
3
|
+
// ── requires ──────────────────────────────────────────────────────────────────
|
|
57
4
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const tmp = require('tmp-promise');
|
|
8
|
+
const nunjucks = require('nunjucks');
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const convertXML = require('xml-js');
|
|
11
|
+
const { StringDecoder } = require('string_decoder');
|
|
12
|
+
const { buildContext } = require('./utils/context');
|
|
13
|
+
|
|
14
|
+
// ── platform ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function getPlatformConfig() {
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
return { shell: 'cmd.exe', args: ['/d', '/s', '/c'] };
|
|
65
19
|
}
|
|
20
|
+
return { shell: '/bin/bash', args: ['-c'] };
|
|
21
|
+
}
|
|
66
22
|
|
|
23
|
+
const PLATFORM = getPlatformConfig();
|
|
67
24
|
|
|
68
|
-
|
|
69
|
-
* Custom Mustache Context capable to collect message property and node
|
|
70
|
-
* flow and global context
|
|
71
|
-
*/
|
|
25
|
+
// ── shell helpers ─────────────────────────────────────────────────────────────
|
|
72
26
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.splice(i, 1);
|
|
78
|
-
i--;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return this;
|
|
27
|
+
// Sets $file / %file% before running the command, matching exec.queue behaviour.
|
|
28
|
+
function buildShellcode(filePath, command) {
|
|
29
|
+
if (PLATFORM.shell === 'cmd.exe') {
|
|
30
|
+
return `set "file=${filePath}" && ${command}`;
|
|
82
31
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
constructor(msg, nodeContext, parent, escapeStrings, cachedContextTokens) {
|
|
86
|
-
super(msg, parent);
|
|
87
|
-
this.nodeContext = nodeContext;
|
|
88
|
-
this.escapeStrings = escapeStrings;
|
|
89
|
-
this.cachedContextTokens = cachedContextTokens;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
lookup(name) {
|
|
93
|
-
try {
|
|
94
|
-
var value = super.lookup(name);
|
|
95
|
-
if (value !== undefined) {
|
|
96
|
-
if (typeof value === "object") {
|
|
97
|
-
value = JSON.stringify(value);
|
|
98
|
-
}
|
|
99
|
-
if (this.escapeStrings && typeof value === "string") {
|
|
100
|
-
value = value.replace(/\\/g, "\\\\")
|
|
101
|
-
.replace(/\n/g, "\\n")
|
|
102
|
-
.replace(/\t/g, "\\t")
|
|
103
|
-
.replace(/\r/g, "\\r")
|
|
104
|
-
.replace(/\f/g, "\\f")
|
|
105
|
-
.replace(/[\b]/g, "\\b");
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
32
|
+
return `\nexport NODE_PATH="$NODE_PATH:/usr/local/lib/node_modules"\nfile="${filePath}"\n${command}\n`;
|
|
33
|
+
}
|
|
109
34
|
|
|
110
|
-
|
|
111
|
-
return this.cachedContextTokens[name];
|
|
112
|
-
}
|
|
35
|
+
// ── output parsing ────────────────────────────────────────────────────────────
|
|
113
36
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return '';
|
|
122
|
-
} catch (err) {
|
|
123
|
-
throw err;
|
|
37
|
+
// Pure: parses rawValue per outputFormat.
|
|
38
|
+
// Returns { value } on success, { error: string } on failure.
|
|
39
|
+
function parseOutputValue(rawValue, outputFormat) {
|
|
40
|
+
if (outputFormat === 'parsedJSON') {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(rawValue);
|
|
43
|
+
if (typeof parsed === 'number') {
|
|
44
|
+
return { error: 'Error parsing JSON: result is a plain number' };
|
|
124
45
|
}
|
|
46
|
+
return { value: parsed };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return { error: 'Error parsing JSON:\n\n' + err };
|
|
125
49
|
}
|
|
50
|
+
}
|
|
126
51
|
|
|
127
|
-
|
|
128
|
-
|
|
52
|
+
if (outputFormat === 'parsedXML') {
|
|
53
|
+
try {
|
|
54
|
+
return { value: convertXML.xml2js(rawValue, { compact: true, spaces: 4 }) };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return { error: 'Error parsing XML:\n\n' + err };
|
|
129
57
|
}
|
|
130
58
|
}
|
|
131
59
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.fieldType = n.fieldType || "msg";
|
|
142
|
-
this.outputFormat = n.output || "str";
|
|
143
|
-
|
|
144
|
-
this.cmd = (n.command || "").trim();
|
|
145
|
-
this.append = (n.append || "").trim();
|
|
146
|
-
this.useSpawn = n.useSpawn
|
|
147
|
-
this.inputEmpty = n.inputEmpty
|
|
148
|
-
this.count = 0
|
|
149
|
-
this.state = ""
|
|
150
|
-
this.queue = n.queue
|
|
151
|
-
this.executingCode = 0
|
|
152
|
-
this.waitingForExecuting = 0
|
|
153
|
-
this.processKilled = false
|
|
154
|
-
this.statusTimerUp = new Date()
|
|
155
|
-
this.statusTimerDown = new Date()
|
|
156
|
-
// this.addpayCB = n.addpayCB
|
|
157
|
-
this.cmdTemplate = n.cmdTemplate
|
|
158
|
-
this.cmd = (n.command || "").trim();
|
|
159
|
-
//this.parsedJSON = n.parsedJSON
|
|
160
|
-
this.splitLine = n.splitLine
|
|
161
|
-
this.cleanQueue = n.cleanQueue
|
|
162
|
-
|
|
163
|
-
if (n.addpay === undefined) { n.addpay = true; }
|
|
164
|
-
this.addpay = n.addpay;
|
|
165
|
-
this.append = (n.append || "").trim();
|
|
166
|
-
this.useSpawn = (n.useSpawn == "true");
|
|
167
|
-
this.timer = Number(n.timer || 0) * 1000;
|
|
168
|
-
this.activeProcesses = {};
|
|
169
|
-
this.tempFiles = []
|
|
170
|
-
this.oldrc = (n.oldrc || false).toString();
|
|
171
|
-
//this.execOpt = {maxBuffer:50000000, windowsHide: (n.winHide === true)};
|
|
172
|
-
//this.execOpt = {encoding:'binary', maxBuffer:50000000, windowsHide: (n.winHide === true)};
|
|
173
|
-
this.execOpt = { maxBuffer: 50000000, windowsHide: (n.winHide === true), detached: true };
|
|
174
|
-
this.spawnOpt = { windowsHide: (n.winHide === true), detached: true }
|
|
175
|
-
this.executed = false;
|
|
176
|
-
|
|
177
|
-
// this.timer = Number(n.timer || 0)*1000;
|
|
178
|
-
// this.activeProcesses = {};
|
|
179
|
-
// this.oldrc = (n.oldrc || false).toString();
|
|
180
|
-
// this.execOpt = {encoding:'binary', maxBuffer:50000000, windowsHide: (n.winHide === true)};
|
|
181
|
-
// this.spawnOpt = {windowsHide: (n.winHide === true) }
|
|
182
|
-
var node = this;
|
|
183
|
-
//** node initialization setup
|
|
184
|
-
if (process.platform === 'linux' && fs.existsSync('/bin/bash')) { node.execOpt.shell = '/bin/bash'; }
|
|
185
|
-
if ( node.inputEmpty == false ){
|
|
186
|
-
node.status({ fill: "yellow", shape: "ring", text: '(0/1)' });
|
|
60
|
+
if (outputFormat === 'parsedYAML') {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = yaml.load(rawValue);
|
|
63
|
+
if (typeof parsed === 'number') {
|
|
64
|
+
return { error: 'Error parsing YAML: result is a plain number' };
|
|
65
|
+
}
|
|
66
|
+
return { value: parsed };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { error: 'Error parsing YAML:\n\n' + err };
|
|
187
69
|
}
|
|
70
|
+
}
|
|
188
71
|
|
|
72
|
+
return { value: rawValue };
|
|
73
|
+
}
|
|
189
74
|
|
|
190
|
-
function emitInputMessage() { node.emit("input", {payload: 'play'}) }
|
|
191
|
-
//** node.on('input')
|
|
192
|
-
node.on("input", async function(msg, send, done) {
|
|
193
|
-
if (!node.executed) {
|
|
194
|
-
node.executed = true;
|
|
195
|
-
node.status({ fill: "blue", shape: "ring", text: '(1/1)' });
|
|
196
|
-
try {
|
|
197
|
-
msg.nodeName = node.name;
|
|
198
75
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
76
|
+
// ── node definition ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
module.exports = function(RED) {
|
|
79
|
+
|
|
80
|
+
function ExecServiceNode(n) {
|
|
81
|
+
RED.nodes.createNode(this, n);
|
|
82
|
+
|
|
83
|
+
this.name = n.name;
|
|
84
|
+
this.command = (n.command || '').trim();
|
|
85
|
+
this.template = n.template || '';
|
|
86
|
+
this.outputFormat = n.output || 'str';
|
|
87
|
+
this.field = n.field || 'payload';
|
|
88
|
+
this.fieldType = n.fieldType || 'msg';
|
|
89
|
+
this.streamMode = n.streamMode || 'delimited';
|
|
90
|
+
this.restartDelay = Math.max(0, Number(n.restartDelay) || 3000);
|
|
91
|
+
this.maxRetries = Math.max(0, Number(n.maxRetries) || 0); // 0 = infinite
|
|
92
|
+
|
|
93
|
+
// Interpret escape sequences in the stored delimiter string
|
|
94
|
+
const delimRaw = (n.delimiter !== undefined && n.delimiter !== '') ? n.delimiter : '\\n';
|
|
95
|
+
this.delimiter = delimRaw.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r');
|
|
96
|
+
|
|
97
|
+
const node = this;
|
|
98
|
+
|
|
99
|
+
// ── state ─────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
let desiredState = 'running'; // 'running' | 'stopped'
|
|
102
|
+
let proc = null; // active child process (or null)
|
|
103
|
+
let retryCount = 0;
|
|
104
|
+
let restartTimer = null;
|
|
105
|
+
let successTimer = null;
|
|
106
|
+
let segmentBuffer = '';
|
|
107
|
+
let closing = false;
|
|
108
|
+
let currentTempFile = null; // path of the rendered $file temp file
|
|
109
|
+
|
|
110
|
+
// ── status ────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function updateStatus(state, retry) {
|
|
113
|
+
node._serviceState = state;
|
|
114
|
+
if (state === 'running') {
|
|
115
|
+
node.status({ fill: 'blue', shape: 'ring', text: 'running' });
|
|
116
|
+
} else if (state === 'restarting') {
|
|
117
|
+
const label = retry ? `restarting (retry ${retry})` : 'restarting';
|
|
118
|
+
node.status({ fill: 'yellow', shape: 'ring', text: label });
|
|
203
119
|
} else {
|
|
204
|
-
node.
|
|
120
|
+
node.status({ fill: 'grey', shape: 'dot', text: 'stopped' });
|
|
205
121
|
}
|
|
206
|
-
}
|
|
207
|
-
|
|
122
|
+
}
|
|
208
123
|
|
|
209
|
-
|
|
210
|
-
node.on('close', async function() {
|
|
211
|
-
//// KILL PROCESSES AND ERASE FILES
|
|
212
|
-
node.executed = false;
|
|
213
|
-
node.status({ fill: "blue", shape: "ring", text: '(1/1)' });
|
|
214
|
-
// node.executingCode = 0
|
|
215
|
-
// node.waitingForExecuting = 0
|
|
216
|
-
// node.processKilled = true
|
|
217
|
-
// for (var pid in node.activeProcesses) {
|
|
218
|
-
// /* istanbul ignore else */
|
|
219
|
-
// if (node.activeProcesses.hasOwnProperty(pid)) {
|
|
220
|
-
// // process.kill(-pid, 9)
|
|
221
|
-
// terminate(pid)
|
|
222
|
-
// node.activeProcesses[pid] = null;
|
|
223
|
-
// node.warn(`Killing pid ${pid}`)
|
|
224
|
-
// }
|
|
225
|
-
// }
|
|
226
|
-
|
|
227
|
-
// for (let i = 0, len = node.tempFiles.length; i < len; i++) {
|
|
228
|
-
// await fsPromises.unlink(file);
|
|
229
|
-
// }
|
|
230
|
-
// node.activeProcesses = {};
|
|
231
|
-
});
|
|
124
|
+
// ── output helpers ────────────────────────────────────────────────────
|
|
232
125
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
126
|
+
// Emit one parsed segment via node.send(). Guards against post-kill emission.
|
|
127
|
+
function emitSegment(value) {
|
|
128
|
+
if (closing || desiredState === 'stopped') return;
|
|
129
|
+
const result = parseOutputValue(value, node.outputFormat);
|
|
130
|
+
if (result.error) {
|
|
131
|
+
node.error(result.error);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (node.fieldType === 'msg') {
|
|
135
|
+
const outMsg = {};
|
|
136
|
+
RED.util.setMessageProperty(outMsg, node.field, result.value);
|
|
137
|
+
node.send(outMsg);
|
|
138
|
+
} else {
|
|
139
|
+
const context = RED.util.parseContextStore(node.field);
|
|
140
|
+
const target = node.context()[node.fieldType];
|
|
141
|
+
const outMsg = {};
|
|
142
|
+
target.set(context.key, result.value, context.store, (err) => {
|
|
143
|
+
if (err) node.error(err);
|
|
144
|
+
else node.send(outMsg);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
236
147
|
}
|
|
237
148
|
|
|
238
|
-
|
|
239
|
-
|
|
149
|
+
// Route a stdout chunk: raw → emit immediately; delimited → buffer and split.
|
|
150
|
+
function handleChunk(chunk) {
|
|
151
|
+
if (closing || desiredState === 'stopped') return;
|
|
152
|
+
if (node.streamMode === 'raw') {
|
|
153
|
+
emitSegment(chunk);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
segmentBuffer += chunk;
|
|
157
|
+
const parts = segmentBuffer.split(node.delimiter);
|
|
158
|
+
segmentBuffer = parts.pop(); // keep incomplete remainder
|
|
159
|
+
for (const part of parts) {
|
|
160
|
+
if (part) emitSegment(part);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
240
163
|
|
|
241
|
-
|
|
242
|
-
const is_json = (node.outputFormat === "parsedJSON");
|
|
243
|
-
let template = node.template || msg.template;
|
|
244
|
-
const value = mustache.render(template, new NodeContext(msg, node.context(), null, is_json, resolvedTokens));
|
|
245
|
-
const tmpObj = await tmp.file();
|
|
246
|
-
await fsPromises.writeFile(tmpObj.path, value, 'utf8');
|
|
164
|
+
// ── process management ────────────────────────────────────────────────
|
|
247
165
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
} else {
|
|
253
|
-
resolvedCommand = node.cmd
|
|
166
|
+
function cleanupTempFile() {
|
|
167
|
+
if (currentTempFile) {
|
|
168
|
+
try { fs.unlinkSync(currentTempFile); } catch (_) {}
|
|
169
|
+
currentTempFile = null;
|
|
254
170
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function startProcess() {
|
|
174
|
+
if (closing || desiredState === 'stopped') return;
|
|
175
|
+
|
|
176
|
+
if (!node.command) {
|
|
177
|
+
node.warn('exec.service: no command configured');
|
|
178
|
+
desiredState = 'stopped';
|
|
179
|
+
updateStatus('stopped');
|
|
180
|
+
return;
|
|
263
181
|
}
|
|
264
182
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
183
|
+
// ── template → $file ─────────────────────────────────────────────
|
|
184
|
+
// If a template is configured, render it with Nunjucks, write it to
|
|
185
|
+
// a temp file, and wrap the command in a shell snippet that sets
|
|
186
|
+
// $file to that path — identical to how exec.queue works.
|
|
187
|
+
let effectiveCommand = node.command;
|
|
188
|
+
cleanupTempFile();
|
|
189
|
+
|
|
190
|
+
if (node.template) {
|
|
191
|
+
try {
|
|
192
|
+
const context = buildContext({}, node);
|
|
193
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
194
|
+
const rendered = env.renderString(node.template, context);
|
|
195
|
+
const tmpObj = tmp.fileSync();
|
|
196
|
+
currentTempFile = tmpObj.name;
|
|
197
|
+
fs.writeFileSync(tmpObj.name, rendered, 'utf8');
|
|
198
|
+
effectiveCommand = buildShellcode(tmpObj.name, node.command);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
node.error('exec.service template error: ' + e.message);
|
|
201
|
+
desiredState = 'stopped';
|
|
202
|
+
updateStatus('stopped');
|
|
203
|
+
return;
|
|
270
204
|
}
|
|
271
|
-
} finally {
|
|
272
|
-
await tmpObj.cleanup();
|
|
273
205
|
}
|
|
274
|
-
}
|
|
275
206
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
code: err.code,
|
|
283
|
-
killed: err.killed
|
|
284
|
-
};
|
|
285
|
-
msg.error_info = error;
|
|
286
|
-
node.error(`error (${msg.nodeName})\n\n${stderr}`, msg);
|
|
287
|
-
} else {
|
|
288
|
-
if (stderr) {
|
|
289
|
-
node.error(`warning (${msg.nodeName})\n\n${stderr}`, msg);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (stdout) {
|
|
293
|
-
stdout = stdout.trim();
|
|
294
|
-
if (node.splitLine === false) {
|
|
295
|
-
output(msg, stdout, send, done);
|
|
296
|
-
} else {
|
|
297
|
-
stdout = stdout.split('\n');
|
|
298
|
-
for (let i = 0; i < stdout.length; i++) {
|
|
299
|
-
node.emit("input", { "message": stdout[i] });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
delete node.activeProcesses[child.pid];
|
|
305
|
-
resolve();
|
|
306
|
-
});
|
|
307
|
-
node.activeProcesses[child.pid] = child.pid;
|
|
207
|
+
// Fresh decoder per process — avoids multibyte state leaking across restarts.
|
|
208
|
+
const decoder = new StringDecoder('utf8');
|
|
209
|
+
segmentBuffer = '';
|
|
210
|
+
|
|
211
|
+
const p = spawn(PLATFORM.shell, [...PLATFORM.args, effectiveCommand], {
|
|
212
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
308
213
|
});
|
|
309
|
-
|
|
214
|
+
proc = p;
|
|
310
215
|
|
|
311
|
-
|
|
312
|
-
return new Promise((resolve, reject) => {
|
|
313
|
-
const child = spawn('/bin/bash', ['-c', shellcode], node.spawnOpt);
|
|
314
|
-
node.activeProcesses[child.pid] = child.pid;
|
|
315
|
-
|
|
316
|
-
child.stdout.on('data', (data) => {
|
|
317
|
-
data = data.toString();
|
|
318
|
-
if (node.splitLine === false) {
|
|
319
|
-
output(msg, data, send, done);
|
|
320
|
-
} else {
|
|
321
|
-
const lines = data.split('\n');
|
|
322
|
-
for (let line of lines) {
|
|
323
|
-
if (line) {
|
|
324
|
-
node.emit("input", { "message": line });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
});
|
|
216
|
+
updateStatus('running');
|
|
329
217
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
218
|
+
// Reset retry counter if the process runs stably for 10 s.
|
|
219
|
+
if (successTimer) clearTimeout(successTimer);
|
|
220
|
+
successTimer = setTimeout(() => {
|
|
221
|
+
successTimer = null;
|
|
222
|
+
if (proc === p) retryCount = 0;
|
|
223
|
+
}, 10000);
|
|
333
224
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
code: code
|
|
339
|
-
};
|
|
340
|
-
msg.error_info = error;
|
|
341
|
-
node.error(`error (${msg.nodeName}): The node hasn't finished its execution`, msg);
|
|
342
|
-
reject(new Error(`Child process exited with code ${code}`));
|
|
343
|
-
}
|
|
344
|
-
delete node.activeProcesses[child.pid];
|
|
345
|
-
resolve();
|
|
346
|
-
});
|
|
225
|
+
p.stdout.on('data', (data) => {
|
|
226
|
+
if (proc !== p) return; // stale event after kill
|
|
227
|
+
const text = decoder.write(data);
|
|
228
|
+
if (text) handleChunk(text);
|
|
347
229
|
});
|
|
348
|
-
}
|
|
349
230
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
231
|
+
p.stderr.on('data', (data) => {
|
|
232
|
+
if (proc !== p) return;
|
|
233
|
+
const text = Buffer.isBuffer(data) ? data.toString() : String(data);
|
|
234
|
+
if (text && text.trim()) node.warn(text.trim());
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
p.on('error', (err) => {
|
|
238
|
+
if (proc !== p || closing) return;
|
|
239
|
+
proc = null;
|
|
240
|
+
if (successTimer) { clearTimeout(successTimer); successTimer = null; }
|
|
241
|
+
node.error(`exec.service error: ${(err && err.message) ? err.message : String(err)}`);
|
|
242
|
+
scheduleRestart();
|
|
243
|
+
});
|
|
358
244
|
|
|
359
|
-
|
|
360
|
-
|
|
245
|
+
p.on('close', (code) => {
|
|
246
|
+
if (proc !== p || closing) return;
|
|
247
|
+
proc = null;
|
|
248
|
+
if (successTimer) { clearTimeout(successTimer); successTimer = null; }
|
|
361
249
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
resolvedTokens[name] = RED.util.evaluateNodeProperty(env_name, 'env', node);
|
|
367
|
-
continue;
|
|
250
|
+
// Flush any remaining buffered segment on clean exit.
|
|
251
|
+
if (node.streamMode === 'delimited' && segmentBuffer && desiredState === 'running') {
|
|
252
|
+
emitSegment(segmentBuffer);
|
|
253
|
+
segmentBuffer = '';
|
|
368
254
|
}
|
|
369
255
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let type = context.type;
|
|
374
|
-
let store = context.store;
|
|
375
|
-
let field = context.field;
|
|
376
|
-
let target = node.context()[type];
|
|
377
|
-
if (target) {
|
|
378
|
-
resolvedTokens[name] = await new Promise((resolve, reject) => {
|
|
379
|
-
target.get(field, store, (err, val) => {
|
|
380
|
-
if (err) reject(err);
|
|
381
|
-
else resolve(val);
|
|
382
|
-
});
|
|
383
|
-
});
|
|
256
|
+
if (desiredState === 'running') {
|
|
257
|
+
if (code !== 0 && code !== null) {
|
|
258
|
+
node.warn(`exec.service: process exited (code ${code})`);
|
|
384
259
|
}
|
|
260
|
+
scheduleRestart();
|
|
385
261
|
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return resolvedTokens;
|
|
262
|
+
});
|
|
389
263
|
}
|
|
390
264
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return new Promise(async (resolve, reject) => {
|
|
394
|
-
try {
|
|
395
|
-
// Extract tokens from the command string
|
|
396
|
-
var tokens = extractTokens(mustache.parse(command));
|
|
397
|
-
|
|
398
|
-
var resolvedTokens = {};
|
|
399
|
-
|
|
400
|
-
// Iterate over the extracted tokens to resolve their values.
|
|
401
|
-
for (let name of tokens) {
|
|
402
|
-
let env_name = parseEnv(name);
|
|
403
|
-
if (env_name) {
|
|
404
|
-
resolvedTokens[name] = RED.util.evaluateNodeProperty(env_name, 'env', node);
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Check if the token refers to a flow or global context variable.
|
|
409
|
-
let context = parseContext(name);
|
|
410
|
-
if (context) {
|
|
411
|
-
let type = context.type;
|
|
412
|
-
let store = context.store;
|
|
413
|
-
let field = context.field;
|
|
414
|
-
let target = node.context()[type];
|
|
415
|
-
if (target) {
|
|
416
|
-
resolvedTokens[name] = await new Promise((innerResolve, innerReject) => {
|
|
417
|
-
target.get(field, store, (err, val) => {
|
|
418
|
-
if (err) innerReject(err);
|
|
419
|
-
else innerResolve(val);
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
265
|
+
function scheduleRestart() {
|
|
266
|
+
if (closing || desiredState === 'stopped') return;
|
|
425
267
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// Extract the variable names from the parsed tokens.
|
|
429
|
-
var variableNames = parsedCommand
|
|
430
|
-
.filter(token => token[0] === '&')
|
|
431
|
-
.map(token => token[1]);
|
|
432
|
-
|
|
433
|
-
var resolvedValues = {};
|
|
434
|
-
|
|
435
|
-
// Resolve the values for each variable.
|
|
436
|
-
for (let variableName of variableNames) {
|
|
437
|
-
let env_name = parseEnv(variableName);
|
|
438
|
-
if (env_name) {
|
|
439
|
-
resolvedValues[variableName] = RED.util.evaluateNodeProperty(env_name, 'env', node);
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Check if the variable refers to a flow or global context variable.
|
|
444
|
-
let context = parseContext(variableName);
|
|
445
|
-
if (context) {
|
|
446
|
-
let type = context.type;
|
|
447
|
-
let store = context.store;
|
|
448
|
-
let field = context.field;
|
|
449
|
-
let target = node.context()[type];
|
|
450
|
-
if (target) {
|
|
451
|
-
resolvedValues[variableName] = await new Promise((innerResolve, innerReject) => {
|
|
452
|
-
target.get(field, store, (err, val) => {
|
|
453
|
-
if (err) innerReject(err);
|
|
454
|
-
else innerResolve(val);
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
} else {
|
|
459
|
-
// If the variable is not in the context or an env variable, try to get it from the msg object.
|
|
460
|
-
resolvedValues[variableName] = msg[variableName];
|
|
461
|
-
}
|
|
462
|
-
}
|
|
268
|
+
retryCount++;
|
|
463
269
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
270
|
+
if (node.maxRetries > 0 && retryCount > node.maxRetries) {
|
|
271
|
+
node.error(`exec.service: max retries (${node.maxRetries}) exceeded — stopping`);
|
|
272
|
+
desiredState = 'stopped';
|
|
273
|
+
updateStatus('stopped');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
468
276
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
});
|
|
277
|
+
updateStatus('restarting', retryCount);
|
|
278
|
+
|
|
279
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
280
|
+
restartTimer = setTimeout(() => {
|
|
281
|
+
restartTimer = null;
|
|
282
|
+
if (!closing && desiredState === 'running') startProcess();
|
|
283
|
+
}, node.restartDelay);
|
|
476
284
|
}
|
|
477
285
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
} catch (error) {
|
|
491
|
-
parseError = true
|
|
492
|
-
node.error('Error parsing JSON: \n\n' + error)
|
|
493
|
-
}
|
|
286
|
+
// ── killAll ───────────────────────────────────────────────────────────
|
|
287
|
+
// Triggered by: msg.stop, HTTP endpoint, editor button, node.on('close').
|
|
288
|
+
|
|
289
|
+
function killAll() {
|
|
290
|
+
desiredState = 'stopped';
|
|
291
|
+
if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; }
|
|
292
|
+
if (successTimer) { clearTimeout(successTimer); successTimer = null; }
|
|
293
|
+
segmentBuffer = '';
|
|
294
|
+
if (proc) {
|
|
295
|
+
const p = proc;
|
|
296
|
+
proc = null; // nullify before kill so close handler skips restart
|
|
297
|
+
try { p.kill('SIGTERM'); } catch (_) {}
|
|
494
298
|
}
|
|
299
|
+
cleanupTempFile();
|
|
300
|
+
updateStatus('stopped');
|
|
301
|
+
}
|
|
495
302
|
|
|
496
|
-
|
|
497
|
-
if (node.outputFormat === "parsedXML") {
|
|
498
|
-
try {
|
|
499
|
-
//value = JSON.parse(convertXML.xml2json(value, {compact: true, spaces: 4}))
|
|
500
|
-
value = convertXML.xml2js(value, { compact: true, spaces: 4 })
|
|
501
|
-
} catch (error) {
|
|
502
|
-
parseError = true
|
|
503
|
-
node.error('Error parsing XML: \n\n' + error)
|
|
504
|
-
}
|
|
505
|
-
}
|
|
303
|
+
node.killAll = killAll;
|
|
506
304
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
parseError = true
|
|
513
|
-
node.error('Error parsing YAML: \n\n' + error)
|
|
514
|
-
}
|
|
515
|
-
} catch (error) {
|
|
516
|
-
parseError = true
|
|
517
|
-
node.error('Error parsing YAML: \n\n' + error)
|
|
518
|
-
}
|
|
519
|
-
}
|
|
305
|
+
// ── restartService ────────────────────────────────────────────────────
|
|
306
|
+
// User-driven restart. Kills any running process, resets all failure
|
|
307
|
+
// state, sets desiredState = 'running', and spawns immediately.
|
|
308
|
+
// Distinct from killAll (which sets desiredState = 'stopped') and from
|
|
309
|
+
// the crash-restart path (scheduleRestart) — this is always intentional.
|
|
520
310
|
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
} else {
|
|
535
|
-
send(msg);
|
|
536
|
-
done();
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
}
|
|
311
|
+
function restartService() {
|
|
312
|
+
if (closing) return;
|
|
313
|
+
|
|
314
|
+
// Cancel any pending crash-restart timer so it doesn't race.
|
|
315
|
+
if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; }
|
|
316
|
+
if (successTimer) { clearTimeout(successTimer); successTimer = null; }
|
|
317
|
+
|
|
318
|
+
// Kill the current process if alive, marking it stale first so its
|
|
319
|
+
// close handler won't trigger another scheduleRestart.
|
|
320
|
+
if (proc) {
|
|
321
|
+
const p = proc;
|
|
322
|
+
proc = null;
|
|
323
|
+
try { p.kill('SIGTERM'); } catch (_) {}
|
|
540
324
|
}
|
|
325
|
+
|
|
326
|
+
// Reset failure counters — this is a fresh start, not a retry.
|
|
327
|
+
retryCount = 0;
|
|
328
|
+
segmentBuffer = '';
|
|
329
|
+
|
|
330
|
+
// Ensure desiredState is running even if service was manually stopped.
|
|
331
|
+
desiredState = 'running';
|
|
332
|
+
|
|
333
|
+
updateStatus('restarting');
|
|
334
|
+
startProcess();
|
|
541
335
|
}
|
|
542
336
|
|
|
337
|
+
node.restartService = restartService;
|
|
338
|
+
|
|
339
|
+
// ── close handler ─────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
node.on('close', function(done) {
|
|
342
|
+
closing = true;
|
|
343
|
+
killAll();
|
|
344
|
+
done();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ── start ─────────────────────────────────────────────────────────────
|
|
348
|
+
// Process starts immediately at deploy time.
|
|
349
|
+
updateStatus('stopped');
|
|
350
|
+
startProcess();
|
|
543
351
|
}
|
|
544
352
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
RED.
|
|
548
|
-
|
|
353
|
+
RED.nodes.registerType('exec.service', ExecServiceNode);
|
|
354
|
+
|
|
355
|
+
RED.httpAdmin.post('/exec-service/:id/kill', function(req, res) {
|
|
356
|
+
const n = RED.nodes.getNode(req.params.id);
|
|
357
|
+
if (n && typeof n.killAll === 'function') {
|
|
358
|
+
n.killAll();
|
|
359
|
+
res.sendStatus(200);
|
|
360
|
+
} else {
|
|
361
|
+
res.sendStatus(404);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
RED.httpAdmin.post('/exec-service/:id/restart', function(req, res) {
|
|
366
|
+
const n = RED.nodes.getNode(req.params.id);
|
|
367
|
+
if (n && typeof n.restartService === 'function') {
|
|
368
|
+
n.restartService();
|
|
369
|
+
res.sendStatus(200);
|
|
370
|
+
} else {
|
|
371
|
+
res.sendStatus(404);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
RED.httpAdmin.get('/exec-service/:id/status', function(req, res) {
|
|
376
|
+
const n = RED.nodes.getNode(req.params.id);
|
|
377
|
+
if (n) {
|
|
378
|
+
res.json({
|
|
379
|
+
running: n._serviceState === 'running',
|
|
380
|
+
restarting: n._serviceState === 'restarting'
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
res.sendStatus(404);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
};
|