From ff505715890c295d4fdf655640876f900fa88ce3 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 31 Mar 2016 20:43:37 +0200 Subject: [PATCH 1/4] Add special handling for spawning `npm` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As laid out in bcoe/nyc#190, spawn-wrap’ing `npm` does currently not work when using the `npm` binary that comes bundled with Node.js. This adds special handler code for the case that `npm` is the program that should be executed, looking for the `npm` executable in PATH using `which` and invoking the shim with it as an argument. --- index.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/index.js b/index.js index 20fc092..dde86ab 100644 --- a/index.js +++ b/index.js @@ -72,6 +72,13 @@ function wrap (argv, env, workingDir) { 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 + } } } } @@ -133,6 +140,17 @@ function wrap (argv, env, workingDir) { 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 spawn.call(this, options) @@ -141,6 +159,14 @@ function wrap (argv, env, workingDir) { return unwrap } +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. From c89ecc70a6547e669f20d50b3f576e28317c635b Mon Sep 17 00:00:00 2001 From: Benjamin Coe Date: Mon, 4 Apr 2016 00:40:18 +0200 Subject: [PATCH 2/4] Add test for special `npm` handling --- test/basic.js | 36 ++++++++++++++++++++++++++++++++++++ test/fixtures/npm | 4 ++++ 2 files changed, 40 insertions(+) create mode 100755 test/fixtures/npm diff --git a/test/basic.js b/test/basic.js index 67e37f4..000a353 100644 --- a/test/basic.js +++ b/test/basic.js @@ -6,6 +6,7 @@ var winNoSig = isWindows && 'no signals get through cmd' var onExit = require('signal-exit') var cp = require('child_process') var fixture = require.resolve('./fixtures/script.js') +var npmFixture = require.resolve('./fixtures/npm') var fs = require('fs') var path = require('path') @@ -293,6 +294,41 @@ t.test('exec shebang', { skip: winNoShebang }, function (t) { }) }) +// see: https://github.com/bcoe/nyc/issues/190 +t.test('Node 5.8.x + npm 3.7.x - spawn', { skip: winNoShebang }, function (t) { + var npmdir = path.dirname(npmFixture) + process.env.PATH = npmdir + ':' + (process.env.PATH || '') + var child = cp.spawn('npm', ['xyz']) + + var out = '' + child.stdout.on('data', function (c) { + out += c + }) + child.on('close', function (code, signal) { + t.equal(code, 0) + t.equal(signal, null) + t.true(~out.indexOf('xyz')) + t.end() + }) +}) + +t.test('Node 5.8.x + npm 3.7.x - shell', { skip: winNoShebang }, function (t) { + var npmdir = path.dirname(npmFixture) + process.env.PATH = npmdir + ':' + (process.env.PATH || '') + var child = cp.exec('npm xyz') + + var out = '' + child.stdout.on('data', function (c) { + out += c + }) + child.on('close', function (code, signal) { + t.equal(code, 0) + t.equal(signal, null) + t.true(~out.indexOf('xyz')) + t.end() + }) +}) + t.test('--harmony', function (t) { var node = process.execPath var child = cp.spawn(node, ['--harmony', fixture, 'xyz']) diff --git a/test/fixtures/npm b/test/fixtures/npm new file mode 100755 index 0000000..8cb57cf --- /dev/null +++ b/test/fixtures/npm @@ -0,0 +1,4 @@ +#!/bin/sh +// 2>/dev/null; exec "`dirname "$0"`/node" "$0" "$@" +console.log('%j', process.execArgv) +console.log('%j', process.argv.slice(2)) From b1c7b9c1111feee820345f1e98304ff3ec5bf0d6 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 4 Apr 2016 21:03:26 -0700 Subject: [PATCH 3/4] Keep npm fixture alive long enough to receive signals --- test/fixtures/npm | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fixtures/npm b/test/fixtures/npm index 8cb57cf..4e026de 100755 --- a/test/fixtures/npm +++ b/test/fixtures/npm @@ -2,3 +2,4 @@ // 2>/dev/null; exec "`dirname "$0"`/node" "$0" "$@" console.log('%j', process.execArgv) console.log('%j', process.argv.slice(2)) +setTimeout(function () {}, 100) From 57473f8f971d62bc6027283e626ba8d9aa1944a0 Mon Sep 17 00:00:00 2001 From: isaacs Date: Sun, 3 Apr 2016 00:01:37 -0700 Subject: [PATCH 4/4] Detect shebangs absolutely to node, wrap those too Skip on windows, because shebangs basically don't exist there. --- index.js | 22 +++++++++++++ package.json | 3 +- test/abs-shebang.js | 67 ++++++++++++++++++++++++++++++++++++++ test/fixtures/test-shim.js | 3 ++ test/fixtures/wrap.js | 27 +++++++++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 test/abs-shebang.js create mode 100644 test/fixtures/test-shim.js create mode 100644 test/fixtures/wrap.js diff --git a/index.js b/index.js index dde86ab..a5b5d42 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ 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') @@ -126,6 +127,27 @@ function wrap (argv, env, workingDir) { 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) { + + options.file = shebangbin + options.args = [shebangbin, workingDir + '/' + maybeNode] + .concat(resolved) + .concat(match[1].split(' ').slice(1)) + .concat(options.args) + } + } + } } for (var i = 0; i < options.envPairs.length; i++) { diff --git a/package.json b/package.json index 6fd302d..2621a19 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "mkdirp": "^0.5.0", "os-homedir": "^1.0.1", "rimraf": "^2.3.3", - "signal-exit": "^2.0.0" + "signal-exit": "^2.0.0", + "which": "^1.2.4" }, "scripts": { "test": "tap test/*.js" diff --git a/test/abs-shebang.js b/test/abs-shebang.js new file mode 100644 index 0000000..18856c2 --- /dev/null +++ b/test/abs-shebang.js @@ -0,0 +1,67 @@ +var path = require('path') +var fs = require('fs') +var spawn = require('child_process').spawn +var t = require('tap') +var node = process.execPath +var wrap = require.resolve('./fixtures/wrap.js') +var rimraf = require('rimraf') +var mkdirp = require('mkdirp') +var fs = require('fs') + +if (process.platform === 'windows') { + t.plan(0, 'No proper shebang support on windows, so skip this') + process.exit(0) +} + +var expect = + 'before in shim\n' + + 'shebang main\n' + + 'after in shim\n' + + 'before in shim\n' + + 'shebang main\n' + + 'after in shim\n' + +var fixdir = path.resolve(__dirname, 'fixtures', 'shebangs') + +t.test('setup', function (t) { + rimraf.sync(fixdir) + mkdirp.sync(fixdir) + t.end() +}) + +t.test('absolute', function (t) { + var file = path.resolve(fixdir, 'absolute.js') + runTest(file, process.execPath, t) +}) + +t.test('env', function (t) { + var file = path.resolve(fixdir, 'env.js') + runTest(file, '/usr/bin/env node', t) +}) + +function runTest (file, shebang, t) { + var content = '#!' + shebang + '\nconsole.log("shebang main")\n' + fs.writeFileSync(file, content, 'utf8') + fs.chmodSync(file, '0755') + var child = spawn(node, [wrap, file]) + var out = '' + var err = '' + child.stdout.on('data', function (c) { + out += c + }) + child.stderr.on('data', function (c) { + err += c + }) + child.on('close', function (code, signal) { + t.equal(code, 0) + t.equal(signal, null) + t.equal(out, expect) + // console.error(err) + t.end() + }) +} + +t.test('cleanup', function (t) { + rimraf.sync(fixdir) + t.end() +}) diff --git a/test/fixtures/test-shim.js b/test/fixtures/test-shim.js new file mode 100644 index 0000000..b6baf69 --- /dev/null +++ b/test/fixtures/test-shim.js @@ -0,0 +1,3 @@ +console.log('before in shim') +require('../..').runMain() +console.log('after in shim') diff --git a/test/fixtures/wrap.js b/test/fixtures/wrap.js new file mode 100644 index 0000000..a8af6e4 --- /dev/null +++ b/test/fixtures/wrap.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +var sw = require('../..') + +sw([require.resolve('./test-shim.js')]) + +var path = require('path') +var spawn = require('child_process').spawn + +spawn(path.resolve(process.argv[2]), process.argv.slice(3), { + stdio: 'inherit' +}).on('close', function (code, signal) { + if (code || signal) { + throw new Error('failed with ' + (code || signal)) + } + + // now run using PATH + process.env.PATH = path.resolve(path.dirname(process.argv[2])) + + ':' + process.env.PATH + + spawn(path.basename(process.argv[2]), process.argv.slice(3), { + stdio: 'inherit', + }, function (code, signal) { + if (code || signal) { + throw new Error('failed with ' + (code || signal)) + } + }) +})