'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), execFile: util.promisify(execFile), spawn: util.promisify(spawn), sudo: util.promisify(sudo) } }; const SUDO = '/usr/bin/sudo'; // 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'); debug(`${tag} exec: ${cmd}`); // https://github.com/nodejs/node/issues/25231 const cp = child_process.exec(cmd, options, 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}: ${cmd} errored`, error); } callback(e, stdout); }); if (options.input) { cp.stdin.write(options.input); cp.stdin.end(); } } // no shell, utf8 encoding, separate args function execFile(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}`); // https://github.com/nodejs/node/issues/25231 const cp = child_process.execFile(file, args, options, 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(); } } // 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 = ''; if (options.logStream) { cp.stdout.pipe(options.logStream); cp.stderr.pipe(options.logStream); } else { 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; }