| 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 home = process.env.SPAWN_WRAP_SHIM_ROOT || require('os-homedir')() |
| var homedir = home + '/.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 shebang = process.platform === 'os390' ? |
| '#!/bin/env ' : '#!' |
| |
| var shim = shebang + 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 |
| if (process.platform === 'os390') |
| child.kill('SIGABRT') |
| else |
| 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) |
| } |
| |
| options.envPairs.push('SPAWN_WRAP_SHIM_ROOT=' + homedir) |
| |
| 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() |
| } |