@pixlcore/xyrun 1.0.0
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/LICENSE.md +28 -0
- package/README.md +74 -0
- package/main.js +979 -0
- package/package.json +29 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 - 2026 PixlCore LLC & CONTRIBUTORS.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
**xyOps Remote Job Runner (xyRun)** is a companion to the [xyOps](https://xyops.io) workflow automation and server monitoring platform. It is a wrapper for running remote jobs inside Docker containers, or over a remote SSH connection.
|
|
4
|
+
|
|
5
|
+
The idea is that when a job is running "remotely" (i.e. not a direct child process of [xySat](https://github.com/pixlcore/xysat)) then we cannot monitor system resources for the job. Also, input and output files simply do not work in these cases (because xySat expects them to be on the local filesystem where it is running). xyRun handles all these complexites for you by sitting "in between" your job and xySat. xyRun should run *inside* the container or on the far end of the SSH connection, where your job process is running.
|
|
6
|
+
|
|
7
|
+
To use xyRun in a xyOps Event Plugin, make sure you set the Plugin's `runner` property to `true`. This hint tells xyOps (and ultimately xySat) that the job is running remotely out if its reach, and it should not perform the usual process and network monitoring, and file management. Those duties get delegated to xyRun.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Handles monitoring procesess, network connections, CPU and memory usage of remote jobs, and passing those metrics back to xyOps.
|
|
12
|
+
- Handles input files by creating a temporary directory for you job and pre-downloading all files from the xyOps master server.
|
|
13
|
+
- Handles output files by intercepting the `files` message and uploading them directly to the xyOps master server.
|
|
14
|
+
|
|
15
|
+
# Installation
|
|
16
|
+
|
|
17
|
+
xyRun requires both Node.js LTS and NPM.
|
|
18
|
+
|
|
19
|
+
## Docker
|
|
20
|
+
|
|
21
|
+
In your Dockerfile, preinstall Node.js LTS if needed (assuming Ubuntu / Debian base):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash
|
|
25
|
+
RUN apt-get update && apt-get install -y nodejs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
And preinstall xyRun like this:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
RUN npm install -g @pixlcore/xyrun
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then wrap your `CMD` with a `xyrun` prefix like this:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
CMD xyrun node /path/to/your-script.js
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
xyRun will directly launch whatever is passed to it on the CLI, including arguments.
|
|
41
|
+
|
|
42
|
+
## Other
|
|
43
|
+
|
|
44
|
+
For other uses (i.e. SSH) install the NPM module globally on the target machine where the remote job will be running:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm install -g @pixlcore/xyrun
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then wrap your remote command with a `xyrun` prefix:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
ssh user@target xyrun node /path/to/your-script.js
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
# Development
|
|
57
|
+
|
|
58
|
+
You can install the source code by using [Git](https://en.wikipedia.org/wiki/Git) ([Node.js](https://nodejs.org/) is also required):
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
git clone https://github.com/pixlcore/xyrun.git
|
|
62
|
+
cd xyrun
|
|
63
|
+
npm install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You can then pass it mock job data by issuing a pipe command such as:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
echo '{"xy":1, "runner":true}' | node main.js node your-script-here.js
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
# License
|
|
73
|
+
|
|
74
|
+
See [LICENSE.md](LICENSE.md) in this repository.
|
package/main.js
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// xyRun - xyOps Remote Job Runner - Main entry point
|
|
4
|
+
// Copyright (c) 2019 - 2025 PixlCore LLC
|
|
5
|
+
// BSD 3-Clause License -- see LICENSE.md
|
|
6
|
+
|
|
7
|
+
const Path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const cp = require('child_process');
|
|
11
|
+
const JSONStream = require('pixl-json-stream');
|
|
12
|
+
const PixlRequest = require('pixl-request');
|
|
13
|
+
const Tools = require('pixl-tools');
|
|
14
|
+
const pkg = require('./package.json');
|
|
15
|
+
const noop = function() {};
|
|
16
|
+
|
|
17
|
+
const app = {
|
|
18
|
+
|
|
19
|
+
activeJobs: {},
|
|
20
|
+
kids: {},
|
|
21
|
+
procCache: {},
|
|
22
|
+
connCache: {},
|
|
23
|
+
|
|
24
|
+
async run() {
|
|
25
|
+
// read job from stdin
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of process.stdin) { chunks.push(chunk); }
|
|
28
|
+
this.job = JSON.parse( chunks.join('') );
|
|
29
|
+
|
|
30
|
+
// create a http request instance for various tasks
|
|
31
|
+
this.request = new PixlRequest( "xyOps Job Runner v" + pkg.version );
|
|
32
|
+
this.request.setTimeout( 300 * 1000 );
|
|
33
|
+
this.request.setFollow( 5 );
|
|
34
|
+
this.request.setAutoError( true );
|
|
35
|
+
this.request.setKeepAlive( true );
|
|
36
|
+
|
|
37
|
+
// prime this for repeated calls
|
|
38
|
+
this.numCPUs = os.cpus().length;
|
|
39
|
+
|
|
40
|
+
// sniff platform
|
|
41
|
+
this.platform = {};
|
|
42
|
+
switch (process.platform) {
|
|
43
|
+
case 'linux': this.platform.linux = true; break;
|
|
44
|
+
case 'darwin': this.platform.darwin = true; break;
|
|
45
|
+
case 'freebsd': case 'openbsd': case 'netbsd': this.platform.bsd = true; break;
|
|
46
|
+
case 'win32': this.platform.windows = true; break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.platform.linux) {
|
|
50
|
+
// pre-calc location of some binaries
|
|
51
|
+
this.psBin = Tools.findBinSync('ps');
|
|
52
|
+
this.ssBin = Tools.findBinSync('ss');
|
|
53
|
+
} // linux
|
|
54
|
+
|
|
55
|
+
if (this.platform.darwin) {
|
|
56
|
+
// pre-calc location of some binaries
|
|
57
|
+
this.psBin = Tools.findBinSync('ps');
|
|
58
|
+
} // darwin
|
|
59
|
+
|
|
60
|
+
// launch child process
|
|
61
|
+
this.prepLaunchJob();
|
|
62
|
+
|
|
63
|
+
// start ticker
|
|
64
|
+
this.timer = setInterval( this.tick.bind(this), 1000 );
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
prepLaunchJob() {
|
|
68
|
+
// setup temp dir for job, and download any files passed to us
|
|
69
|
+
var self = this;
|
|
70
|
+
var job = this.job;
|
|
71
|
+
|
|
72
|
+
// no input files? skip prep, and no temp dir needed
|
|
73
|
+
if (!job.input || !job.input.files || !job.input.files.length) {
|
|
74
|
+
this.launchJob();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async.series([
|
|
79
|
+
function(callback) {
|
|
80
|
+
// create temp dir for job, full access
|
|
81
|
+
Tools.mkdirp( job.cwd, { mode: 0o777 }, callback );
|
|
82
|
+
},
|
|
83
|
+
function(callback) {
|
|
84
|
+
// download input files to job temp dir if we were given any
|
|
85
|
+
async.eachSeries( job.input.files,
|
|
86
|
+
function(file, callback) {
|
|
87
|
+
var dest_file = Path.join( job.cwd, file.filename );
|
|
88
|
+
var url = job.base_url + '/' + file.path;
|
|
89
|
+
var opts = Tools.mergeHashes( job.socket_opts || {}, {
|
|
90
|
+
download: dest_file
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log( `Downloading file: ${dest_file} (${Tools.getTextFromBytes(file.size)})` );
|
|
94
|
+
|
|
95
|
+
self.request.get( url, opts, function(err, resp, data, perf) {
|
|
96
|
+
if (err) {
|
|
97
|
+
return callback( new Error("Failed to download job file: " + file.filename + ": " + (err.message || err)) );
|
|
98
|
+
}
|
|
99
|
+
callback();
|
|
100
|
+
} ); // request.get
|
|
101
|
+
},
|
|
102
|
+
callback
|
|
103
|
+
); // eachSeries
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
function(err) {
|
|
107
|
+
if (err) {
|
|
108
|
+
// something went wrong
|
|
109
|
+
job.pid = 0;
|
|
110
|
+
job.code = 1;
|
|
111
|
+
job.description = "Runner: " + err;
|
|
112
|
+
console.error(job.description);
|
|
113
|
+
self.activeJobs[ job.id ] = job;
|
|
114
|
+
self.finishJob( job );
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// launch job for real
|
|
119
|
+
self.launchJob();
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
launchJob() {
|
|
124
|
+
// launch job on this server!
|
|
125
|
+
var self = this;
|
|
126
|
+
var job = this.job;
|
|
127
|
+
var child = null;
|
|
128
|
+
var worker = {};
|
|
129
|
+
|
|
130
|
+
var child_args = process.argv.slice(2);
|
|
131
|
+
var child_cmd = child_args.shift();
|
|
132
|
+
|
|
133
|
+
if (!child_cmd) {
|
|
134
|
+
// no command!
|
|
135
|
+
job.pid = 0;
|
|
136
|
+
job.code = 1;
|
|
137
|
+
job.description = "Runner: No command specified.";
|
|
138
|
+
console.error(job.description);
|
|
139
|
+
self.activeJobs[ job.id ] = job;
|
|
140
|
+
self.finishJob();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!job.runner) {
|
|
144
|
+
// not in runner mode
|
|
145
|
+
job.pid = 0;
|
|
146
|
+
job.code = 1;
|
|
147
|
+
job.description = "Job not launched in runner mode (Set runner flag in event plugin).";
|
|
148
|
+
console.error(job.description);
|
|
149
|
+
self.activeJobs[ job.id ] = job;
|
|
150
|
+
self.finishJob();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log( "Running command: " + child_cmd, child_args );
|
|
155
|
+
|
|
156
|
+
// setup environment for child
|
|
157
|
+
var child_opts = {
|
|
158
|
+
env: Object.assign( {}, process.env )
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// get uid / gid info for child env vars
|
|
162
|
+
if (!this.platform.windows) {
|
|
163
|
+
child_opts.uid = job.uid || process.getuid();
|
|
164
|
+
child_opts.gid = process.getgid();
|
|
165
|
+
|
|
166
|
+
var user_info = Tools.getpwnam( child_opts.uid, true );
|
|
167
|
+
if (user_info) {
|
|
168
|
+
child_opts.uid = user_info.uid;
|
|
169
|
+
child_opts.gid = user_info.gid;
|
|
170
|
+
child_opts.env.USER = child_opts.env.USERNAME = user_info.username;
|
|
171
|
+
child_opts.env.HOME = user_info.dir;
|
|
172
|
+
child_opts.env.SHELL = user_info.shell;
|
|
173
|
+
}
|
|
174
|
+
else if (child_opts.uid != process.getuid()) {
|
|
175
|
+
// user not found
|
|
176
|
+
job.pid = 0;
|
|
177
|
+
job.code = 1;
|
|
178
|
+
job.description = "Plugin Error: User does not exist: " + child_opts.uid;
|
|
179
|
+
console.error(job.description);
|
|
180
|
+
this.activeJobs[ job.id ] = job;
|
|
181
|
+
this.finishJob();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (job.gid) {
|
|
186
|
+
var grp_info = Tools.getgrnam( job.gid, true );
|
|
187
|
+
if (grp_info) {
|
|
188
|
+
child_opts.gid = grp_info.gid;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// gid not found
|
|
192
|
+
job.pid = 0;
|
|
193
|
+
job.code = 1;
|
|
194
|
+
job.description = "Plugin Error: Group does not exist: " + job.gid;
|
|
195
|
+
console.error(job.description);
|
|
196
|
+
this.activeJobs[ job.id ] = job;
|
|
197
|
+
this.finishJob();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
child_opts.uid = parseInt( child_opts.uid );
|
|
203
|
+
child_opts.gid = parseInt( child_opts.gid );
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// windows additions
|
|
207
|
+
if (this.platform.windows) {
|
|
208
|
+
child_opts.windowsHide = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// attach streams
|
|
212
|
+
child_opts.stdio = ['pipe', 'pipe', 'inherit'];
|
|
213
|
+
|
|
214
|
+
// spawn child
|
|
215
|
+
try {
|
|
216
|
+
child = cp.spawn( child_cmd, child_args, child_opts );
|
|
217
|
+
if (!child || !child.pid || !child.stdin || !child.stdout) {
|
|
218
|
+
throw new Error("Child process failed to spawn (Check executable location and permissions?)");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (child) child.on('error', function() {}); // prevent crash
|
|
223
|
+
job.pid = 0;
|
|
224
|
+
job.code = 1;
|
|
225
|
+
job.description = "Runner: Child spawn error: " + child_cmd + ": " + Tools.getErrorDescription(err);
|
|
226
|
+
console.error(job.description);
|
|
227
|
+
this.activeJobs[ job.id ] = job;
|
|
228
|
+
this.finishJob();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
job.pid = child.pid || 0;
|
|
232
|
+
|
|
233
|
+
// connect json stream to child's stdio
|
|
234
|
+
// order reversed deliberately (out, in)
|
|
235
|
+
var stream = new JSONStream( child.stdout, child.stdin );
|
|
236
|
+
stream.recordRegExp = /^\s*\{.+\}\s*$/;
|
|
237
|
+
stream.preserveWhitespace = true;
|
|
238
|
+
stream.maxLineLength = 1024 * 1024;
|
|
239
|
+
stream.EOL = "\n";
|
|
240
|
+
|
|
241
|
+
worker.pid = job.pid;
|
|
242
|
+
worker.child = child;
|
|
243
|
+
worker.stream = stream;
|
|
244
|
+
this.worker = worker;
|
|
245
|
+
|
|
246
|
+
stream.on('json', function(data) {
|
|
247
|
+
// received data from child
|
|
248
|
+
if (!self.handleChildResponse(job, worker, data)) {
|
|
249
|
+
// unrecognized json, emit as raw text
|
|
250
|
+
stream.emit('text', JSON.stringify(data) + "\n");
|
|
251
|
+
}
|
|
252
|
+
} );
|
|
253
|
+
|
|
254
|
+
stream.on('text', function(line) {
|
|
255
|
+
// received non-json text from child, log it
|
|
256
|
+
if (self.platform.windows) line = line.replace(/\r$/, '');
|
|
257
|
+
process.stdout.write(line);
|
|
258
|
+
} );
|
|
259
|
+
|
|
260
|
+
stream.on('error', function(err, text) {
|
|
261
|
+
// Probably a JSON parse error (child emitting garbage)
|
|
262
|
+
console.error( "Child stream error: Job ID " + job.id + ": PID " + job.pid + ": " + err );
|
|
263
|
+
if (text) process.stdout.write(text);
|
|
264
|
+
} );
|
|
265
|
+
|
|
266
|
+
child.on('error', function (err) {
|
|
267
|
+
// child error
|
|
268
|
+
job.code = 1;
|
|
269
|
+
job.description = "Runner: Child process error: " + Tools.getErrorDescription(err);
|
|
270
|
+
worker.child_exited = true;
|
|
271
|
+
console.error(job.description);
|
|
272
|
+
self.finishJob();
|
|
273
|
+
} );
|
|
274
|
+
|
|
275
|
+
child.on('close', function (code, signal) {
|
|
276
|
+
// child exited
|
|
277
|
+
if (code || signal) {
|
|
278
|
+
console.error( "Child exited with code: " + (code || signal) );
|
|
279
|
+
}
|
|
280
|
+
worker.child_exited = true;
|
|
281
|
+
self.finishJob();
|
|
282
|
+
} ); // on exit
|
|
283
|
+
|
|
284
|
+
// pass job to child
|
|
285
|
+
worker.child.stdin.write( JSON.stringify(job) + "\n" );
|
|
286
|
+
|
|
287
|
+
// we're done writing to the child -- don't hold its stdin open
|
|
288
|
+
worker.child.stdin.end();
|
|
289
|
+
|
|
290
|
+
// track job in our own hash
|
|
291
|
+
this.activeJobs[ job.id ] = job;
|
|
292
|
+
this.kids[ job.pid ] = worker;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
handleChildResponse(job, worker, data) {
|
|
296
|
+
// intercept child responses, handle files and completion ourselves
|
|
297
|
+
if (!data.xy) return false;
|
|
298
|
+
Tools.mergeHashInto( job, data );
|
|
299
|
+
|
|
300
|
+
// intecept complete/code, as we need to handle it here after file uploads
|
|
301
|
+
if ('code' in data) delete data.code;
|
|
302
|
+
if ('description' in data) delete data.description;
|
|
303
|
+
if ('complete' in data) delete data.complete;
|
|
304
|
+
|
|
305
|
+
if (data.files) {
|
|
306
|
+
// remove from job update for parent, as we'll handle this here
|
|
307
|
+
delete data.files;
|
|
308
|
+
}
|
|
309
|
+
else if (data.push && data.push.files) {
|
|
310
|
+
// carefully extract files out of push update
|
|
311
|
+
if (!job.files) job.files = [];
|
|
312
|
+
job.files = job.files.concat( data.push.files );
|
|
313
|
+
delete data.push.files;
|
|
314
|
+
if (!Tools.numKeys(data.push)) delete data.push;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// passthrough other data if present
|
|
318
|
+
var updates = Tools.copyHashRemoveKeys(data, { xy:1 });
|
|
319
|
+
return !Tools.numKeys(updates);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
abortJob() {
|
|
323
|
+
// send kill signal if child is active
|
|
324
|
+
console.error(`Caught abort signal, shutting down`);
|
|
325
|
+
|
|
326
|
+
var job = this.job;
|
|
327
|
+
var worker = this.worker;
|
|
328
|
+
|
|
329
|
+
if (!job || !worker) process.exit(0);
|
|
330
|
+
|
|
331
|
+
if (worker.child) {
|
|
332
|
+
// kill process(es) or not, depending on abort policy
|
|
333
|
+
if (job.kill === 'none') {
|
|
334
|
+
// kill none, just unref and finish
|
|
335
|
+
worker.child.unref();
|
|
336
|
+
this.shutdown();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
worker.kill_timer = setTimeout( function() {
|
|
341
|
+
// child didn't die, kill with prejudice
|
|
342
|
+
if ((job.kill === 'all') && job.procs && Tools.firstKey(job.procs)) {
|
|
343
|
+
// sig-kill ALL job processes
|
|
344
|
+
var pids = Object.keys(job.procs);
|
|
345
|
+
console.error( "Children did not exit, killing harder: " + pids.join(', '));
|
|
346
|
+
pids.forEach( function(pid) {
|
|
347
|
+
try { process.kill(pid, 'SIGKILL'); }
|
|
348
|
+
catch(e) {;}
|
|
349
|
+
} );
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// sig-kill parent only
|
|
353
|
+
console.error( "Child did not exit, killing harder: " + job.pid);
|
|
354
|
+
worker.child.kill('SIGKILL');
|
|
355
|
+
}
|
|
356
|
+
}, 10000 );
|
|
357
|
+
|
|
358
|
+
// try killing nicely first
|
|
359
|
+
if ((job.kill === 'all') && job.procs && Tools.firstKey(job.procs)) {
|
|
360
|
+
// sig-term ALL job processes
|
|
361
|
+
var pids = Object.keys(job.procs);
|
|
362
|
+
console.log( "Killing all job processes: " + pids.join(', '));
|
|
363
|
+
pids.forEach( function(pid) {
|
|
364
|
+
try { process.kill(pid, 'SIGTERM'); }
|
|
365
|
+
catch(e) {;}
|
|
366
|
+
} );
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// sig-term parent only
|
|
370
|
+
console.log( "Killing job process: " + job.pid);
|
|
371
|
+
worker.child.kill('SIGTERM');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else process.exit(0);
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
finishJob() {
|
|
378
|
+
// upload files
|
|
379
|
+
var self = this;
|
|
380
|
+
var job = this.job;
|
|
381
|
+
var worker = this.worker;
|
|
382
|
+
|
|
383
|
+
if (!job || !worker) return this.shutdown(); // sanity
|
|
384
|
+
|
|
385
|
+
if (worker.kill_timer) {
|
|
386
|
+
clearTimeout( worker.kill_timer );
|
|
387
|
+
delete worker.kill_timer;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
delete this.job;
|
|
391
|
+
delete this.worker;
|
|
392
|
+
|
|
393
|
+
this.activeJobs = {};
|
|
394
|
+
this.kids = {};
|
|
395
|
+
|
|
396
|
+
this.prepUploadJobFiles(job, function(err) {
|
|
397
|
+
if (err) {
|
|
398
|
+
job.code = err.code || 'upload';
|
|
399
|
+
job.description = "Runner: " + (err.message || err);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// did we upload files? if so, send the metadata along now
|
|
403
|
+
if (job.files && job.files.length) {
|
|
404
|
+
console.log( JSON.stringify({ xy: 1, files: job.files }) );
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// now we're done done with job -- send final update
|
|
408
|
+
console.log( JSON.stringify({ xy: 1, complete: true, code: job.code || 0, description: job.description || "" }) );
|
|
409
|
+
|
|
410
|
+
// delete temp dir
|
|
411
|
+
if (job.cwd) Tools.rimraf( job.cwd, noop );
|
|
412
|
+
|
|
413
|
+
// all done
|
|
414
|
+
self.shutdown();
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
prepUploadJobFiles(job, callback) {
|
|
419
|
+
// glob all file requests to resolve them to individual files, then upload
|
|
420
|
+
var self = this;
|
|
421
|
+
var to_upload = [];
|
|
422
|
+
if (!job.files || !job.files.length || !Tools.isaArray(job.files)) return callback();
|
|
423
|
+
|
|
424
|
+
async.eachSeries( job.files,
|
|
425
|
+
function(file, callback) {
|
|
426
|
+
if (typeof(file) == 'string') {
|
|
427
|
+
file = { path: file };
|
|
428
|
+
}
|
|
429
|
+
else if (Array.isArray(file)) {
|
|
430
|
+
if (file.length == 3) file = { path: file[0], filename: file[1], delete: file[2] };
|
|
431
|
+
else if (file.length == 2) file = { path: file[0], filename: file[1] };
|
|
432
|
+
else file = { path: file[0] };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!file.path) return; // sanity
|
|
436
|
+
|
|
437
|
+
// prepend job cwd if path is not absolute
|
|
438
|
+
if (!Path.isAbsolute(file.path)) file.path = Path.join(job.cwd, file.path);
|
|
439
|
+
|
|
440
|
+
if (file.filename) {
|
|
441
|
+
// if user specified a custom filename, then do not perform a glob
|
|
442
|
+
to_upload.push(file);
|
|
443
|
+
process.nextTick(callback);
|
|
444
|
+
}
|
|
445
|
+
else Tools.glob( file.path, function(err, files) {
|
|
446
|
+
if (!files) files = [];
|
|
447
|
+
files.forEach( function(path) {
|
|
448
|
+
to_upload.push({ path: path, delete: !!file.delete });
|
|
449
|
+
} );
|
|
450
|
+
callback();
|
|
451
|
+
} );
|
|
452
|
+
},
|
|
453
|
+
function() {
|
|
454
|
+
job.files = to_upload;
|
|
455
|
+
self.uploadJobFiles(job, callback);
|
|
456
|
+
}
|
|
457
|
+
); // eachSeries
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
uploadJobFiles(job, callback) {
|
|
461
|
+
// upload all job files (from user) if applicable
|
|
462
|
+
var self = this;
|
|
463
|
+
var final_files = [];
|
|
464
|
+
var server_id = job.server;
|
|
465
|
+
if (!job.files || !job.files.length || !Tools.isaArray(job.files)) return callback();
|
|
466
|
+
|
|
467
|
+
async.eachSeries( job.files,
|
|
468
|
+
function(file, callback) {
|
|
469
|
+
var filename = Path.basename(file.filename || file.path).replace(/[^\w\-\+\.\,\s\(\)\[\]\{\}\'\"\!\&\^\%\$\#\@\*\?\~]+/g, '_');
|
|
470
|
+
console.log( "Uploading file: " + filename );
|
|
471
|
+
|
|
472
|
+
var url = job.base_url + '/api/app/upload_job_file';
|
|
473
|
+
var opts = Tools.mergeHashes( job.socket_opts || {}, {
|
|
474
|
+
"files": {
|
|
475
|
+
file1: [file.path, filename]
|
|
476
|
+
},
|
|
477
|
+
"data": {
|
|
478
|
+
id: job.id,
|
|
479
|
+
server: job.server,
|
|
480
|
+
auth: job.auth_token
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
self.request.post( url, opts, function(err, resp, data, perf) {
|
|
485
|
+
if (err) {
|
|
486
|
+
return callback( new Error("Failed to upload job file: " + filename + ": " + (err.message || err)) );
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
var json = null;
|
|
490
|
+
try { json = JSON.parse( data.toString() ); }
|
|
491
|
+
catch (err) { return callback(err); }
|
|
492
|
+
|
|
493
|
+
if (json.code && json.description) {
|
|
494
|
+
return callback( new Error("Failed to upload job file: " + filename + ": " + json.description) );
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// save file metadata
|
|
498
|
+
final_files.push({
|
|
499
|
+
id: file.id || Tools.generateShortID('f'),
|
|
500
|
+
date: Tools.timeNow(true),
|
|
501
|
+
filename: filename,
|
|
502
|
+
path: json.key,
|
|
503
|
+
size: json.size,
|
|
504
|
+
server: server_id,
|
|
505
|
+
job: job.id
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (file.delete) fs.unlink(file.path, callback);
|
|
509
|
+
else return callback();
|
|
510
|
+
}); // request.post
|
|
511
|
+
},
|
|
512
|
+
function(err) {
|
|
513
|
+
// replace job.files with storage keys
|
|
514
|
+
if (err) console.error(err);
|
|
515
|
+
else job.files = final_files;
|
|
516
|
+
callback(err);
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
tick() {
|
|
522
|
+
// called every second
|
|
523
|
+
// monitor job processes
|
|
524
|
+
this.jobTick();
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
jobTick() {
|
|
528
|
+
// called every second
|
|
529
|
+
var self = this;
|
|
530
|
+
if (!this.job) return;
|
|
531
|
+
|
|
532
|
+
if (this.jobTickInProgress) return; // no steppy on toesy
|
|
533
|
+
this.jobTickInProgress = true;
|
|
534
|
+
|
|
535
|
+
// scan all processes on machine
|
|
536
|
+
// si.processes( function(data) {
|
|
537
|
+
this.getProcsCached( function(data) {
|
|
538
|
+
if (!self.job) {
|
|
539
|
+
self.jobTickInProgress = false;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// cleanup and convert to hash of pids
|
|
544
|
+
var pids = {};
|
|
545
|
+
data.list.forEach( function(proc) {
|
|
546
|
+
// proc.started = (new Date( proc.started )).getTime() / 1000;
|
|
547
|
+
// proc.memRss = proc.memRss * 1024;
|
|
548
|
+
// proc.memVsz = proc.memVsz * 1024;
|
|
549
|
+
pids[ proc.pid ] = proc;
|
|
550
|
+
} );
|
|
551
|
+
|
|
552
|
+
for (var job_id in self.activeJobs) {
|
|
553
|
+
var job = self.activeJobs[job_id];
|
|
554
|
+
self.measureJobResources(job, pids);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async.parallel(
|
|
558
|
+
[
|
|
559
|
+
self.measureJobDiskIO.bind(self),
|
|
560
|
+
self.measureJobNetworkIO.bind(self)
|
|
561
|
+
],
|
|
562
|
+
function() {
|
|
563
|
+
if (!self.job) {
|
|
564
|
+
self.jobTickInProgress = false;
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// update job data
|
|
569
|
+
console.log( JSON.stringify({ xy: 1, procs: job.procs, conns: job.conns, cpu: job.cpu, mem: job.mem }) );
|
|
570
|
+
|
|
571
|
+
self.jobTickInProgress = false;
|
|
572
|
+
}
|
|
573
|
+
); // async.parallel
|
|
574
|
+
} ); // si.processes
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
measureJobResources(job, pids) {
|
|
578
|
+
// scan process list for all processes that are descendents of job pid
|
|
579
|
+
delete job.procs;
|
|
580
|
+
|
|
581
|
+
if (pids[ job.pid ]) {
|
|
582
|
+
// add all procs into job
|
|
583
|
+
job.procs = {};
|
|
584
|
+
job.procs[ job.pid ] = pids[ job.pid ];
|
|
585
|
+
|
|
586
|
+
var info = pids[ job.pid ];
|
|
587
|
+
var cpu = info.cpu;
|
|
588
|
+
var mem = info.memRss;
|
|
589
|
+
|
|
590
|
+
// also consider children of the child (up to 100 generations deep)
|
|
591
|
+
var levels = 0;
|
|
592
|
+
var family = {};
|
|
593
|
+
family[ job.pid ] = 1;
|
|
594
|
+
|
|
595
|
+
while (Tools.numKeys(family) && (++levels <= 100)) {
|
|
596
|
+
for (var fpid in family) {
|
|
597
|
+
for (var cpid in pids) {
|
|
598
|
+
if (pids[ cpid ].parentPid == fpid) {
|
|
599
|
+
family[ cpid ] = 1;
|
|
600
|
+
cpu += pids[ cpid ].cpu;
|
|
601
|
+
mem += pids[ cpid ].memRss;
|
|
602
|
+
job.procs[ cpid ] = pids[ cpid ];
|
|
603
|
+
} // matched
|
|
604
|
+
} // cpid loop
|
|
605
|
+
delete family[fpid];
|
|
606
|
+
} // fpid loop
|
|
607
|
+
} // while
|
|
608
|
+
|
|
609
|
+
if (job.cpu) {
|
|
610
|
+
if (cpu < job.cpu.min) job.cpu.min = cpu;
|
|
611
|
+
if (cpu > job.cpu.max) job.cpu.max = cpu;
|
|
612
|
+
job.cpu.total += cpu;
|
|
613
|
+
job.cpu.count++;
|
|
614
|
+
job.cpu.current = cpu;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
job.cpu = { min: cpu, max: cpu, total: cpu, count: 1, current: cpu };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (job.mem) {
|
|
621
|
+
if (mem < job.mem.min) job.mem.min = mem;
|
|
622
|
+
if (mem > job.mem.max) job.mem.max = mem;
|
|
623
|
+
job.mem.total += mem;
|
|
624
|
+
job.mem.count++;
|
|
625
|
+
job.mem.current = mem;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
job.mem = { min: mem, max: mem, total: mem, count: 1, current: mem };
|
|
629
|
+
}
|
|
630
|
+
} // matched job with pid
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
measureJobDiskIO(callback) {
|
|
634
|
+
// use linux /proc/PID/io to glean disk r/w per sec per job proc
|
|
635
|
+
var self = this;
|
|
636
|
+
var procs = [];
|
|
637
|
+
|
|
638
|
+
// zero everything out for non-linux
|
|
639
|
+
for (var job_id in this.activeJobs) {
|
|
640
|
+
var job = this.activeJobs[job_id];
|
|
641
|
+
if (job.procs) {
|
|
642
|
+
for (var pid in job.procs) { job.procs[pid].disk = 0; }
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// this trick is linux only
|
|
647
|
+
if (process.platform != 'linux') return process.nextTick( callback );
|
|
648
|
+
|
|
649
|
+
// get array of all active job procs
|
|
650
|
+
for (var job_id in this.activeJobs) {
|
|
651
|
+
var job = this.activeJobs[job_id];
|
|
652
|
+
if (job.procs) procs = procs.concat( Object.values(job.procs) );
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// parallelize this just a smidge, as it can be a lot of reads
|
|
656
|
+
async.eachLimit( procs, 4,
|
|
657
|
+
function(proc, callback) {
|
|
658
|
+
fs.readFile( '/proc/' + proc.pid + '/io', 'utf8', function(err, text) {
|
|
659
|
+
// if (!text) text = "rchar: " + Math.floor( Tools.timeNow(true) * 1024 ); // sample data (for testing)
|
|
660
|
+
if (!text) text = "";
|
|
661
|
+
|
|
662
|
+
// parse into key/value pairs
|
|
663
|
+
var params = {};
|
|
664
|
+
text.replace( /(\w+)\:\s*(\d+)/g, function(m_all, key, value) {
|
|
665
|
+
params[key] = parseInt(value);
|
|
666
|
+
return m_all;
|
|
667
|
+
} );
|
|
668
|
+
|
|
669
|
+
// take disk w + r per proc
|
|
670
|
+
proc.disk = (params.rchar || 0) + (params.wchar || 0);
|
|
671
|
+
// proc.disk = (params.read_bytes || 0) + (params.write_bytes || 0);
|
|
672
|
+
|
|
673
|
+
callback();
|
|
674
|
+
} );
|
|
675
|
+
},
|
|
676
|
+
callback
|
|
677
|
+
); // async.eachLimit
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
measureJobNetworkIO(callback) {
|
|
681
|
+
// use linux `ss` utility to glean network r/w per sec per job proc
|
|
682
|
+
var self = this;
|
|
683
|
+
|
|
684
|
+
// zero everything out for non-linux
|
|
685
|
+
for (var job_id in this.activeJobs) {
|
|
686
|
+
var job = this.activeJobs[job_id];
|
|
687
|
+
if (job.procs) {
|
|
688
|
+
for (var pid in job.procs) {
|
|
689
|
+
job.procs[pid].conns = 0;
|
|
690
|
+
job.procs[pid].net = 0;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// this trick is linux only
|
|
696
|
+
if ((process.platform != 'linux') || !this.ssBin) return process.nextTick( callback );
|
|
697
|
+
|
|
698
|
+
cp.exec( this.ssBin + ' -nutipaO', { timeout: 1000, maxBuffer: 1024 * 1024 * 32 }, function(err, stdout, stderr) {
|
|
699
|
+
if (err) {
|
|
700
|
+
console.error("Failed to launch ss: " + err);
|
|
701
|
+
return callback();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
var now = Tools.timeNow(true);
|
|
705
|
+
var lines = stdout.split(/\n/);
|
|
706
|
+
var ids = {};
|
|
707
|
+
|
|
708
|
+
lines.forEach( function(line) {
|
|
709
|
+
if (line.match(/^(tcp|tcp4|tcp6|udp|udp4|udp6)\s+(\w+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+.+pid\=(\d+)/)) {
|
|
710
|
+
var type = RegExp.$1, state = RegExp.$2, local_addr = RegExp.$5, remote_addr = RegExp.$6, pid = RegExp.$7;
|
|
711
|
+
|
|
712
|
+
// clean up some stuff
|
|
713
|
+
pid = parseInt(pid);
|
|
714
|
+
if (state == "ESTAB") state = 'ESTABLISHED';
|
|
715
|
+
if (state == "UNCONN") state = 'UNCONNECTED';
|
|
716
|
+
|
|
717
|
+
// generate socket "id" key using combo of local + remote
|
|
718
|
+
var id = local_addr + '|' + remote_addr;
|
|
719
|
+
|
|
720
|
+
if (!self.connCache[id]) self.connCache[id] = { bytes: 0, delta: 0, started: now };
|
|
721
|
+
var conn = self.connCache[id];
|
|
722
|
+
|
|
723
|
+
conn.type = type;
|
|
724
|
+
conn.state = state;
|
|
725
|
+
conn.local_addr = local_addr;
|
|
726
|
+
conn.remote_addr = remote_addr;
|
|
727
|
+
conn.pid = pid;
|
|
728
|
+
|
|
729
|
+
var bytes = 0;
|
|
730
|
+
if (line.match(/\bbytes_acked\:(\d+)/)) bytes += parseInt( RegExp.$1 ); // tx
|
|
731
|
+
if (line.match(/\bbytes_received\:(\d+)/)) bytes += parseInt( RegExp.$1 ); // rx
|
|
732
|
+
|
|
733
|
+
conn.delta = bytes - conn.bytes;
|
|
734
|
+
conn.bytes = bytes;
|
|
735
|
+
|
|
736
|
+
ids[id] = 1;
|
|
737
|
+
}
|
|
738
|
+
} ); // foreach line
|
|
739
|
+
|
|
740
|
+
// delete sweep for removed conns
|
|
741
|
+
for (var id in self.connCache) {
|
|
742
|
+
if (!(id in ids)) delete self.connCache[id];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// join up conns with jobs and job procs
|
|
746
|
+
Object.values(self.activeJobs).forEach( function(job) {
|
|
747
|
+
if (!job.procs) return;
|
|
748
|
+
|
|
749
|
+
job.conns = [];
|
|
750
|
+
for (var id in self.connCache) {
|
|
751
|
+
var conn = self.connCache[id];
|
|
752
|
+
if (conn.pid in job.procs) {
|
|
753
|
+
job.conns.push(conn);
|
|
754
|
+
job.procs[conn.pid].conns++;
|
|
755
|
+
job.procs[conn.pid].net += conn.delta;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
}); // foreach job
|
|
760
|
+
|
|
761
|
+
callback();
|
|
762
|
+
} ); // cp.exec
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
getProcsCached(callback) {
|
|
766
|
+
// get process information, cached with a dynamic rolling debounce
|
|
767
|
+
// (TTL is based on the previous cache miss elapsed time)
|
|
768
|
+
// (designed to throttle on slower machines, or with thousands of processes)
|
|
769
|
+
var self = this;
|
|
770
|
+
var now = Tools.timeNow();
|
|
771
|
+
var cache = this.procCache;
|
|
772
|
+
|
|
773
|
+
if (cache.data) {
|
|
774
|
+
if (now < cache.expires) {
|
|
775
|
+
// still fresh
|
|
776
|
+
return callback( Tools.copyHash(cache.data, true) );
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this.getProcsFast( function(data) {
|
|
781
|
+
// save cache data
|
|
782
|
+
cache.data = data;
|
|
783
|
+
cache.date = Tools.timeNow();
|
|
784
|
+
cache.elapsed = cache.date - now;
|
|
785
|
+
cache.expires = cache.date + (cache.elapsed * 5);
|
|
786
|
+
callback( Tools.copyHash(cache.data, true) );
|
|
787
|
+
} );
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
getProcsFast(callback) {
|
|
791
|
+
// get process information fast
|
|
792
|
+
var self = this;
|
|
793
|
+
var now = Tools.timeNow(true);
|
|
794
|
+
|
|
795
|
+
if (this.platform.windows) {
|
|
796
|
+
return si.processes( function(data) {
|
|
797
|
+
data.list.forEach( function(proc) {
|
|
798
|
+
// convert data to our native format
|
|
799
|
+
try {
|
|
800
|
+
proc.started = Math.floor( (new Date(proc.started)).getTime() / 1000 );
|
|
801
|
+
proc.age = now - proc.started;
|
|
802
|
+
}
|
|
803
|
+
catch (e) { proc.started = proc.age = 0; }
|
|
804
|
+
|
|
805
|
+
// some commands are quoted
|
|
806
|
+
proc.command = proc.command.replace(/^\"(.+?)\"/, '$1');
|
|
807
|
+
|
|
808
|
+
// cleanup state
|
|
809
|
+
proc.state = Tools.ucfirst( proc.state || 'unknown' );
|
|
810
|
+
|
|
811
|
+
// memory readings are in kilobytes
|
|
812
|
+
proc.memRss *= 1024;
|
|
813
|
+
proc.memVsz *= 1024;
|
|
814
|
+
|
|
815
|
+
// delete redundant props
|
|
816
|
+
delete proc.path;
|
|
817
|
+
delete proc.params;
|
|
818
|
+
});
|
|
819
|
+
callback(data);
|
|
820
|
+
} );
|
|
821
|
+
} // windows
|
|
822
|
+
|
|
823
|
+
var info = { list: [] };
|
|
824
|
+
var ps_args = [];
|
|
825
|
+
var ps_opts = {
|
|
826
|
+
env: Object.assign( {}, process.env ),
|
|
827
|
+
maxBuffer: 1024 * 1024 * 100,
|
|
828
|
+
timeout: 30000
|
|
829
|
+
};
|
|
830
|
+
const colMap = {
|
|
831
|
+
ppid: 'parentPid',
|
|
832
|
+
rss: 'memRss',
|
|
833
|
+
vsz: 'memVsz',
|
|
834
|
+
tt: 'tty',
|
|
835
|
+
thcnt: 'threads',
|
|
836
|
+
pri: 'priority',
|
|
837
|
+
ni: 'nice',
|
|
838
|
+
s: 'state',
|
|
839
|
+
stat: 'state',
|
|
840
|
+
elapsed: 'age',
|
|
841
|
+
cls: 'class',
|
|
842
|
+
gid: 'group',
|
|
843
|
+
args: 'command'
|
|
844
|
+
};
|
|
845
|
+
const stateMap = {
|
|
846
|
+
I: 'Idle',
|
|
847
|
+
S: 'Sleeping',
|
|
848
|
+
D: 'Sleeping',
|
|
849
|
+
U: 'Sleeping',
|
|
850
|
+
R: 'Running',
|
|
851
|
+
Z: 'Zombie',
|
|
852
|
+
T: 'Stopped',
|
|
853
|
+
t: 'Stopped',
|
|
854
|
+
W: 'Paged',
|
|
855
|
+
X: 'Dead'
|
|
856
|
+
};
|
|
857
|
+
const classMap = {
|
|
858
|
+
TS: 'Other',
|
|
859
|
+
FF: 'FIFO',
|
|
860
|
+
RR: 'RR',
|
|
861
|
+
B: 'Batch',
|
|
862
|
+
ISO: 'ISO',
|
|
863
|
+
IDL: 'Idle',
|
|
864
|
+
DLN: 'Deadline'
|
|
865
|
+
};
|
|
866
|
+
const filterMap = {
|
|
867
|
+
pid: parseInt,
|
|
868
|
+
parentPid: parseInt,
|
|
869
|
+
priority: parseInt,
|
|
870
|
+
nice: parseInt,
|
|
871
|
+
threads: parseInt,
|
|
872
|
+
time: parseInt,
|
|
873
|
+
|
|
874
|
+
// cpu: parseFloat,
|
|
875
|
+
mem: parseFloat,
|
|
876
|
+
|
|
877
|
+
cpu: function(value) {
|
|
878
|
+
// divide by CPU count for real value
|
|
879
|
+
return parseFloat(value) / self.numCPUs;
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
age: function(value) {
|
|
883
|
+
if (value.match(/^\d+$/)) return parseInt(value);
|
|
884
|
+
if (value.match(/^(\d+)\-(\d+)\:(\d+)\:(\d+)$/)) {
|
|
885
|
+
// DD-HH:MI:SS
|
|
886
|
+
var [ dd, hh, mi, ss ] = [ RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4 ];
|
|
887
|
+
return ( (parseInt(dd) * 86400) + (parseInt(hh) * 3600) + (parseInt(mi) * 60) + parseInt(ss) );
|
|
888
|
+
}
|
|
889
|
+
if (value.match(/^(\d+)\:(\d+)\:(\d+)$/)) {
|
|
890
|
+
// HH:MI:SS
|
|
891
|
+
var [ hh, mi, ss ] = [ RegExp.$1, RegExp.$2, RegExp.$3 ];
|
|
892
|
+
return ( (parseInt(hh) * 3600) + (parseInt(mi) * 60) + parseInt(ss) );
|
|
893
|
+
}
|
|
894
|
+
if (value.match(/^(\d+)\:(\d+)$/)) {
|
|
895
|
+
// MI:SS
|
|
896
|
+
var [ mi, ss ] = [ RegExp.$1, RegExp.$2 ];
|
|
897
|
+
return ( (parseInt(mi) * 60) + parseInt(ss) );
|
|
898
|
+
}
|
|
899
|
+
return 0;
|
|
900
|
+
},
|
|
901
|
+
memRss: function(value) {
|
|
902
|
+
return parseInt(value) * 1024;
|
|
903
|
+
},
|
|
904
|
+
memVsz: function(value) {
|
|
905
|
+
return parseInt(value) * 1024;
|
|
906
|
+
},
|
|
907
|
+
state: function(value) {
|
|
908
|
+
return stateMap[value.substring(0, 1)] || 'Unknown';
|
|
909
|
+
},
|
|
910
|
+
class: function(value) {
|
|
911
|
+
return classMap[value] || 'Unknown';
|
|
912
|
+
},
|
|
913
|
+
group: function(value) {
|
|
914
|
+
if (value.match(/^\d+$/)) {
|
|
915
|
+
var group = Tools.getgrnam( value, true ); // cached in ram
|
|
916
|
+
if (group && group.name) return group.name;
|
|
917
|
+
}
|
|
918
|
+
return value;
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
if (this.platform.linux) {
|
|
923
|
+
// PID PPID USER %CPU RSS ELAPSED S PRI NI VSZ TT %MEM CLS GROUP THCNT TIME COMMAND
|
|
924
|
+
ps_args = ['-eo', 'pid,ppid,user,%cpu,rss,etimes,state,pri,nice,vsz,tty,%mem,class,group,thcount,times,args'];
|
|
925
|
+
ps_opts.env.LC_ALL = 'C';
|
|
926
|
+
}
|
|
927
|
+
else if (this.platform.darwin) {
|
|
928
|
+
// PID PPID %CPU %MEM PRI VSZ RSS NI ELAPSED STAT TTY USER GID ARGS
|
|
929
|
+
ps_args = ['-axro', 'pid,ppid,%cpu,%mem,pri,vsz,rss,nice,etime,state,tty,user,group,args'];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
cp.execFile( this.psBin, ps_args, ps_opts, function(err, stdout, stderr) {
|
|
933
|
+
if (err) return callback(info);
|
|
934
|
+
|
|
935
|
+
var lines = stdout.trim().split(/\n/);
|
|
936
|
+
var headers = lines.shift().trim().split(/\s+/).map( function(key) { return key.trim().toLowerCase().replace(/\W+/g, ''); } );
|
|
937
|
+
|
|
938
|
+
lines.forEach( function(line) {
|
|
939
|
+
var cols = line.trim().split(/\s+/);
|
|
940
|
+
if (cols.length > headers.length) {
|
|
941
|
+
var extras = cols.splice(headers.length);
|
|
942
|
+
cols[ headers.length - 1 ] += ' ' + extras.join(' ');
|
|
943
|
+
}
|
|
944
|
+
var proc = {};
|
|
945
|
+
|
|
946
|
+
headers.forEach( function(key, idx) {
|
|
947
|
+
key = colMap[key] || key;
|
|
948
|
+
proc[key] = filterMap[key] ? filterMap[key](cols[idx]) : cols[idx];
|
|
949
|
+
} );
|
|
950
|
+
|
|
951
|
+
proc.started = Math.max(0, now - (proc.age || 0));
|
|
952
|
+
|
|
953
|
+
// state bookkeeping
|
|
954
|
+
var state = proc.state.toLowerCase();
|
|
955
|
+
info[ state ] = (info[ state ] || 0) + 1;
|
|
956
|
+
info.all = (info.all || 0) + 1;
|
|
957
|
+
|
|
958
|
+
// filter out ps itself
|
|
959
|
+
if ((proc.parentPid == process.pid) && (proc.command.startsWith(self.psBin))) return;
|
|
960
|
+
|
|
961
|
+
info.list.push(proc);
|
|
962
|
+
} );
|
|
963
|
+
|
|
964
|
+
callback(info);
|
|
965
|
+
}); // cp.execFile
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
shutdown() {
|
|
969
|
+
// all done
|
|
970
|
+
if (this.timer) clearInterval(this.timer);
|
|
971
|
+
delete this.timer;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
process.on('SIGTERM', app.abortJob.bind(app) );
|
|
976
|
+
process.on('SIGINT', app.abortJob.bind(app) );
|
|
977
|
+
process.on('SIGHUP', app.abortJob.bind(app) );
|
|
978
|
+
|
|
979
|
+
app.run();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pixlcore/xyrun",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remote job runner script for xyOps.",
|
|
5
|
+
"author": "Joseph Huckaby <jhuckaby@pixlcore.com>",
|
|
6
|
+
"homepage": "https://github.com/pixlcore/xyrun",
|
|
7
|
+
"license": "BSD-3-Clause",
|
|
8
|
+
"bin": {
|
|
9
|
+
"xyrun": "./main.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "main.js",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/pixlcore/xyrun"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/pixlcore/xyrun/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"xyops",
|
|
21
|
+
"xyrun"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"pixl-json-stream": "^1.0.7",
|
|
25
|
+
"pixl-request": "^2.4.1",
|
|
26
|
+
"pixl-tools": "^1.1.12",
|
|
27
|
+
"systeminformation": "5.27.7"
|
|
28
|
+
}
|
|
29
|
+
}
|