-
Notifications
You must be signed in to change notification settings - Fork 6
/
index.js
438 lines (380 loc) · 12.1 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
'use strict'
const fs = require('fs')
const path = require('path')
const proc = require('child_process')
const stripAnsi = require('strip-ansi')
const ProcessInfo = require('./process-info')
module.exports = {
start,
restartByName,
stopByName,
stop,
stopAll,
onstopping,
forEach,
}
/* understand/
* PROCESS REGISTRY
*/
let REG = []
function forEach(cb) { REG.forEach(cb) }
/* understand/
* Shutdown hook to be called before shutdown (should be called only
* once)
*/
let ONSTOPPING
let ONSTOPPING_CALLED
/* outcome/
* We get the values we require from the user, set up some defaults, and
* start the given process depending on what type it is
*/
function start(pi, cb) {
if(!cb) cb = (err, pid) => {
if(err && pid) {
if(pi && pi.name) console.error(pi.name, err, pid)
else console.error(err, pid)
} else if(err) {
if(pi && pi.name) console.error(pi.name, err)
else console.error(err)
} else {
console.log(`Starting ${pi.name} on ${pi.cwd} - pid (${pi.child.pid})`)
}
}
if(!pi) return cb(`Cannot start process without any information`)
if(!pi.script && !pi.cwd) return cb(`Cannot start process without 'script' or 'cwd'`)
pi = {
name: pi.name,
script: pi.script,
args: pi.args,
cwd: pi.cwd || process.cwd(),
log: pi.log,
stripANSI: pi.stripANSI,
restartAt: pi.restartAt,
restartOk: pi.restartOk,
env: pi.env,
cb: cb,
}
pi = Object.assign(new ProcessInfo(), pi);
if(!pi.restartAt) pi.restartAt = [100,500,1000,30*1000,60*1000,5*60*1000,15*60*1000]
if(!pi.restartOk) pi.restartOk = 30 * 60 * 1000
get_script_1(pi, (script) => {
pi._script = script
if(!pi._script) return cb(`Failed getting program to run`)
fixAsarIssue(pi)
let handler = getScriptHandler(script)
if(!handler) return cb(`Don't know how to start ${script}`)
REG.push(pi)
handler(pi)
cb(null, pi)
})
/**
* outcome/
* This will check the given script or CWD is inside asar file or not.
* If given script or CWD is inside, it will change the script and CWD
* to as per asar child process support.
* else this will keep same
* @param {*} pi
*/
function fixAsarIssue(pi) {
if (pi.cwd.includes('/app.asar/') ||
pi.cwd.includes('\\app.asar\\')) {
let p = pi.cwd.split('app.asar')
if (p.length == 2){
pi._script = path.join('app.asar', p[1], pi._script)
pi.cwd = p[0]
}
} else if (pi._script.includes('/app.asar/') ||
pi._script.includes('\\app.asar\\')) {
let p = pi._script.split('app.asar')
if (p.length == 2){
pi._script = path.join('app.asar', p[1])
pi.cwd = p[0]
}
}
}
/* understand/
* A nodejs module contains a 'package.json' file which generally
* gives the 'main' entry script for the module. So we can use this
* to find the script to run if we haven't been given it.
*
* outcome/
* If the script is provided, we use that. Otherwise we check if we
* are a node module and try and derive the script from the
* 'package.json'.
*/
function get_script_1(pi, cb) {
if(pi.script) return cb(pi.script)
let pkg = path.join(pi.cwd, 'package.json')
fs.readFile(pkg, (err, data) => {
if(err) {
pi.emit('error', err)
cb()
} else {
try {
let obj = JSON.parse(data)
cb(obj.main)
} catch(e) {
pi.emit('error', err)
cb()
}
}
})
}
}
function restartByName(name) {
REG.forEach((pi) => {
if(pi.name === name) restart(pi)
})
}
function stopByName(name) {
REG.forEach((pi) => {
if(pi.name === name) stop(pi)
})
}
function stopAll() { REG.forEach(pi => stop(pi)) }
/* outcome/
* Set the 'onstopping' hook which is called before the process shuts
* down.
*/
function onstopping(hook) {
ONSTOPPING = hook
ONSTOPPING_CALLED = false
process.on('message', (m) => {
if(m.stopping) callOnStoppingHook_1()
})
process.on('beforeExit', (code) => callOnStoppingHook_1())
process.on('exit', (code) => callOnStoppingHook_1())
function callOnStoppingHook_1() {
if(ONSTOPPING_CALLED) return
ONSTOPPING_CALLED = true
ONSTOPPING()
}
}
/* outcome/
* Restart the requested process by stopping it and then starting it
* again.
*/
function restart(pi) {
stop(pi, () => startAgain(pi))
}
/* outcome/
* This function finds the appropriate handler for the process and
* starts the process again marking the time it has been restarted.
* It assumes that the process has been correctly started/setup before
* and stopped so it does no error checking
*/
function startAgain(pi) {
let handler = getScriptHandler(pi._script)
if(!handler) pi.emit('error', `Don't know how to restart ${pi._script}`)
handler(pi)
pi.stopRequested = false
pi.lastStart = Date.now()
pi.emit('restart')
}
/* outcome/
* Send a message to the child to stop and wait a bit to see if it
* complies. If it does fine, otherwise try to kill it.
*/
function stop(pi, cb) {
pi.stopRequested = true
if(pi.restartInProgress) clearTimeout(pi.restartInProgress)
if(!pi.child || pi.child.exitCode !== null) {
cb && cb()
return
}
try {
if(pi.child) {
if(pi.child.send) pi.child.send({ stopping: true })
pi.child.kill()
}
cb && cb()
} catch(err) {
console.error(err)
pi.emit('error', err)
}
}
/* problem/
* Depending on the type of file we need to run, return a handler that
* can launch that type.
* way/
* Use the extension of the file to determine it's type and then return
* a matching handler
*/
function getScriptHandler(script) {
if(!script) return
let handlers = {
".js" : launchJSProcess,
".py" : launchPythonProcess,
}
let ext = path.extname(script)
if(ext) return handlers[ext]
}
/* outcome/
* We use the standard `child_process.spawn` function to launch a python
* process with the given script as the first argument. Then we capture
* the output and handle process exits.
*/
function launchPythonProcess(pi) {
let opts = {
windowsHide: false,
detached: false,
}
if(pi.cwd) opts.cwd = pi.cwd
if(pi.env) opts.env = pi.env
if(!pi.args) pi.args = [pi._script]
else pi.args = [pi._script].concat(pi.args)
pi.child = proc.spawn('python', pi.args, opts)
pi.flush = captureOutput(pi)
handleExit(pi)
}
/* understand/
* To launch the requested process as a new NodeJS process, we use a
* special node js function (`child_process.fork`) that launches other
* nodejs processes and creates a connection with them so we can
* communicate via messages. This both (a) allows us to use the electron
* embedded NodeJS and allows us to send messages requesting the child
* to shutdown when we are shutting down ourselves.
*
* outcome/
* Launch the child process using `child_process.fork`, capturing the
* output and handling what happens when the process exits.
*/
function launchJSProcess(pi) {
let opts = {
silent: true,
detached: false,
}
if(pi.cwd) opts.cwd = pi.cwd
if(pi.env) opts.env = pi.env
if(pi.stdio) opts.stdio = pi.stdio
if(!pi.args) pi.args = []
pi.child = proc.fork(pi._script, pi.args, opts)
pi.flush = captureOutput(pi)
handleExit(pi)
}
/* outcome/
* As data comes in either the error or output stream we capture it and
* show individual lines.
*/
function captureOutput(pi) {
if(!pi.child) return () => "Doing Nothing"
let op = ""
let er = ""
pi.child.stdout.on('data', (data) => {
op += data
op = show_lines_1(op)
})
pi.child.stderr.on('data', (data) => {
er += data
er = show_lines_1(er, true)
})
return flush
function flush() {
if(op && op.trim()) out(pi, op.trim())
if(er && er.trim()) out(pi, er.trim(), true)
op = ""
er = ""
}
function show_lines_1(f, iserr) {
if(!f) return f
let lines = f.split(/[\n\r]+/)
for(let i = 0;i < lines.length-1;i++) {
out(pi, lines[i], iserr)
}
return lines[lines.length-1]
}
/* outcome/
* Given a log file we output to the log file. If no log file is
* given we output to stdout/stderr.
*/
function out(pi, line, iserr) {
if(pi.stripANSI) line = stripAnsi(line)
if(pi.log) {
if(pi.name) line = `${pi.name}: ${line}\n`
else line = line + '\n'
fs.appendFile(pi.log, line, (err) => {
if(err) {
console.error(err)
}
})
} else {
if(pi.name) line = `${pi.name}: ${line}`
if(iserr) console.error(line)
else console.log(line)
}
}
}
/* understand/
* The ChildProcess is an `EventEmitter` with the following events:
* + 'error': Failed to start the given process
* + 'exit': Process exited (fires sometimes)
* + 'close': Process exited cleanly
* `exit` and `close` may both be fired or not.
*
* outcome/
* If there is an error, exit, or close, we flush whatever data we have
* so far and then callback with the error or completion and clear the
* child process. Allow the process to restart if needed.
*/
function handleExit(pi) {
if(!pi.child) return
const child = pi.child
child.on('error', (err) => {
if(child == pi.child) pi.child = null
pi.flush && pi.flush()
pi.emit('error', err)
restartIfNeeded(pi)
})
child.on('exit', on_done_1)
child.on('close', on_done_1)
let prevcode, prevsignal
function on_done_1(code, signal) {
if(child == pi.child) pi.child = null
pi.flush && pi.flush()
if(code == prevcode && signal == prevsignal) return
prevcode = code
prevsignal = signal
if(code && code) {
pi.emit('exit',`Exited with error`, child.pid)
} else if(signal) {
pi.emit('exit',`Killed`, child.pid)
} else {
pi.emit('exit','Process exit')
}
restartIfNeeded(pi)
}
}
/* outcome/
* If the process is not running, check which restart interval is needed
* then launch a restart after that time. Note that if the `restartAt`
* parameter is the special parameters `[]` or `[0]` or if the process
* is already started somehow we don't start it again.
*/
function restartIfNeeded(pi) {
if(!pi.restartAt || pi.restartAt.length == 0) return
if(pi.restartAt.length == 1 && pi.restartAt[0] == 0) return
if(pi.child && pi.child.exitCode === null) return
if(pi.stopRequested) return
if(pi.restartInProgress) return
let intv = get_restart_interval_1()
pi.restartInProgress = setTimeout(() => {
pi.restartInProgress = false
if(pi.stopRequested) return
if(!pi.child || pi.child.exitCode !== null) startAgain(pi)
}, intv)
/* outcome/
* The restartAt[] parameter gives a list of times (usually
* increasing) at which to attempt the restart of this process.
* We keep track of the current index and go all the way to the end.
* If the process has been running successfully for `restartOk` we go
* back to the begginning cycle again.
*/
function get_restart_interval_1() {
let ndx = pi.restartAtNdx ? pi.restartAtNdx : 0
if(pi.lastStart && (Date.now() - pi.lastStart > pi.restartOk)) ndx = 0
if(!ndx) pi.restartAtNdx = 1
else pi.restartAtNdx += 1
if(pi.restartAtNdx >= pi.restartAt.length) pi.restartAtNdx = pi.restartAt.length-1
return pi.restartAt[ndx]
}
}