UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

335 lines (319 loc) 11.2 kB
(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);