smc-hub
Version:
CoCalc: Backend webserver component
308 lines • 13.4 kB
JavaScript
(function () {
//########################################################################
// This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
// License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
//########################################################################
// Execute code in a subprocess, etc.
var aggregate, async, child_process, defaults, execute_code, fs, misc, required, shell_escape, temp, walltime, winston;
winston = require('winston');
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {
level: 'debug',
timestamp: true,
colorize: true
});
temp = require('temp');
async = require('async');
fs = require('fs');
child_process = require('child_process');
shell_escape = require('shell-escape');
misc = require('smc-util/misc');
(walltime = misc.walltime, defaults = misc.defaults, required = misc.required);
(aggregate = require('smc-util/aggregate').aggregate);
exports.execute_code = execute_code = aggregate(function (opts) {
var env, exit_code, info, k, ran_code, ref, s, start_time, stderr, stdout, v;
opts = defaults(opts, {
command: required,
args: [],
path: void 0,
timeout: 10,
ulimit_timeout: true,
// This has no effect if bash not true.
err_on_exit: true,
max_output: void 0,
bash: false,
home: void 0,
uid: void 0,
gid: void 0,
env: void 0,
aggregate: void 0,
verbose: true,
cb: void 0
});
start_time = walltime();
if (opts.verbose) {
winston.debug("execute_code: \"" + opts.command + " " + opts.args.join(' ') + "\"");
}
s = opts.command.split(/\s+/g); // split on whitespace
if (opts.args.length === 0 && s.length > 1) {
opts.bash = true;
}
else if (opts.bash && opts.args.length > 0) {
// Selected bash, but still passed in args.
opts.command = shell_escape([opts.command].concat(opts.args));
opts.args = [];
}
if (opts.home == null) {
opts.home = process.env.HOME;
}
if (opts.path == null) {
opts.path = opts.home;
}
else if (opts.path[0] !== '/') {
opts.path = opts.home + '/' + opts.path;
}
stdout = '';
stderr = '';
exit_code = void 0;
env = misc.copy(process.env);
if (opts.env != null) {
ref = opts.env;
for (k in ref) {
v = ref[k];
env[k] = v;
}
}
if (opts.uid != null) {
env.HOME = opts.home;
}
ran_code = false;
info = void 0;
return async.series([
function (c) {
var cmd;
if (!opts.bash) {
c();
return;
}
if (opts.timeout && opts.ulimit_timeout) {
// This ensures that everything involved with this
// command really does die no matter what; it's
// better than killing from outside, since it gets
// all subprocesses since they inherit the limits.
cmd = "ulimit -t " + opts.timeout + "\n" + opts.command;
}
else {
cmd = opts.command;
}
if (opts.verbose) {
winston.debug("execute_code: writing temporary file that contains bash program.");
}
return temp.open('', function (err, _info) {
if (err) {
return c(err);
}
else {
info = _info;
opts.command = 'bash';
opts.args = [info.path];
return fs.writeFile(info.fd, cmd, c);
}
});
},
function (c) {
if (info != null) {
return fs.close(info.fd, c);
}
else {
return c();
}
},
function (c) {
if (info != null) {
return fs.chmod(info.path, 448, c);
}
else {
return c();
}
},
function (c) {
var callback_done, e, f, finish, o, r, stderr_is_done, stdout_is_done;
if (opts.verbose) {
winston.debug("Spawning the command " + opts.command + " with given args " + opts.args + " and timeout of " + opts.timeout + "s...");
}
o = {
cwd: opts.path
};
if (env != null) {
o.env = env;
}
if (opts.uid) {
o.uid = opts.uid;
}
if (opts.gid) {
o.gid = opts.gid;
}
try {
r = child_process.spawn(opts.command, opts.args, o);
if ((r.stdout == null) || (r.stderr == null)) {
// The docs/examples at https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
// suggest that r.stdout and r.stderr are always defined. However, this is
// definitely NOT the case in edge cases, as we have observed.
c("error creating child process -- couldn't spawn child process");
return;
}
}
catch (error) {
e = error;
// Yes, spawn can cause this error if there is no memory, and there's no event! -- Error: spawn ENOMEM
ran_code = false;
c("error " + misc.to_json(e));
return;
}
ran_code = true;
if (opts.verbose) {
winston.debug("Listen for stdout, stderr and exit events.");
}
stdout = '';
r.stdout.on('data', function (data) {
data = data.toString();
if (opts.max_output != null) {
if (stdout.length < opts.max_output) {
return stdout += data.slice(0, opts.max_output - stdout.length);
}
}
else {
return stdout += data;
}
});
r.stderr.on('data', function (data) {
data = data.toString();
if (opts.max_output != null) {
if (stderr.length < opts.max_output) {
return stderr += data.slice(0, opts.max_output - stderr.length);
}
}
else {
return stderr += data;
}
});
stderr_is_done = stdout_is_done = false;
r.stderr.on('end', function () {
stderr_is_done = true;
return finish();
});
r.stdout.on('end', function () {
stdout_is_done = true;
return finish();
});
r.on('exit', function (code) {
exit_code = code;
return finish();
});
// This can happen, e.g., "Error: spawn ENOMEM" if there is no memory. Without this handler,
// an unhandled exception gets raised, which is nasty.
// From docs: "Note that the exit-event may or may not fire after an error has occured. "
r.on('error', function (err) {
if (exit_code == null) {
exit_code = 1;
}
stderr += misc.to_json(err);
// a fundamental issue, we were not running some code
ran_code = false;
return finish();
});
callback_done = false;
finish = function () {
if (stdout_is_done && stderr_is_done && (exit_code != null)) {
if (opts.err_on_exit && exit_code !== 0) {
if (!callback_done) {
callback_done = true;
return c("command '" + opts.command + "' (args=" + opts.args.join(' ') + ") exited with nonzero code " + exit_code + " -- stderr='" + stderr + "'");
}
}
else if (!ran_code) { // regardless of opts.err_on_exit !
if (!callback_done) {
callback_done = true;
return c("command '" + opts.command + "' (args=" + opts.args.join(' ') + ") was not able to run -- stderr='" + stderr + "'");
}
}
else {
if (opts.max_output != null) {
if (stdout.length >= opts.max_output) {
stdout += " (truncated at " + opts.max_output + " characters)";
}
if (stderr.length >= opts.max_output) {
stderr += " (truncated at " + opts.max_output + " characters)";
}
}
if (!callback_done) {
callback_done = true;
return c();
}
}
}
};
if (opts.timeout) {
f = function () {
if (r.exitCode === null) {
if (opts.verbose) {
winston.debug("execute_code: subprocess did not exit after " + opts.timeout + " seconds, so killing with SIGKILL");
}
try {
r.kill("SIGKILL"); // this does not kill the process group :-(
}
catch (error) {
e = error;
// Exceptions can happen, which left uncaught messes up calling code bigtime.
if (opts.verbose) {
winston.debug("execute_code: r.kill raised an exception.");
}
}
if (!callback_done) {
callback_done = true;
return c("killed command '" + opts.command + " " + opts.args.join(' ') + "'");
}
}
};
return setTimeout(f, opts.timeout * 1000);
}
},
function (c) {
if ((info != null ? info.path : void 0) != null) {
// Do not litter:
return fs.unlink(info.path, c);
}
else {
return c();
}
}
], function (err) {
if (exit_code == null) {
exit_code = 1; // don't have one due to SIGKILL
}
// This log message is very dangerous, e.g., it could print out a secret_token to a log file.
// So it commented out. Only include for low level debugging.
// winston.debug("(time: #{walltime() - start_time}): Done running '#{opts.command} #{opts.args.join(' ')}'; resulted in stdout='#{misc.trunc(stdout,512)}', stderr='#{misc.trunc(stderr,512)}', exit_code=#{exit_code}, err=#{err}")
if (opts.verbose) {
winston.debug("finished exec of " + opts.command + " (took " + walltime(start_time) + "s)");
winston.debug("stdout='" + misc.trunc(stdout, 512) + "', stderr='" + misc.trunc(stderr, 512) + "', exit_code=" + exit_code);
}
if ((!opts.err_on_exit) && ran_code) {
return typeof opts.cb === "function" ? opts.cb(false, {
stdout: stdout,
stderr: stderr,
exit_code: exit_code
}) : void 0;
}
else {
return typeof opts.cb === "function" ? opts.cb(err, {
stdout: stdout,
stderr: stderr,
exit_code: exit_code
}) : void 0;
}
});
});
}).call(this);
//# sourceMappingURL=execute-code.js.map