module.exports = wrap wrap.runMain = runMain var Module = require('module') var fs = require('fs') var cp = require('child_process') var ChildProcess = cp.ChildProcess var assert = require('assert') var crypto = require('crypto') var mkdirp = require('mkdirp') var rimraf = require('rimraf') var path = require('path') var signalExit = require('signal-exit') var homedir = require('os-homedir')() + '/.node-spawn-wrap-' var which = require('which') var util = require('util') var doDebug = process.env.SPAWN_WRAP_DEBUG === '1' var debug = doDebug ? function () { var message = util.format.apply(util, arguments).trim() var pref = 'SW ' + process.pid + ': ' message = pref + message.split('\n').join('\n' + pref) process.stderr.write(message + '\n') } : function () {} var shim = '#!' + process.execPath + '\n' + fs.readFileSync(__dirname + '/shim.js') var isWindows = require('./lib/is-windows')() var pathRe = /^PATH=/ if (isWindows) pathRe = /^PATH=/i var colon = isWindows ? ';' : ':' function wrap (argv, env, workingDir) { if (!ChildProcess) { var child = cp.spawn(process.execPath, []) ChildProcess = child.constructor child.kill('SIGKILL') } // spawn_sync available since Node v0.11 var spawnSyncBinding, spawnSync try { spawnSyncBinding = process.binding('spawn_sync') } catch (e) {} // if we're passed in the working dir, then it means that setup // was already done, so no need. var doSetup = !workingDir if (doSetup) { workingDir = setup(argv, env) } var spawn = ChildProcess.prototype.spawn if (spawnSyncBinding) { spawnSync = spawnSyncBinding.spawn } function unwrap () { if (doSetup && !doDebug) { rimraf.sync(workingDir) } ChildProcess.prototype.spawn = spawn if (spawnSyncBinding) { spawnSyncBinding.spawn = spawnSync } } if (spawnSyncBinding) { spawnSyncBinding.spawn = wrappedSpawnFunction(spawnSync, workingDir) } ChildProcess.prototype.spawn = wrappedSpawnFunction(spawn, workingDir) return unwrap } function wrappedSpawnFunction (fn, workingDir) { return wrappedSpawn function wrappedSpawn (options) { munge(workingDir, options) debug('WRAPPED', options) return fn.call(this, options) } } function isSh (file) { return file === 'dash' || file === 'sh' || file === 'bash' || file === 'zsh' } function mungeSh (workingDir, options) { var cmdi = options.args.indexOf('-c') if (cmdi === -1) return // no -c argument var c = options.args[cmdi + 1] var re = /^\s*((?:[^\= ]*\=[^\=\s]*)*[\s]*)([^\s]+|"[^"]+"|'[^']+')( .*)?$/ var match = c.match(re) if (!match) return // not a command invocation. weird but possible var command = match[2] // strip quotes off the command var quote = command.charAt(0) if ((quote === '"' || quote === '\'') && quote === command.slice(-1)) { command = command.slice(1, -1) } var exe = path.basename(command) if (isNode(exe)) { options.originalNode = command c = match[1] + match[2] + ' "' + workingDir + '/node" ' + match[3] options.args[cmdi + 1] = c } else if (exe === 'npm' && !isWindows) { // XXX this will exhibit weird behavior when using /path/to/npm, // if some other npm is first in the path. var npmPath = whichOrUndefined('npm') if (npmPath) { c = c.replace(re, '$1 "' + workingDir + '/node" "' + npmPath + '" $3') options.args[cmdi + 1] = c debug('npm munge!', c) } } } function isCmd (file) { var comspec = path.basename(process.env.comspec || '').replace(/\.exe$/i, '') return isWindows && (file === comspec || /^cmd(\.exe|\.EXE)?$/.test(file)) } function mungeCmd (workingDir, options) { var cmdi = options.args.indexOf('/c') if (cmdi === -1) return var re = /^\s*("*)([^"]*?\b(?:node|iojs)(?:\.exe|\.EXE)?)("*)( .*)?$/ var npmre = /^\s*("*)([^"]*?\b(?:npm))("*)( |$)/ var path_ = require('path') if (path_.win32) path_ = path_.win32 var command = options.args[cmdi + 1] if (!command) return var m = command.match(re) var replace if (m) { options.originalNode = m[2] replace = m[1] + workingDir + '/node.cmd' + m[3] + m[4] options.args[cmdi + 1] = m[1] + m[2] + m[3] + ' "' + workingDir + '\\node"' + m[4] } else { // XXX probably not a good idea to rewrite to the first npm in the // path if it's a full path to npm. And if it's not a full path to // npm, then the dirname will not work properly! m = command.match(npmre) if (!m) return var npmPath = whichOrUndefined('npm') || 'npm' npmPath = path_.dirname(npmPath) + '\\node_modules\\npm\\bin\\npm-cli.js' replace = m[1] + workingDir + '/node.cmd' + ' "' + npmPath + '"' + m[3] + m[4] options.args[cmdi + 1] = command.replace(npmre, replace) } } function isNode (file) { var cmdname = path.basename(process.execPath).replace(/\.exe$/i, '') return file === 'node' || file === 'iojs' || cmdname === file } function mungeNode (workingDir, options) { options.originalNode = options.file var command = path.basename(options.file).replace(/\.exe$/i, '') // make sure it has a main script. // otherwise, just let it through. var a = 0 var hasMain = false var mainIndex = 1 for (var a = 1; !hasMain && a < options.args.length; a++) { switch (options.args[a]) { case '-p': case '-i': case '--interactive': case '--eval': case '-e': case '-pe': hasMain = false a = options.args.length continue case '-r': case '--require': a += 1 continue default: if (options.args[a].match(/^-/)) { continue } else { hasMain = true mainIndex = a a = options.args.length break } } } if (hasMain) { var replace = workingDir + '/' + command options.args.splice(mainIndex, 0, replace) } // If the file is just something like 'node' then that'll // resolve to our shim, and so to prevent double-shimming, we need // to resolve that here first. // This also handles the case where there's not a main file, like // `node -e 'program'`, where we want to avoid the shim entirely. if (options.file === options.basename) { var realNode = whichOrUndefined(options.file) || process.execPath options.file = options.args[0] = realNode } debug('mungeNode after', options.file, options.args) } function mungeShebang (workingDir, options) { try { var resolved = which.sync(options.file) } catch (er) { // nothing to do if we can't resolve // Most likely: file doesn't exist or is not executable. // Let exec pass through, probably will fail, oh well. return } var shebang = fs.readFileSync(resolved, 'utf8') var match = shebang.match(/^#!([^\r\n]+)/) if (!match) return // not a shebang script, probably a binary var shebangbin = match[1].split(' ')[0] var maybeNode = path.basename(shebangbin) if (!isNode(maybeNode)) return // not a node shebang, leave untouched options.originalNode = shebangbin options.basename = maybeNode options.file = shebangbin options.args = [shebangbin, workingDir + '/' + maybeNode] .concat(resolved) .concat(match[1].split(' ').slice(1)) .concat(options.args.slice(1)) } function mungeEnv (workingDir, options) { var pathEnv for (var i = 0; i < options.envPairs.length; i++) { var ep = options.envPairs[i] if (ep.match(pathRe)) { pathEnv = ep.substr(5) var k = ep.substr(0, 5) options.envPairs[i] = k + workingDir + colon + pathEnv } } if (!pathEnv) { options.envPairs.push((isWindows ? 'Path=' : 'PATH=') + workingDir) } if (options.originalNode) { var key = path.basename(workingDir).substr('.node-spawn-wrap-'.length) options.envPairs.push('SW_ORIG_' + key + '=' + options.originalNode) } if (process.env.SPAWN_WRAP_DEBUG === '1') options.envPairs.push('SPAWN_WRAP_DEBUG=1') } function isnpm (file) { // XXX is this even possible/necessary? // wouldn't npm just be detected as a node shebang? return file === 'npm' && !isWindows } function mungenpm (workingDir, options) { debug('munge npm') // XXX weird effects of replacing a specific npm with a global one var npmPath = whichOrUndefined('npm') if (npmPath) { options.args[0] = npmPath options.file = workingDir + '/node' options.args.unshift(workingDir + '/node') } } function munge (workingDir, options) { options.basename = path.basename(options.file).replace(/\.exe$/i, '') // XXX: dry this if (isSh(options.basename)) { mungeSh(workingDir, options) } else if (isCmd(options.basename)) { mungeCmd(workingDir, options) } else if (isNode(options.basename)) { mungeNode(workingDir, options) } else if (isnpm(options.basename)) { // XXX unnecessary? on non-windows, npm is just another shebang mungenpm(workingDir, options) } else { mungeShebang(workingDir, options) } // now the options are munged into shape. // whether we changed something or not, we still update the PATH // so that if a script somewhere calls `node foo`, it gets our // wrapper instead. mungeEnv(workingDir, options) } function whichOrUndefined (executable) { var path try { path = which.sync(executable) } catch (er) {} return path } function setup (argv, env) { if (argv && typeof argv === 'object' && !env && !Array.isArray(argv)) { env = argv argv = [] } if (!argv && !env) { throw new Error('at least one of "argv" and "env" required') } if (argv) { assert(Array.isArray(argv), 'argv must be array') } else { argv = [] } if (env) { assert(typeof env === 'object', 'env must be an object') } else { env = {} } debug('setup argv=%j env=%j', argv, env) // For stuff like --use_strict or --harmony, we need to inject // the argument *before* the wrap-main. var execArgv = [] for (var i = 0; i < argv.length; i++) { if (argv[i].match(/^-/)) { execArgv.push(argv[i]) if (argv[i] === '-r' || argv[i] === '--require') { execArgv.push(argv[++i]) } } else { break } } if (execArgv.length) { if (execArgv.length === argv.length) { argv.length = 0 } else { argv = argv.slice(execArgv.length) } } var key = process.pid + '-' + crypto.randomBytes(6).toString('hex') var workingDir = homedir + key var settings = JSON.stringify({ module: __filename, deps: { foregroundChild: require.resolve('foreground-child'), signalExit: require.resolve('signal-exit'), }, key: key, workingDir: workingDir, argv: argv, execArgv: execArgv, env: env, root: process.pid }, null, 2) + '\n' signalExit(function () { if (!doDebug) rimraf.sync(workingDir) }) mkdirp.sync(workingDir) workingDir = fs.realpathSync(workingDir) if (isWindows) { var cmdShim = '@echo off\r\n' + 'SETLOCAL\r\n' + 'SET PATHEXT=%PATHEXT:;.JS;=;%\r\n' + '"' + process.execPath + '"' + ' "%~dp0\\.\\node" %*\r\n' fs.writeFileSync(workingDir + '/node.cmd', cmdShim) fs.chmodSync(workingDir + '/node.cmd', '0755') fs.writeFileSync(workingDir + '/iojs.cmd', cmdShim) fs.chmodSync(workingDir + '/iojs.cmd', '0755') } fs.writeFileSync(workingDir + '/node', shim) fs.chmodSync(workingDir + '/node', '0755') fs.writeFileSync(workingDir + '/iojs', shim) fs.chmodSync(workingDir + '/iojs', '0755') var cmdname = path.basename(process.execPath).replace(/\.exe$/i, '') if (cmdname !== 'iojs' && cmdname !== 'node') { fs.writeFileSync(workingDir + '/' + cmdname, shim) fs.chmodSync(workingDir + '/' + cmdname, '0755') } fs.writeFileSync(workingDir + '/settings.json', settings) return workingDir } function runMain () { process.argv.splice(1, 1) process.argv[1] = path.resolve(process.argv[1]) delete require.cache[process.argv[1]] Module.runMain() }