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 winRebase = require('./lib/win-rebase') var which = require('which') var cmdname = path.basename(process.execPath, '.exe') 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) { rimraf.sync(workingDir) } ChildProcess.prototype.spawn = spawn if (spawnSyncBinding) { spawnSync = spawnSyncBinding.spawn } } if (spawnSyncBinding) { spawnSyncBinding.spawn = wrappedSpawnFunction(spawnSync, workingDir) } ChildProcess.prototype.spawn = wrappedSpawnFunction(spawn, workingDir) return unwrap } function wrappedSpawnFunction (fn, workingDir) { return wrappedSpawn function wrappedSpawn (options) { var pathEnv var cmdi, c, re, match, exe // handle case where node/iojs is exec'd // this doesn't handle EVERYTHING, but just the most common // case of doing `exec(process.execPath + ' file.js') var file = path.basename(options.file, '.exe') if (file === 'dash' || file === 'sh' || file === 'bash' || file === 'zsh') { cmdi = options.args.indexOf('-c') if (cmdi !== -1) { c = options.args[cmdi + 1] re = /^\s*((?:[^\= ]*\=[^\=\s]*\s*)*)([^\s]+|"[^"]+"|'[^']+')/ match = c.match(re) if (match) { exe = path.basename(match[2]) // Strip a trailing quote from the basename if it matches a // leading quote. var quote = match[2].charAt(0) if ((quote === '"' || quote === '\'') && quote === exe.slice(-1)) { exe = exe.slice(0, -1) } if (exe === 'iojs' || exe === 'node' || exe === cmdname) { c = c.replace(re, '$1 ' + workingDir + '/node') options.args[cmdi + 1] = c } else if (exe === 'npm' && !isWindows) { var npmPath = whichOrUndefined('npm') if (npmPath) { c = c.replace(re, '$1 ' + workingDir + '/node ' + npmPath) options.args[cmdi + 1] = c } } } } } else if (isWindows && ( file === path.basename(process.env.comspec || '') || file === 'cmd' )) { cmdi = options.args.indexOf('/c') if (cmdi !== -1) { options.args[cmdi + 1] = winRebase(options.args[cmdi + 1], workingDir + '/node.cmd', whichOrUndefined) } } else if (file === 'node' || file === 'iojs' || cmdname === file) { // make sure it has a main script. // otherwise, just let it through. var a = 0 var hasMain = false for (var a = 1; !hasMain && a < options.args.length; a++) { switch (options.args[a]) { 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 a = options.args.length break } } } if (hasMain) { options.file = workingDir + '/' + file options.args[0] = workingDir + '/' + file } } else { try { var resolved = which.sync(options.file) } catch (er) {} if (resolved) { var shebang = fs.readFileSync(resolved, 'utf8') var match = shebang.match(/^#!([^\r\n]+)/) if (match) { var shebangbin = match[1].split(' ')[0] var maybeNode = path.basename(shebangbin) if (maybeNode === 'node' || maybeNode === 'iojs' || cmdname === maybeNode) { file = options.file = shebangbin options.args = [shebangbin, workingDir + '/' + maybeNode] .concat(resolved) .concat(match[1].split(' ').slice(1)) .concat(options.args.slice(1)) } } } } 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 (file === 'npm' && !isWindows) { var npmPath = whichOrUndefined('npm') if (npmPath) { options.args[0] = npmPath options.file = workingDir + '/node' options.args.unshift(workingDir + '/node') } } if (isWindows) fixWindowsBins(workingDir, options) return fn.call(this, options) } } function whichOrUndefined (executable) { var path try { path = which.sync(executable) } catch (er) {} return path } // by default Windows will reference the full // path to the node.exe or iojs.exe as the bin, // we should instead point spawn() at our .cmd shim. function fixWindowsBins (workingDir, options) { var re = /.*\b(node|iojs)(\.exe)?$/ if (options.file.match(re)) { options.file = process.execPath var shim = workingDir + '/node' options.args.splice(0, 1, options.file, workingDir + '/node') } } 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 = {} } // 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 settings = JSON.stringify({ module: __filename, deps: { foregroundChild: require.resolve('foreground-child'), signalExit: require.resolve('signal-exit'), }, argv: argv, execArgv: execArgv, env: env, root: process.pid }, null, 2) + '\n' var workingDir = homedir + process.pid + '-' + crypto.randomBytes(6).toString('hex') signalExit(function () { 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') 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]) Module.runMain() }