'use strict'; const assert = require('assert'), BoxError = require('./boxerror.js'), child_process = require('child_process'), debug = require('debug')('box:shell'), once = require('./once.js'), util = require('util'); exports = module.exports = { sudo, promises: { exec: util.promisify(exec), execArgs: util.promisify(execArgs), sudo: util.promisify(sudo) } }; const SUDO = '/usr/bin/sudo'; // default encoding utf8, no shell, separate args function execArgs(tag, file, args, options, callback) { assert.strictEqual(typeof tag, 'string'); assert.strictEqual(typeof file, 'string'); assert(Array.isArray(args)); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debug(`${tag} exec: ${file}`); const execOptions = Object.assign({ encoding: 'utf8', shell: false }, options); // https://github.com/nodejs/node/issues/25231 const cp = child_process.execFile(file, args, execOptions, function (error, stdout, stderr) { let e = null; if (error) { e = new BoxError(BoxError.SHELL_ERROR, `${tag} errored with code ${error.code} message ${error.message}`); e.stdout = stdout; // when promisified, this is the way to get stdout e.stderr = stderr; // when promisified, this is the way to get stderr debug(`${tag}: ${file} with args ${args.join(' ')} errored`, error); } callback(e, stdout); }); if (options.input) { cp.stdin.write(options.input); cp.stdin.end(); } } // default encoding utf8, shell, handles input, full command function exec(tag, cmd, options, callback) { assert.strictEqual(typeof tag, 'string'); assert.strictEqual(typeof cmd, 'string'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const [file, ...args] = cmd.split(' '); execArgs(tag, file, args, options, callback); } // use this when you are afraid of how arguments will split up function spawn(tag, file, args, options, callback) { assert.strictEqual(typeof tag, 'string'); assert.strictEqual(typeof file, 'string'); assert(Array.isArray(args)); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); callback = once(callback); // exit may or may not be called after an 'error' if (options.ipc) options.stdio = ['pipe', 'pipe', 'pipe', 'ipc']; debug(tag + ' spawn: %s %s', file, args.join(' ').replace(/\n/g, '\\n')); const cp = child_process.spawn(file, args, options); let stdoutResult = ''; cp.stdout.on('data', function (data) { debug(tag + ' (stdout): %s', data.toString('utf8')); stdoutResult += data.toString('utf8'); }); cp.stderr.on('data', function (data) { debug(tag + ' (stderr): %s', data.toString('utf8')); }); cp.on('exit', function (code, signal) { if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal); if (code === 0) return callback(null, stdoutResult); let e = new BoxError(BoxError.SHELL_ERROR, `${tag} exited with code ${code} signal ${signal}`); e.code = code; e.signal = signal; callback(e); }); cp.on('error', function (error) { debug(tag + ' code: %s, signal: %s', error.code, error.signal); let e = new BoxError(BoxError.SHELL_ERROR, `${tag} errored with code ${error.code} message ${error.message}`); callback(e); }); return cp; } function sudo(tag, args, options, callback) { assert.strictEqual(typeof tag, 'string'); assert(Array.isArray(args)); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); let sudoArgs = [ '-S' ]; // -S makes sudo read stdin for password if (options.preserveEnv) sudoArgs.push('-E'); // -E preserves environment if (options.ipc) sudoArgs.push('--close-from=4'); // keep the ipc open. requires closefrom_override in sudoers file const cp = spawn(tag, SUDO, sudoArgs.concat(args), options, callback); cp.stdin.end(); if (options.onMessage) cp.on('message', options.onMessage); return cp; }