smc-hub
Version:
CoCalc: Backend webserver component
335 lines (319 loc) • 11.2 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, defaults, required} = misc);
({aggregate} = require('smc-util/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, // defaults to home directory; where code is executed from
timeout: 10, // timeout in *seconds*
ulimit_timeout: true, // If set, use ulimit to ensure a cpu timeout -- don't use when launching a daemon!
// This has no effect if bash not true.
err_on_exit: true, // if true, then a nonzero exit code will result in cb(error_message)
max_output: void 0, // bound on size of stdout and stderr; further output ignored
bash: false, // if true, ignore args and evaluate command as a bash command
home: void 0,
uid: void 0,
gid: void 0,
env: void 0, // if given, added to exec environment
aggregate: void 0, // if given, aggregates multiple calls with same sequence number into one -- see smc-util/aggregate; typically make this a timestamp for compiling code.
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,
0o700,
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);