forked from jhuckaby/Cronicle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
run-detached.js
executable file
·142 lines (114 loc) · 4.1 KB
/
run-detached.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
#!/usr/bin/env node
// Detached Plugin Runner for Cronicle
// Copyright (c) 2015 - 2017 Joseph Huckaby
// Released under the MIT License
var fs = require('fs');
var os = require('os');
var cp = require('child_process');
var path = require('path');
var sqparse = require('shell-quote').parse;
var JSONStream = require('pixl-json-stream');
var Tools = require('pixl-tools');
var args = process.argv.slice(-2);
if (!args[1] || !args[1].match(/\.json$/)) {
throw new Error("Usage: ./run-detached.js detached /PATH/TO/JSON/FILE.json");
}
var job_file = args[1];
var job = require( job_file );
fs.unlink( job_file, function(err) {;} );
var child_cmd = job.command;
var child_args = [];
// if command has cli args, parse using shell-quote
if (child_cmd.match(/\s+(.+)$/)) {
var cargs_raw = RegExp.$1;
child_cmd = child_cmd.replace(/\s+(.+)$/, '');
child_args = sqparse( cargs_raw, process.env );
}
var child = cp.spawn( child_cmd, child_args, {
stdio: ['pipe', 'pipe', fs.openSync(job.log_file, 'a')]
} );
var updates = { detached_pid: child.pid };
var kill_timer = null;
var update_timer = null;
var cstream = new JSONStream( child.stdout, child.stdin );
cstream.recordRegExp = /^\s*\{.+\}\s*$/;
cstream.on('json', function(data) {
// received JSON data from child
// store in object and send to Cronicle on exit
for (var key in data) updates[key] = data[key];
} );
cstream.on('text', function(line) {
// received non-json text from child, just log it
fs.appendFileSync(job.log_file, line);
} );
cstream.on('error', function(err, text) {
// Probably a JSON parse error (child emitting garbage)
if (text) fs.appendFileSync(job.log_file, text + "\n");
} );
child.on('error', function (err) {
// child error
updates = {};
if (kill_timer) clearTimeout(kill_timer);
if (update_timer) clearTimeout(update_timer);
var queue_file = job.queue_dir + '/' + job.id + '-' + Date.now() + '.json';
fs.writeFileSync( queue_file + '.tmp', JSON.stringify({
action: "detachedJobUpdate",
id: job.id,
complete: 1,
code: 1,
description: "Script failed: " + Tools.getErrorDescription(err)
}) );
fs.renameSync( queue_file + '.tmp', queue_file );
} );
child.on('exit', function (code, signal) {
// child exited
if (kill_timer) clearTimeout(kill_timer);
if (update_timer) clearTimeout(update_timer);
code = (code || signal || 0);
if (code && !updates.code) {
updates.code = code;
updates.description = "Plugin exited with code: " + code;
}
updates.action = "detachedJobUpdate";
updates.id = job.id;
updates.complete = 1;
updates.time_end = Tools.timeNow();
// write file atomically, just in case
var queue_file = job.queue_dir + '/' + job.id + '-complete.json';
fs.writeFileSync( queue_file + '.tmp', JSON.stringify(updates) );
fs.renameSync( queue_file + '.tmp', queue_file );
} );
// silence EPIPE errors on child STDIN
child.stdin.on('error', function(err) {
// ignore
} );
// send initial job + params
cstream.write( job );
// we're done writing to the child -- don't hold open its stdin
child.stdin.end();
// send updates every N seconds, if the child sent us anything (i.e. progress updates)
// randomize interval so we don't bash the queue dir when multiple detached jobs are running
update_timer = setInterval( function() {
if (Tools.numKeys(updates) && !updates.complete) {
updates.action = "detachedJobUpdate";
updates.id = job.id;
updates.in_progress = 1;
// write file atomically, just in case
var queue_file = job.queue_dir + '/' + job.id + '-' + Date.now() + '.json';
fs.writeFileSync( queue_file + '.tmp', JSON.stringify(updates) );
fs.renameSync( queue_file + '.tmp', queue_file );
updates = {};
}
}, 30000 + Math.floor( Math.random() * 25000 ) );
// Handle termination (server shutdown or job aborted)
process.on('SIGTERM', function() {
// console.log("Caught SIGTERM, killing child: " + child.pid);
if (update_timer) clearTimeout(update_timer);
kill_timer = setTimeout( function() {
// child didn't die, kill with prejudice
// console.log("Child did not exit, killing harder: " + child.pid);
child.kill('SIGKILL');
}, 9 * 1000 );
// try killing nicely first
child.kill('SIGTERM');
} );