UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

308 lines 13.4 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 = 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