blob: 30a2d32e705852faebce36bb6a28f9a602856d13 [file] [log] [blame]
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()
}