├── .gitignore ├── test ├── fixtures │ ├── npm.cmd │ ├── test-shim.js │ ├── node_modules │ │ └── npm │ │ │ └── bin │ │ │ └── npm-cli.js │ ├── npm │ ├── script.js │ └── wrap.js ├── shim-root-space.js ├── exec-flag.js ├── wrap-twice.js ├── abs-shebang.js ├── double-wrap.js └── basic.js ├── CONTRIBUTING.md ├── lib ├── homedir.js ├── which-or-undefined.js ├── debug.js ├── mungers │ ├── npm.js │ ├── shebang.js │ ├── env.js │ ├── cmd.js │ ├── sh.js │ └── node.js ├── exe-type.js └── munge.js ├── .github └── workflows │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── package-json-repo.js │ ├── ci.yml │ └── isaacs-makework.yml ├── package.json ├── LICENSE.md ├── CHANGELOG.md ├── README.md ├── index.js └── shim.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .nyc_output 3 | coverage/ 4 | -------------------------------------------------------------------------------- /test/fixtures/npm.cmd: -------------------------------------------------------------------------------- 1 | @echo This code should never be executed. 2 | exit /B 1 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please consider signing [the neveragain.tech pledge](http://neveragain.tech/) 2 | -------------------------------------------------------------------------------- /test/fixtures/test-shim.js: -------------------------------------------------------------------------------- 1 | console.log('before in shim') 2 | require('../..').runMain() 3 | console.log('after in shim') 4 | -------------------------------------------------------------------------------- /lib/homedir.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | 5 | module.exports = process.env.SPAWN_WRAP_SHIM_ROOT || os.homedir() 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/npm/bin/npm-cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | console.log('%j', process.execArgv) 3 | console.log('%j', process.argv.slice(2)) 4 | setTimeout(function () {}, 100) 5 | -------------------------------------------------------------------------------- /test/fixtures/npm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | // 2>/dev/null; exec "`dirname "$0"`/node" "$0" "$@" 3 | console.log('%j', process.execArgv) 4 | console.log('%j', process.argv.slice(2)) 5 | setTimeout(function () {}, 100) 6 | -------------------------------------------------------------------------------- /test/fixtures/script.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | console.log('%j', process.execArgv) 3 | console.log('%j', process.argv.slice(2)) 4 | 5 | // Keep the event loop alive long enough to receive signals. 6 | setTimeout(function() {}, 100) 7 | -------------------------------------------------------------------------------- /lib/which-or-undefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const which = require("which") 4 | 5 | function whichOrUndefined(executable) { 6 | try { 7 | return which.sync(executable) 8 | } catch (error) { 9 | } 10 | } 11 | 12 | module.exports = whichOrUndefined 13 | -------------------------------------------------------------------------------- /.github/workflows/commit-if-modified.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.email "$1" 3 | shift 4 | git config --global user.name "$1" 5 | shift 6 | message="$1" 7 | shift 8 | if [ $(git status --porcelain "$@" | egrep '^ M' | wc -l) -gt 0 ]; then 9 | git add "$@" 10 | git commit -m "$message" 11 | git push || git pull --rebase 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /.github/workflows/copyright-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=${1:-$PWD} 3 | dates=($(git log --date=format:%Y --pretty=format:'%ad' --reverse | sort | uniq)) 4 | if [ "${#dates[@]}" -eq 1 ]; then 5 | datestr="${dates}" 6 | else 7 | datestr="${dates}-${dates[${#dates[@]}-1]}" 8 | fi 9 | 10 | stripDate='s/^((.*)Copyright\b(.*?))((?:,\s*)?(([0-9]{4}\s*-\s*[0-9]{4})|(([0-9]{4},\s*)*[0-9]{4})))(?:,)?\s*(.*)\n$/$1$9\n/g' 11 | addDate='s/^.*Copyright(?:\s*\(c\))? /Copyright \(c\) '$datestr' /g' 12 | for l in $dir/LICENSE*; do 13 | perl -pi -e "$stripDate" $l 14 | perl -pi -e "$addDate" $l 15 | done 16 | -------------------------------------------------------------------------------- /.github/workflows/package-json-repo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pf = require.resolve(`${process.cwd()}/package.json`) 4 | const pj = require(pf) 5 | 6 | if (!pj.repository && process.env.GITHUB_REPOSITORY) { 7 | const fs = require('fs') 8 | const server = process.env.GITHUB_SERVER_URL || 'https://github.com' 9 | const repo = `${server}/${process.env.GITHUB_REPOSITORY}` 10 | pj.repository = repo 11 | const json = fs.readFileSync(pf, 'utf8') 12 | const match = json.match(/^\s*\{[\r\n]+([ \t]*)"/) 13 | const indent = match[1] 14 | const output = JSON.stringify(pj, null, indent || 2) + '\n' 15 | fs.writeFileSync(pf, output) 16 | } 17 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const fs = require('fs') 5 | 6 | /** 7 | * Boolean indicating if debug mode is enabled. 8 | * 9 | * @type {boolean} 10 | */ 11 | const IS_DEBUG = process.env.SPAWN_WRAP_DEBUG === '1' 12 | 13 | /** 14 | * If debug is enabled, write message to stderr. 15 | * 16 | * If debug is disabled, no message is written. 17 | */ 18 | function debug(...args) { 19 | if (!IS_DEBUG) { 20 | return; 21 | } 22 | 23 | const prefix = `SW ${process.pid}: ` 24 | const data = util.format(...args).trim() 25 | const message = data.split('\n').map(line => `${prefix}${line}\n`).join('') 26 | fs.writeSync(2, message) 27 | } 28 | 29 | module.exports = { 30 | IS_DEBUG, 31 | debug, 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/wrap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var sw = require('../..') 3 | 4 | sw([require.resolve('./test-shim.js')]) 5 | 6 | var path = require('path') 7 | var spawn = require('child_process').spawn 8 | 9 | spawn(path.resolve(process.argv[2]), process.argv.slice(3), { 10 | stdio: 'inherit' 11 | }).on('close', function (code, signal) { 12 | if (code || signal) { 13 | throw new Error('failed with ' + (code || signal)) 14 | } 15 | 16 | // now run using PATH 17 | process.env.PATH = path.resolve(path.dirname(process.argv[2])) + 18 | ':' + process.env.PATH 19 | 20 | spawn(path.basename(process.argv[2]), process.argv.slice(3), { 21 | stdio: 'inherit', 22 | }, function (code, signal) { 23 | if (code || signal) { 24 | throw new Error('failed with ' + (code || signal)) 25 | } 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/shim-root-space.js: -------------------------------------------------------------------------------- 1 | const IS_WINDOWS = require('is-windows')(); 2 | const tap = require('tap'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | tap.test('spaces in shim-root/homedir path on windows do not break wrapped npm calls', { skip: !IS_WINDOWS }, function(t) { 7 | // create temp folder with spaces in path 8 | process.env.SPAWN_WRAP_SHIM_ROOT = path.join(__dirname, 'fixtures', 'space path'); 9 | 10 | // wrap with custom root path 11 | const sw = require('..'); 12 | const unwrap = sw([]); 13 | 14 | // run a child process with an npm command 15 | const cp = require('child_process'); 16 | const child = cp.exec('npm --version'); 17 | child.on('exit', function(code) { 18 | t.equal(code, 0); 19 | unwrap(); 20 | fs.rmdirSync(process.env.SPAWN_WRAP_SHIM_ROOT); 21 | t.end(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [12.x, 14.x, 16.x, 17.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | - os: macos-latest 14 | shell: bash 15 | - os: windows-latest 16 | shell: bash 17 | - os: windows-latest 18 | shell: powershell 19 | fail-fast: false 20 | 21 | runs-on: ${{ matrix.platform.os }} 22 | defaults: 23 | run: 24 | shell: ${{ matrix.platform.shell }} 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v1.1.0 29 | 30 | - name: Use Nodejs ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Run Tests 39 | run: npm test -- -c -t0 40 | -------------------------------------------------------------------------------- /lib/mungers/npm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require("path") 4 | const {debug} = require("../debug") 5 | const whichOrUndefined = require("../which-or-undefined") 6 | 7 | /** 8 | * Intercepts npm spawned processes. 9 | * 10 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 11 | * @param options {import("../munge").InternalSpawnOptions} Original internal spawn options. 12 | * @return {import("../munge").InternalSpawnOptions} Updated internal spawn options. 13 | */ 14 | function mungeNpm(workingDir, options) { 15 | debug('munge npm') 16 | // XXX weird effects of replacing a specific npm with a global one 17 | const npmPath = whichOrUndefined('npm') 18 | 19 | if (npmPath === undefined) { 20 | return {...options}; 21 | } 22 | 23 | const newArgs = [...options.args] 24 | 25 | newArgs[0] = npmPath 26 | const file = path.join(workingDir, 'node') 27 | newArgs.unshift(file) 28 | 29 | return {...options, file, args: newArgs} 30 | } 31 | 32 | module.exports = mungeNpm 33 | -------------------------------------------------------------------------------- /test/exec-flag.js: -------------------------------------------------------------------------------- 1 | var sw = require('../') 2 | 3 | if (process.argv[2] === 'wrapper') { 4 | // note: this should never happen, 5 | // because -e invocations aren't wrapped 6 | throw new Error('this wrapper should not be executed') 7 | } 8 | 9 | var t = require('tap') 10 | var cp = require('child_process') 11 | var spawn = cp.spawn 12 | var exec = cp.exec 13 | var nodes = [ 'node', process.execPath ] 14 | 15 | sw([__filename, 'wrapper']) 16 | 17 | t.test('try to wrap a -e invocation but it isnt wrapped', function (t) { 18 | nodes.forEach(function (node) { 19 | t.test(node, function (t) { 20 | var script = "console.log('hello')\n" 21 | var child = spawn(node, ['-e', script]) 22 | var out = '' 23 | child.stdout.on('data', function (c) { out += c }) 24 | child.stderr.on('data', function (c) { process.stderr.write(c) }) 25 | child.on('close', function (code, signal) { 26 | var actual = { 27 | out: out, 28 | code: code, 29 | signal: signal 30 | } 31 | var expect = { 32 | out: 'hello\n', 33 | code: 0, 34 | signal: null 35 | } 36 | t.match(actual, expect) 37 | t.end() 38 | }) 39 | }) 40 | }) 41 | t.end() 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spawn-wrap", 3 | "version": "2.0.0", 4 | "description": "Wrap all spawned Node.js child processes by adding environs and arguments ahead of the main JavaScript file argument.", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=8" 8 | }, 9 | "dependencies": { 10 | "foreground-child": "^2.0.0", 11 | "is-windows": "^1.0.2", 12 | "make-dir": "^3.0.0", 13 | "rimraf": "^3.0.0", 14 | "signal-exit": "^3.0.2", 15 | "which": "^2.0.1" 16 | }, 17 | "scripts": { 18 | "test": "tap", 19 | "release": "standard-version", 20 | "clean": "rm -rf ~/.node-spawn-wrap-*" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/istanbuljs/spawn-wrap.git" 25 | }, 26 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 27 | "license": "BlueOak-1.0.0", 28 | "bugs": { 29 | "url": "https://github.com/istanbuljs/spawn-wrap/issues" 30 | }, 31 | "homepage": "https://github.com/istanbuljs/spawn-wrap#readme", 32 | "devDependencies": { 33 | "standard-version": "^7.1.0", 34 | "tap": "^15.1.6" 35 | }, 36 | "files": [ 37 | "lib/", 38 | "index.js", 39 | "shim.js" 40 | ], 41 | "tap": { 42 | "coverage": false, 43 | "timeout": 240 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/isaacs-makework.yml: -------------------------------------------------------------------------------- 1 | name: "various tidying up tasks to silence nagging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | makework: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2.1.4 18 | with: 19 | node-version: 16.x 20 | - name: put repo in package.json 21 | run: node .github/workflows/package-json-repo.js 22 | - name: check in package.json if modified 23 | run: | 24 | bash -x .github/workflows/commit-if-modified.sh \ 25 | "package-json-repo-bot@example.com" \ 26 | "package.json Repo Bot" \ 27 | "chore: add repo to package.json" \ 28 | package.json package-lock.json 29 | - name: put all dates in license copyright line 30 | run: bash .github/workflows/copyright-year.sh 31 | - name: check in licenses if modified 32 | run: | 33 | bash .github/workflows/commit-if-modified.sh \ 34 | "license-year-bot@example.com" \ 35 | "License Year Bot" \ 36 | "chore: add copyright year to license" \ 37 | LICENSE* 38 | -------------------------------------------------------------------------------- /test/wrap-twice.js: -------------------------------------------------------------------------------- 1 | var sw = require('../') 2 | var argv = process.argv.slice(1).map(function (arg) { 3 | if (arg === __filename) 4 | arg = 'double-wrap.js' 5 | return arg 6 | }) 7 | 8 | var node = process.execPath 9 | var fg = require('foreground-child') 10 | 11 | // apply 2 spawn-wraps, make sure they don't clobber one another 12 | switch (process.argv[2]) { 13 | case 'outer': 14 | console.log('outer') 15 | sw.runMain() 16 | break 17 | 18 | case 'inner': 19 | console.log('inner') 20 | sw.runMain() 21 | break 22 | 23 | case 'main': 24 | console.log('main') 25 | sw([__filename, 'outer']) 26 | sw([__filename, 'inner']) 27 | fg(node, [__filename, 'parent']) 28 | break 29 | 30 | case 'parent': 31 | console.log('parent') 32 | fg(node, [__filename, 'child']) 33 | break 34 | 35 | case 'child': 36 | console.log('child') 37 | break 38 | 39 | default: 40 | runTest() 41 | break 42 | } 43 | 44 | function runTest () { 45 | var t = require('tap') 46 | var spawn = require('child_process').spawn 47 | var child = spawn(node, [__filename, 'main']) 48 | // child.stderr.pipe(process.stderr) 49 | var out = '' 50 | child.stdout.on('data', function (c) { 51 | out += c 52 | }) 53 | child.on('close', function (code, signal) { 54 | t.notOk(code) 55 | t.notOk(signal) 56 | t.equal(out, 'main\nouter\ninner\nparent\nouter\ninner\nchild\n') 57 | t.end() 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /lib/mungers/shebang.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require("fs") 4 | const path = require("path") 5 | const {isNode} = require("../exe-type") 6 | const whichOrUndefined = require("../which-or-undefined") 7 | 8 | /** 9 | * Intercepts processes spawned through a script with a shebang line. 10 | * 11 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 12 | * @param options {import("../munge").InternalSpawnOptions} Original internal spawn options. 13 | * @return {import("../munge").InternalSpawnOptions} Updated internal spawn options. 14 | */ 15 | function mungeShebang(workingDir, options) { 16 | const resolved = whichOrUndefined(options.file) 17 | if (resolved === undefined) { 18 | return {...options} 19 | } 20 | 21 | const shebang = fs.readFileSync(resolved, 'utf8') 22 | const match = shebang.match(/^#!([^\r\n]+)/) 23 | if (!match) { 24 | return {...options} // not a shebang script, probably a binary 25 | } 26 | 27 | const shebangbin = match[1].split(' ')[0] 28 | const maybeNode = path.basename(shebangbin) 29 | if (!isNode(maybeNode)) { 30 | return {...options} // not a node shebang, leave untouched 31 | } 32 | 33 | const originalNode = shebangbin 34 | const file = shebangbin 35 | const args = [shebangbin, path.join(workingDir, maybeNode)] 36 | .concat(resolved) 37 | .concat(match[1].split(' ').slice(1)) 38 | .concat(options.args.slice(1)) 39 | 40 | return {...options, file, args, originalNode}; 41 | } 42 | 43 | module.exports = mungeShebang 44 | -------------------------------------------------------------------------------- /lib/exe-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isWindows = require("is-windows") 4 | const path = require("path") 5 | 6 | function isCmd(file) { 7 | const comspec = path.basename(process.env.comspec || '').replace(/\.exe$/i, '') 8 | return isWindows() && (file === comspec || /^cmd(?:\.exe)?$/i.test(file)) 9 | } 10 | 11 | function isNode(file) { 12 | const cmdname = path.basename(process.execPath).replace(/\.exe$/i, '') 13 | return file === 'node' || cmdname === file 14 | } 15 | 16 | function isNpm(file) { 17 | // XXX is this even possible/necessary? 18 | // wouldn't npm just be detected as a node shebang? 19 | return file === 'npm' && !isWindows() 20 | } 21 | 22 | function isSh(file) { 23 | return ['dash', 'sh', 'bash', 'zsh'].includes(file) 24 | } 25 | 26 | /** 27 | * Returns the basename of the executable. 28 | * 29 | * On Windows, strips the `.exe` extension (if any) and normalizes the name to 30 | * lowercase. 31 | * 32 | * @param exePath {string} Path of the executable as passed to spawned processes: 33 | * either command or a path to a file. 34 | * @return {string} Basename of the executable. 35 | */ 36 | function getExeBasename(exePath) { 37 | const baseName = path.basename(exePath); 38 | if (isWindows()) { 39 | // Stripping `.exe` seems to be enough for our usage. We may eventually 40 | // want to handle all executable extensions (such as `.bat` or `.cmd`). 41 | return baseName.replace(/\.exe$/i, "").toLowerCase(); 42 | } else { 43 | return baseName; 44 | } 45 | } 46 | 47 | module.exports = { 48 | isCmd, 49 | isNode, 50 | isNpm, 51 | isSh, 52 | getExeBasename, 53 | } 54 | -------------------------------------------------------------------------------- /lib/mungers/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isWindows = require("is-windows") 4 | const path = require("path") 5 | const homedir = require("../homedir") 6 | 7 | const pathRe = isWindows() ? /^PATH=/i : /^PATH=/; 8 | 9 | /** 10 | * Updates the environment variables to intercept `node` commands and pass down options. 11 | * 12 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 13 | * @param options {import("../munge").InternalSpawnOptions} Original internal spawn options. 14 | * @return {import("../munge").InternalSpawnOptions} Updated internal spawn options. 15 | */ 16 | function mungeEnv(workingDir, options) { 17 | let pathEnv 18 | 19 | const envPairs = options.envPairs.map((ep) => { 20 | if (pathRe.test(ep)) { 21 | // `PATH` env var: prefix its value with `workingDir` 22 | // `5` corresponds to the length of `PATH=` 23 | pathEnv = ep.substr(5) 24 | const k = ep.substr(0, 5) 25 | return k + workingDir + path.delimiter + pathEnv 26 | } else { 27 | // Return as-is 28 | return ep; 29 | } 30 | }); 31 | 32 | if (pathEnv === undefined) { 33 | envPairs.push((isWindows() ? 'Path=' : 'PATH=') + workingDir) 34 | } 35 | if (options.originalNode) { 36 | const key = path.basename(workingDir).substr('.node-spawn-wrap-'.length) 37 | envPairs.push('SW_ORIG_' + key + '=' + options.originalNode) 38 | } 39 | 40 | envPairs.push('SPAWN_WRAP_SHIM_ROOT=' + homedir) 41 | 42 | if (process.env.SPAWN_WRAP_DEBUG === '1') { 43 | envPairs.push('SPAWN_WRAP_DEBUG=1') 44 | } 45 | 46 | return {...options, envPairs}; 47 | } 48 | 49 | module.exports = mungeEnv 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Blue Oak Model License 2 | 3 | Version 1.0.0 4 | 5 | ## Purpose 6 | 7 | This license gives everyone as much permission to work with 8 | this software as possible, while protecting contributors 9 | from liability. 10 | 11 | ## Acceptance 12 | 13 | In order to receive this license, you must agree to its 14 | rules. The rules of this license are both obligations 15 | under that agreement and conditions to your license. 16 | You must not do anything with this software that triggers 17 | a rule that you cannot or will not follow. 18 | 19 | ## Copyright 20 | 21 | Each contributor licenses you to do everything with this 22 | software that would otherwise infringe that contributor's 23 | copyright in it. 24 | 25 | ## Notices 26 | 27 | You must ensure that everyone who gets a copy of 28 | any part of this software from you, with or without 29 | changes, also gets the text of this license or a link to 30 | . 31 | 32 | ## Excuse 33 | 34 | If anyone notifies you in writing that you have not 35 | complied with [Notices](#notices), you can keep your 36 | license by taking all practical steps to comply within 30 37 | days after the notice. If you do not do so, your license 38 | ends immediately. 39 | 40 | ## Patent 41 | 42 | Each contributor licenses you to do everything with this 43 | software that would otherwise infringe any patent claims 44 | they can license or become able to license. 45 | 46 | ## Reliability 47 | 48 | No contributor can revoke this license. 49 | 50 | ## No Liability 51 | 52 | ***As far as the law allows, this software comes as is, 53 | without any warranty or condition, and no contributor 54 | will be liable to anyone for any damages related to this 55 | software or this license, under any kind of legal claim.*** 56 | -------------------------------------------------------------------------------- /test/abs-shebang.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | var spawn = require('child_process').spawn 4 | var t = require('tap') 5 | var node = process.execPath 6 | var wrap = require.resolve('./fixtures/wrap.js') 7 | var rimraf = require('rimraf') 8 | var mkdirp = require('mkdirp') 9 | 10 | if (process.platform === 'win32') { 11 | t.plan(0, 'No proper shebang support on windows, so skip this') 12 | process.exit(0) 13 | } 14 | 15 | var expect = 16 | 'before in shim\n' + 17 | 'shebang main foo,bar\n' + 18 | 'after in shim\n' + 19 | 'before in shim\n' + 20 | 'shebang main foo,bar\n' + 21 | 'after in shim\n' 22 | 23 | var fixdir = path.resolve(__dirname, 'fixtures', 'shebangs') 24 | 25 | t.test('setup', function (t) { 26 | rimraf.sync(fixdir) 27 | mkdirp.sync(fixdir) 28 | t.end() 29 | }) 30 | 31 | t.test('absolute', function (t) { 32 | var file = path.resolve(fixdir, 'absolute.js') 33 | runTest(file, process.execPath, t) 34 | }) 35 | 36 | t.test('env', function (t) { 37 | var file = path.resolve(fixdir, 'env.js') 38 | runTest(file, '/usr/bin/env node', t) 39 | }) 40 | 41 | function runTest (file, shebang, t) { 42 | var content = '#!' + shebang + '\n' + 43 | 'console.log("shebang main " + process.argv.slice(2))\n' 44 | fs.writeFileSync(file, content, 'utf8') 45 | fs.chmodSync(file, '0755') 46 | var child = spawn(node, [wrap, file, 'foo', 'bar']) 47 | var out = '' 48 | var err = '' 49 | child.stdout.on('data', function (c) { 50 | out += c 51 | }) 52 | child.stderr.on('data', function (c) { 53 | err += c 54 | }) 55 | child.on('close', function (code, signal) { 56 | t.equal(code, 0) 57 | t.equal(signal, null) 58 | t.equal(out, expect) 59 | t.end() 60 | }) 61 | } 62 | 63 | t.test('cleanup', function (t) { 64 | rimraf.sync(fixdir) 65 | t.end() 66 | }) 67 | -------------------------------------------------------------------------------- /test/double-wrap.js: -------------------------------------------------------------------------------- 1 | var sw = require('../') 2 | var argv = process.argv.slice(1).map(function (arg) { 3 | if (arg === __filename) 4 | arg = 'double-wrap.js' 5 | return arg 6 | }) 7 | 8 | var node = process.execPath 9 | var fg = require('foreground-child') 10 | 11 | /* 12 | main adds sw([first]), spawns 'parent' 13 | first outputs some junk, calls runMain 14 | parent adds sw([second]), spawns 'child' 15 | second outputs some junk, calls runMain 16 | child outputs some junk 17 | */ 18 | 19 | switch (process.argv[2]) { 20 | case 'main': 21 | console.error('main', process.pid, process.execArgv.concat(argv)) 22 | console.log('main') 23 | sw([__filename, 'first']) 24 | fg(node, [__filename, 'parent']) 25 | break 26 | case 'first': 27 | console.error('first', process.pid, process.execArgv.concat(argv)) 28 | console.log('first') 29 | sw.runMain() 30 | break 31 | case 'parent': 32 | console.error('parent', process.pid, process.execArgv.concat(argv)) 33 | console.log('parent') 34 | sw([__filename, 'second']) 35 | fg(node, [__filename, 'child']) 36 | break 37 | case 'second': 38 | console.error('second', process.pid, process.execArgv.concat(argv)) 39 | console.log('second') 40 | sw.runMain() 41 | break 42 | case 'child': 43 | console.error('child', process.pid, process.execArgv.concat(argv)) 44 | console.log('child') 45 | break 46 | default: 47 | runTest() 48 | break 49 | } 50 | 51 | function runTest () { 52 | var t = require('tap') 53 | var spawn = require('child_process').spawn 54 | var child = spawn(node, [__filename, 'main']) 55 | // child.stderr.pipe(process.stderr) 56 | var out = '' 57 | child.stdout.on('data', function (c) { 58 | out += c 59 | }) 60 | child.on('close', function (code, signal) { 61 | t.notOk(code) 62 | t.notOk(signal) 63 | t.equal(out, 'main\nfirst\nparent\nfirst\nsecond\nchild\n') 64 | t.end() 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /lib/mungers/cmd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require("path") 4 | const whichOrUndefined = require("../which-or-undefined") 5 | 6 | /** 7 | * Intercepts Node and npm processes spawned through Windows' `cmd.exe`. 8 | * 9 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 10 | * @param options {import("../munge").InternalSpawnOptions} Original internal spawn options. 11 | * @return {import("../munge").InternalSpawnOptions} Updated internal spawn options. 12 | */ 13 | function mungeCmd(workingDir, options) { 14 | const cmdi = options.args.indexOf('/c') 15 | if (cmdi === -1) { 16 | return {...options} 17 | } 18 | 19 | const re = /^\s*("*)([^"]*?\bnode(?:\.exe|\.EXE)?)("*)( .*)?$/ 20 | const npmre = /^\s*("*)([^"]*?\b(?:npm))("*)( |$)/ 21 | 22 | const command = options.args[cmdi + 1] 23 | if (command === undefined) { 24 | return {...options} 25 | } 26 | 27 | let newArgs = [...options.args]; 28 | // Remember the original Node command to use it in the shim 29 | let originalNode; 30 | 31 | let m = command.match(re) 32 | let replace 33 | if (m) { 34 | originalNode = m[2] 35 | // TODO: Remove `replace`: seems unused 36 | replace = m[1] + path.join(workingDir, 'node.cmd') + m[3] + m[4] 37 | newArgs[cmdi + 1] = m[1] + m[2] + m[3] + 38 | ' "' + path.join(workingDir, 'node') + '"' + m[4] 39 | } else { 40 | // XXX probably not a good idea to rewrite to the first npm in the 41 | // path if it's a full path to npm. And if it's not a full path to 42 | // npm, then the dirname will not work properly! 43 | m = command.match(npmre) 44 | if (m === null) { 45 | return {...options} 46 | } 47 | 48 | let npmPath = whichOrUndefined('npm') || 'npm' 49 | npmPath = path.join(path.dirname(npmPath), 'node_modules', 'npm', 'bin', 'npm-cli.js') 50 | replace = m[1] + '"' + path.join(workingDir, 'node.cmd') + '"' + 51 | ' "' + npmPath + '"' + 52 | m[3] + m[4] 53 | newArgs[cmdi + 1] = command.replace(npmre, replace) 54 | } 55 | 56 | return {...options, args: newArgs, originalNode}; 57 | } 58 | 59 | module.exports = mungeCmd 60 | -------------------------------------------------------------------------------- /lib/mungers/sh.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isWindows = require("is-windows") 4 | const path = require("path") 5 | const {debug} = require("../debug") 6 | const {isNode} = require("../exe-type") 7 | const whichOrUndefined = require("../which-or-undefined") 8 | 9 | /** 10 | * Intercepts Node and npm processes spawned through a Linux shell. 11 | * 12 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 13 | * @param options {import("../munge").InternalSpawnOptions} Original internal spawn options. 14 | * @return {import("../munge").InternalSpawnOptions} Updated internal spawn options. 15 | */ 16 | function mungeSh(workingDir, options) { 17 | const cmdi = options.args.indexOf('-c') 18 | if (cmdi === -1) { 19 | return {...options} // no -c argument 20 | } 21 | 22 | let c = options.args[cmdi + 1] 23 | const re = /^\s*((?:[^\= ]*\=[^\=\s]*)*[\s]*)([^\s]+|"[^"]+"|'[^']+')( .*)?$/ 24 | const match = c.match(re) 25 | if (match === null) { 26 | return {...options} // not a command invocation. weird but possible 27 | } 28 | 29 | let command = match[2] 30 | // strip quotes off the command 31 | const quote = command.charAt(0) 32 | if ((quote === '"' || quote === '\'') && command.endsWith(quote)) { 33 | command = command.slice(1, -1) 34 | } 35 | const exe = path.basename(command) 36 | 37 | let newArgs = [...options.args]; 38 | // Remember the original Node command to use it in the shim 39 | let originalNode; 40 | const workingNode = path.join(workingDir, 'node') 41 | 42 | if (isNode(exe)) { 43 | originalNode = command 44 | c = `${match[1]}${match[2]} "${workingNode}" ${match[3]}` 45 | newArgs[cmdi + 1] = c 46 | } else if (exe === 'npm' && !isWindows()) { 47 | // XXX this will exhibit weird behavior when using /path/to/npm, 48 | // if some other npm is first in the path. 49 | const npmPath = whichOrUndefined('npm') 50 | 51 | if (npmPath) { 52 | c = c.replace(re, `$1 "${workingNode}" "${npmPath}" $3`) 53 | newArgs[cmdi + 1] = c 54 | debug('npm munge!', c) 55 | } 56 | } 57 | 58 | return {...options, args: newArgs, originalNode}; 59 | } 60 | 61 | module.exports = mungeSh 62 | -------------------------------------------------------------------------------- /lib/mungers/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path') 4 | const {debug} = require("../debug") 5 | const {getExeBasename} = require("../exe-type") 6 | const whichOrUndefined = require("../which-or-undefined") 7 | 8 | /** 9 | * Intercepts Node spawned processes. 10 | * 11 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 12 | * @param options {import("../munge").InternalSpawnOptions} Original internal spawn options. 13 | * @return {import("../munge").InternalSpawnOptions} Updated internal spawn options. 14 | */ 15 | function mungeNode(workingDir, options) { 16 | // Remember the original Node command to use it in the shim 17 | const originalNode = options.file 18 | 19 | const command = getExeBasename(options.file) 20 | // make sure it has a main script. 21 | // otherwise, just let it through. 22 | let a = 0 23 | let hasMain = false 24 | let mainIndex = 1 25 | for (a = 1; !hasMain && a < options.args.length; a++) { 26 | switch (options.args[a]) { 27 | case '-p': 28 | case '-i': 29 | case '--interactive': 30 | case '--eval': 31 | case '-e': 32 | case '-pe': 33 | hasMain = false 34 | a = options.args.length 35 | continue 36 | 37 | case '-r': 38 | case '--require': 39 | a += 1 40 | continue 41 | 42 | default: 43 | // TODO: Double-check this part 44 | if (options.args[a].startsWith('-')) { 45 | continue 46 | } else { 47 | hasMain = true 48 | mainIndex = a 49 | a = options.args.length 50 | break 51 | } 52 | } 53 | } 54 | 55 | const newArgs = [...options.args]; 56 | let newFile = options.file; 57 | 58 | if (hasMain) { 59 | const replace = path.join(workingDir, command) 60 | newArgs.splice(mainIndex, 0, replace) 61 | } 62 | 63 | // If the file is just something like 'node' then that'll 64 | // resolve to our shim, and so to prevent double-shimming, we need 65 | // to resolve that here first. 66 | // This also handles the case where there's not a main file, like 67 | // `node -e 'program'`, where we want to avoid the shim entirely. 68 | if (options.file === command) { 69 | const realNode = whichOrUndefined(options.file) || process.execPath 70 | newArgs[0] = realNode 71 | newFile = realNode 72 | } 73 | 74 | debug('mungeNode after', options.file, options.args) 75 | 76 | return {...options, file: newFile, args: newArgs, originalNode}; 77 | } 78 | 79 | module.exports = mungeNode 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.0.0](https://github.com/istanbuljs/spawn-wrap/compare/v1.4.3...v2.0.0) (2019-12-20) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * no longer feature detect spawnSync, present since Node 0.11. 11 | * Drop support for iojs (#84) 12 | * explicitly drops support for Node 6 13 | 14 | ### Bug Fixes 15 | 16 | * Avoid path concatenation ([5626f2a](https://github.com/istanbuljs/spawn-wrap/commit/5626f2a)) 17 | * Handle whitespace in homedir paths ([#98](https://github.com/istanbuljs/spawn-wrap/issues/98)) ([f002ecc](https://github.com/istanbuljs/spawn-wrap/commit/f002ecc)), closes [istanbuljs/nyc#784](https://github.com/istanbuljs/nyc/issues/784) 18 | * Make munge functions pure ([#99](https://github.com/istanbuljs/spawn-wrap/issues/99)) ([5c1293e](https://github.com/istanbuljs/spawn-wrap/commit/5c1293e)) 19 | * Remove '/.node-spawn-wrap-' from lib/homedir.js export ([5bcb288](https://github.com/istanbuljs/spawn-wrap/commit/5bcb288)) 20 | * Remove legacy `ChildProcess` resolution ([#85](https://github.com/istanbuljs/spawn-wrap/issues/85)) ([da05012](https://github.com/istanbuljs/spawn-wrap/commit/da05012)) 21 | * Remove legacy `spawnSync` feature detection ([#87](https://github.com/istanbuljs/spawn-wrap/issues/87)) ([78777aa](https://github.com/istanbuljs/spawn-wrap/commit/78777aa)) 22 | * Switch from mkdirp to make-dir ([#94](https://github.com/istanbuljs/spawn-wrap/issues/94)) ([b8dace1](https://github.com/istanbuljs/spawn-wrap/commit/b8dace1)) 23 | * Use `is-windows` package for detection ([#88](https://github.com/istanbuljs/spawn-wrap/issues/88)) ([c3e6239](https://github.com/istanbuljs/spawn-wrap/commit/c3e6239)), closes [istanbuljs/spawn-wrap#61](https://github.com/istanbuljs/spawn-wrap/issues/61) 24 | * Use safe path functions in `setup` ([#86](https://github.com/istanbuljs/spawn-wrap/issues/86)) ([4103f72](https://github.com/istanbuljs/spawn-wrap/commit/4103f72)) 25 | 26 | ### Features 27 | 28 | * Drop support for iojs ([#84](https://github.com/istanbuljs/spawn-wrap/issues/84)) ([6e86337](https://github.com/istanbuljs/spawn-wrap/commit/6e86337)) 29 | * require Node 8 ([#80](https://github.com/istanbuljs/spawn-wrap/issues/80)) ([19543e7](https://github.com/istanbuljs/spawn-wrap/commit/19543e7)) 30 | 31 | 32 | ## [2.0.0-beta.0](https://github.com/istanbuljs/spawn-wrap/compare/v1.4.3...v2.0.0-beta.0) (2019-10-07) 33 | 34 | 35 | See 2.0.0 for notes. 36 | 37 | 38 | ### [1.4.3](https://github.com/isaacs/spawn-wrap/compare/v1.4.2...v1.4.3) (2019-08-23) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **win32:** handle cases where "node" is quoted ([#102](https://github.com/isaacs/spawn-wrap/issues/102)) ([aac8730](https://github.com/isaacs/spawn-wrap/commit/aac8730)) 44 | -------------------------------------------------------------------------------- /lib/munge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {isCmd, isNode, isNpm, isSh, getExeBasename} = require("./exe-type") 4 | const mungeCmd = require("./mungers/cmd") 5 | const mungeEnv = require("./mungers/env") 6 | const mungeNode = require("./mungers/node") 7 | const mungeNpm = require("./mungers/npm") 8 | const mungeSh = require("./mungers/sh") 9 | const mungeShebang = require("./mungers/shebang") 10 | 11 | /** 12 | * @typedef {object} InternalSpawnOptions Options for the internal spawn functions 13 | * `childProcess.ChildProcess.prototype.spawn` and `process.binding('spawn_sync').spawn`. 14 | * These are the options mapped by the `munge` function to intercept spawned processes and 15 | * handle the wrapping logic. 16 | * 17 | * @property {string} file File to execute: either an absolute system-dependent path or a 18 | * command name. 19 | * @property {string[]} args Command line arguments passed to the spawn process, including argv0. 20 | * @property {string | undefined} cwd Optional path to the current working directory passed to the 21 | * spawned process. Default: `process.cwd()` 22 | * @property {boolean} windowsHide Boolean controlling if the process should be spawned as 23 | * hidden (no GUI) on Windows. 24 | * @property {boolean} windowsVerbatimArguments Boolean controlling if Node should preprocess 25 | * the CLI arguments on Windows. 26 | * @property {boolean} detached Boolean controlling if the child process should keep its parent 27 | * alive or not. 28 | * @property {string[]} envPairs Array of serialized environment variable key/value pairs. The 29 | * variables serialized as `key + "=" + value`. 30 | * @property {import("child_process").StdioOptions} stdio Stdio options, with the same semantics 31 | * as the `stdio` parameter from the public API. 32 | * @property {number | undefined} uid User id for the spawn process, same as the `uid` parameter 33 | * from the public API. 34 | * @property {number | undefined} gid Group id for the spawn process, same as the `gid` parameter 35 | * from the public API. 36 | * 37 | * @property {string | undefined} originalNode Custom property only used by `spawn-wrap`. It is 38 | * used to remember the original Node executable that was intended to be spawned by the user. 39 | */ 40 | 41 | /** 42 | * Returns updated internal spawn options to redirect the process through the shim and wrapper. 43 | * 44 | * This works on the options passed to `childProcess.ChildProcess.prototype.spawn` and 45 | * `process.binding('spawn_sync').spawn`. 46 | * 47 | * This function works by trying to identify the spawn process and map the options accordingly. 48 | * `spawn-wrap` recognizes most shells, Windows `cmd.exe`, Node and npm invocations; when spawn 49 | * either directly or through a script with a shebang line. 50 | * It also unconditionally updates the environment variables so bare `node` commands execute 51 | * the shim script instead of Node's binary. 52 | * 53 | * @param workingDir {string} Absolute system-dependent path to the directory containing the shim files. 54 | * @param options {InternalSpawnOptions} Original internal spawn options. 55 | * @return {InternalSpawnOptions} Updated internal spawn options. 56 | */ 57 | function munge(workingDir, options) { 58 | const basename = getExeBasename(options.file); 59 | 60 | // XXX: dry this 61 | if (isSh(basename)) { 62 | options = mungeSh(workingDir, options) 63 | } else if (isCmd(basename)) { 64 | options = mungeCmd(workingDir, options) 65 | } else if (isNode(basename)) { 66 | options = mungeNode(workingDir, options) 67 | } else if (isNpm(basename)) { 68 | // XXX unnecessary? on non-windows, npm is just another shebang 69 | options = mungeNpm(workingDir, options) 70 | } else { 71 | options = mungeShebang(workingDir, options) 72 | } 73 | 74 | // now the options are munged into shape. 75 | // whether we changed something or not, we still update the PATH 76 | // so that if a script somewhere calls `node foo`, it gets our 77 | // wrapper instead. 78 | 79 | options = mungeEnv(workingDir, options) 80 | 81 | return options 82 | } 83 | 84 | module.exports = munge 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spawn-wrap 2 | 3 | Wrap all spawned Node.js child processes by adding environs and 4 | arguments ahead of the main JavaScript file argument. 5 | 6 | Any child processes launched by that child process will also be 7 | wrapped in a similar fashion. 8 | 9 | This is a bit of a brutal hack, designed primarily to support code 10 | coverage reporting in cases where tests or the system under test are 11 | loaded via child processes rather than via `require()`. 12 | 13 | It can also be handy if you want to run your own mock executable 14 | instead of some other thing when child procs call into it. 15 | 16 | [![Build Status](https://travis-ci.org/istanbuljs/spawn-wrap.svg)](https://travis-ci.org/istanbuljs/spawn-wrap) 17 | 18 | ## USAGE 19 | 20 | ```javascript 21 | var wrap = require('spawn-wrap') 22 | 23 | // wrap(wrapperArgs, environs) 24 | var unwrap = wrap(['/path/to/my/main.js', 'foo=bar'], { FOO: 1 }) 25 | 26 | // later to undo the wrapping, you can call the returned function 27 | unwrap() 28 | ``` 29 | 30 | In this example, the `/path/to/my/main.js` file will be used as the 31 | "main" module, whenever any Node or io.js child process is started, 32 | whether via a call to `spawn` or `exec`, whether node is invoked 33 | directly as the command or as the result of a shebang `#!` lookup. 34 | 35 | In `/path/to/my/main.js`, you can do whatever instrumentation or 36 | environment manipulation you like. When you're done, and ready to run 37 | the "real" main.js file (ie, the one that was spawned in the first 38 | place), you can do this: 39 | 40 | ```javascript 41 | // /path/to/my/main.js 42 | // process.argv[1] === 'foo=bar' 43 | // and process.env.FOO === '1' 44 | 45 | // my wrapping manipulations 46 | setupInstrumentationOrCoverageOrWhatever() 47 | process.on('exit', function (code) { 48 | storeCoverageInfoSynchronously() 49 | }) 50 | 51 | // now run the instrumented and covered or whatever codes 52 | require('spawn-wrap').runMain() 53 | ``` 54 | 55 | ## ENVIRONMENT VARIABLES 56 | 57 | Spawn-wrap responds to two environment variables, both of which are 58 | preserved through child processes. 59 | 60 | `SPAWN_WRAP_DEBUG=1` in the environment will make this module dump a 61 | lot of information to stderr. 62 | 63 | `SPAWN_WRAP_SHIM_ROOT` can be set to a path on the filesystem where 64 | the shim files are written in a `.node-spawn-wrap-` folder. By 65 | default this is done in `$HOME`, but in some environments you may wish 66 | to point it at some other root. (For example, if `$HOME` is mounted 67 | as read-only in a virtual machine or container.) 68 | 69 | ## CONTRACTS and CAVEATS 70 | 71 | The initial wrap call uses synchronous I/O. Probably you should not 72 | be using this script in any production environments anyway. 73 | 74 | Also, this will slow down child process execution by a lot, since 75 | we're adding a few layers of indirection. 76 | 77 | The contract which this library aims to uphold is: 78 | 79 | * Wrapped processes behave identical to their unwrapped counterparts 80 | for all intents and purposes. That means that the wrapper script 81 | propagates all signals and exit codes. 82 | * If you send a signal to the wrapper, the child gets the signal. 83 | * If the child exits with a numeric status code, then the wrapper 84 | exits with that code. 85 | * If the child dies with a signal, then the wrapper dies with the 86 | same signal. 87 | * If you execute any Node child process, in any of the various ways 88 | that such a thing can be done, it will be wrapped. 89 | * Children of wrapped processes are also wrapped. 90 | 91 | (Much of this made possible by 92 | [foreground-child](http://npm.im/foreground-child).) 93 | 94 | There are a few ways situations in which this contract cannot be 95 | adhered to, despite best efforts: 96 | 97 | 1. In order to handle cases where `node` is invoked in a shell script, 98 | the `PATH` environment variable is modified such that the the shim 99 | will be run before the "real" node. However, since Windows does 100 | not allow executing shebang scripts like regular programs, a 101 | `node.cmd` file is required. 102 | 2. Signal propagation through `dash` doesn't always work. So, if you 103 | use `child_process.exec()` on systems where `/bin/sh` is actually 104 | `dash`, then the process may exit with a status code > 128 rather 105 | than indicating that it received a signal. 106 | 3. `cmd.exe` is even stranger with how it propagates and interprets 107 | unix signals. If you want your programs to be portable, then 108 | probably you wanna not rely on signals too much. 109 | 4. It *is* possible to escape the wrapping, if you spawn a bash 110 | script, and that script modifies the `PATH`, and then calls a 111 | specific `node` binary explicitly. 112 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = wrap 4 | wrap.runMain = runMain 5 | 6 | const Module = require('module') 7 | const fs = require('fs') 8 | const cp = require('child_process') 9 | const ChildProcess = cp.ChildProcess 10 | const assert = require('assert') 11 | const crypto = require('crypto') 12 | const IS_WINDOWS = require('is-windows')() 13 | const makeDir = require('make-dir') 14 | const rimraf = require('rimraf') 15 | const path = require('path') 16 | const signalExit = require('signal-exit') 17 | const {IS_DEBUG, debug} = require("./lib/debug") 18 | const munge = require("./lib/munge") 19 | const homedir = require("./lib/homedir") 20 | 21 | const shebang = process.platform === 'os390' ? 22 | '#!/bin/env ' : '#!' 23 | 24 | const shim = shebang + process.execPath + '\n' + 25 | fs.readFileSync(path.join(__dirname, 'shim.js')) 26 | 27 | function wrap(argv, env, workingDir) { 28 | const spawnSyncBinding = process.binding('spawn_sync') 29 | 30 | // if we're passed in the working dir, then it means that setup 31 | // was already done, so no need. 32 | const doSetup = !workingDir 33 | if (doSetup) { 34 | workingDir = setup(argv, env) 35 | } 36 | const spawn = ChildProcess.prototype.spawn 37 | const spawnSync = spawnSyncBinding.spawn 38 | 39 | function unwrap() { 40 | if (doSetup && !IS_DEBUG) { 41 | rimraf.sync(workingDir) 42 | } 43 | ChildProcess.prototype.spawn = spawn 44 | spawnSyncBinding.spawn = spawnSync 45 | } 46 | 47 | spawnSyncBinding.spawn = wrappedSpawnFunction(spawnSync, workingDir) 48 | ChildProcess.prototype.spawn = wrappedSpawnFunction(spawn, workingDir) 49 | 50 | return unwrap 51 | } 52 | 53 | function wrappedSpawnFunction (fn, workingDir) { 54 | return wrappedSpawn 55 | 56 | function wrappedSpawn (options) { 57 | const mungedOptions = munge(workingDir, options) 58 | debug('WRAPPED', mungedOptions) 59 | return fn.call(this, mungedOptions) 60 | } 61 | } 62 | 63 | function setup(argv, env) { 64 | if (argv && typeof argv === 'object' && !env && !Array.isArray(argv)) { 65 | env = argv 66 | argv = [] 67 | } 68 | 69 | if (!argv && !env) { 70 | throw new Error('at least one of "argv" and "env" required') 71 | } 72 | 73 | if (argv) { 74 | assert(Array.isArray(argv), 'argv must be an array') 75 | } else { 76 | argv = [] 77 | } 78 | 79 | if (env) { 80 | assert(typeof env === 'object', 'env must be an object') 81 | } else { 82 | env = {} 83 | } 84 | 85 | debug('setup argv=%j env=%j', argv, env) 86 | 87 | // For stuff like --use_strict or --harmony, we need to inject 88 | // the argument *before* the wrap-main. 89 | const execArgv = [] 90 | for (let i = 0; i < argv.length; i++) { 91 | if (argv[i].startsWith('-')) { 92 | execArgv.push(argv[i]) 93 | if (argv[i] === '-r' || argv[i] === '--require') { 94 | execArgv.push(argv[++i]) 95 | } 96 | } else { 97 | break 98 | } 99 | } 100 | if (execArgv.length) { 101 | if (execArgv.length === argv.length) { 102 | argv.length = 0 103 | } else { 104 | argv = argv.slice(execArgv.length) 105 | } 106 | } 107 | 108 | const key = process.pid + '-' + crypto.randomBytes(6).toString('hex') 109 | let workingDir = path.resolve(homedir, `.node-spawn-wrap-${key}`) 110 | 111 | const settings = JSON.stringify({ 112 | module: __filename, 113 | deps: { 114 | foregroundChild: require.resolve('foreground-child'), 115 | signalExit: require.resolve('signal-exit'), 116 | debug: require.resolve('./lib/debug') 117 | }, 118 | isWindows: IS_WINDOWS, 119 | key, 120 | workingDir, 121 | argv, 122 | execArgv, 123 | env, 124 | root: process.pid 125 | }, null, 2) + '\n' 126 | 127 | if (!IS_DEBUG) { 128 | signalExit(() => rimraf.sync(workingDir)) 129 | } 130 | 131 | makeDir.sync(workingDir) 132 | workingDir = fs.realpathSync(workingDir) 133 | if (IS_WINDOWS) { 134 | const cmdShim = 135 | '@echo off\r\n' + 136 | 'SETLOCAL\r\n' + 137 | 'CALL :find_dp0\r\n' + 138 | 'SET PATHEXT=%PATHEXT:;.JS;=;%\r\n' + 139 | '"' + process.execPath + '" "%dp0%node" %*\r\n' + 140 | 'EXIT /b %errorlevel%\r\n'+ 141 | ':find_dp0\r\n' + 142 | 'SET dp0=%~dp0\r\n' + 143 | 'EXIT /b\r\n' 144 | 145 | fs.writeFileSync(path.join(workingDir, 'node.cmd'), cmdShim) 146 | fs.chmodSync(path.join(workingDir, 'node.cmd'), '0755') 147 | } 148 | fs.writeFileSync(path.join(workingDir, 'node'), shim) 149 | fs.chmodSync(path.join(workingDir, 'node'), '0755') 150 | const cmdname = path.basename(process.execPath).replace(/\.exe$/i, '') 151 | if (cmdname !== 'node') { 152 | fs.writeFileSync(path.join(workingDir, cmdname), shim) 153 | fs.chmodSync(path.join(workingDir, cmdname), '0755') 154 | } 155 | fs.writeFileSync(path.join(workingDir, 'settings.json'), settings) 156 | 157 | return workingDir 158 | } 159 | 160 | function runMain () { 161 | process.argv.splice(1, 1) 162 | process.argv[1] = path.resolve(process.argv[1]) 163 | delete require.cache[process.argv[1]] 164 | Module.runMain() 165 | } 166 | -------------------------------------------------------------------------------- /shim.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This module should *only* be loaded as a main script 4 | // by child processes wrapped by spawn-wrap. It sets up 5 | // argv to include the injected argv (including the user's 6 | // wrapper script) and any environment variables specified. 7 | // 8 | // If any argv were passed in (ie, if it's used to force 9 | // a wrapper script, and not just ensure that an env is kept 10 | // around through all the child procs), then we also set up 11 | // a require('spawn-wrap').runMain() function that will strip 12 | // off the injected arguments and run the main file. 13 | 14 | // wrap in iife for babylon to handle module-level return 15 | ;(function () { 16 | 17 | if (module !== require.main) { 18 | throw new Error('spawn-wrap: cli wrapper invoked as non-main script') 19 | } 20 | 21 | const Module = require('module') 22 | const path = require('path') 23 | const settings = require('./settings.json') 24 | const {debug} = require(settings.deps.debug) 25 | 26 | debug('shim', [process.argv[0]].concat(process.execArgv, process.argv.slice(1))) 27 | 28 | const foregroundChild = require(settings.deps.foregroundChild) 29 | const IS_WINDOWS = settings.isWindows 30 | const argv = settings.argv 31 | const nargs = argv.length 32 | const env = settings.env 33 | const key = settings.key 34 | const node = process.env['SW_ORIG_' + key] || process.execPath 35 | 36 | Object.assign(process.env, env) 37 | 38 | const needExecArgv = settings.execArgv || [] 39 | 40 | // If the user added their OWN wrapper pre-load script, then 41 | // this will pop that off of the argv, and load the "real" main 42 | function runMain () { 43 | debug('runMain pre', process.argv) 44 | process.argv.splice(1, nargs) 45 | process.argv[1] = path.resolve(process.argv[1]) 46 | delete require.cache[process.argv[1]] 47 | debug('runMain post', process.argv) 48 | Module.runMain() 49 | debug('runMain after') 50 | } 51 | 52 | // Argv coming in looks like: 53 | // bin shim execArgv main argv 54 | // 55 | // Turn it into: 56 | // bin settings.execArgv execArgv settings.argv main argv 57 | // 58 | // If we don't have a main script, then just run with the necessary 59 | // execArgv 60 | let hasMain = false 61 | for (let a = 2; !hasMain && a < process.argv.length; a++) { 62 | switch (process.argv[a]) { 63 | case '-i': 64 | case '--interactive': 65 | case '--eval': 66 | case '-e': 67 | case '-p': 68 | case '-pe': 69 | hasMain = false 70 | a = process.argv.length 71 | continue 72 | 73 | case '-r': 74 | case '--require': 75 | a += 1 76 | continue 77 | 78 | default: 79 | // TODO: Double-check what's going on 80 | if (process.argv[a].startsWith('-')) { 81 | continue 82 | } else { 83 | hasMain = a 84 | a = process.argv.length 85 | break 86 | } 87 | } 88 | } 89 | debug('after argv parse hasMain=%j', hasMain) 90 | 91 | if (hasMain > 2) { 92 | // if the main file is above #2, then it means that there 93 | // was a --exec_arg *before* it. This means that we need 94 | // to slice everything from 2 to hasMain, and pass that 95 | // directly to node. This also splices out the user-supplied 96 | // execArgv from the argv. 97 | const addExecArgv = process.argv.splice(2, hasMain - 2) 98 | needExecArgv.push(...addExecArgv) 99 | } 100 | 101 | if (!hasMain) { 102 | // we got loaded by mistake for a `node -pe script` or something. 103 | const args = process.execArgv.concat(needExecArgv, process.argv.slice(2)) 104 | debug('no main file!', args) 105 | foregroundChild(node, args) 106 | return 107 | } 108 | 109 | // If there are execArgv, and they're not the same as how this module 110 | // was executed, then we need to inject those. This is for stuff like 111 | // --harmony or --use_strict that needs to be *before* the main 112 | // module. 113 | if (needExecArgv.length) { 114 | const pexec = process.execArgv 115 | if (JSON.stringify(pexec) !== JSON.stringify(needExecArgv)) { 116 | debug('need execArgv for this', pexec, '=>', needExecArgv) 117 | const sargs = pexec.concat(needExecArgv).concat(process.argv.slice(1)) 118 | foregroundChild(node, sargs) 119 | return 120 | } 121 | } 122 | 123 | // At this point, we've verified that we got the correct execArgv, 124 | // and that we have a main file, and that the main file is sitting at 125 | // argv[2]. Splice this shim off the list so it looks like the main. 126 | process.argv.splice(1, 1, ...argv) 127 | 128 | // Unwrap the PATH environment var so that we're not mucking 129 | // with the environment. It'll get re-added if they spawn anything 130 | if (IS_WINDOWS) { 131 | for (const i in process.env) { 132 | if (/^path$/i.test(i)) { 133 | process.env[i] = process.env[i].replace(__dirname + ';', '') 134 | } 135 | } 136 | } else { 137 | process.env.PATH = process.env.PATH.replace(__dirname + ':', '') 138 | } 139 | 140 | const spawnWrap = require(settings.module) 141 | if (nargs) { 142 | spawnWrap.runMain = function (original) { 143 | return function () { 144 | spawnWrap.runMain = original 145 | runMain() 146 | } 147 | }(spawnWrap.runMain) 148 | } 149 | spawnWrap(argv, env, __dirname) 150 | 151 | debug('shim runMain', process.argv) 152 | delete require.cache[process.argv[1]] 153 | Module.runMain() 154 | 155 | // end iife wrapper for babylon 156 | })() 157 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var sw = require('../') 2 | var IS_WINDOWS = require('is-windows')() 3 | var winNoShebang = IS_WINDOWS && 'no shebang execution on windows' 4 | var winNoSig = IS_WINDOWS && 'no signals get through cmd' 5 | 6 | var onExit = require('signal-exit') 7 | var cp = require('child_process') 8 | var fixture = require.resolve('./fixtures/script.js') 9 | var npmFixture = require.resolve('./fixtures/npm') 10 | var fs = require('fs') 11 | var path = require('path') 12 | 13 | if (process.argv[2] === 'parent') { 14 | // hang up once 15 | process.once('SIGHUP', function onHup () { 16 | console.log('SIGHUP') 17 | }) 18 | // handle sigints forever 19 | process.on('SIGINT', function onInt () { 20 | console.log('SIGINT') 21 | }) 22 | onExit(function (code, signal) { 23 | console.log('EXIT %j', [code, signal]) 24 | }) 25 | var argv = process.argv.slice(3).map(function (arg) { 26 | if (arg === fixture) { 27 | return '{{FIXTURE}}' 28 | } 29 | return arg 30 | }) 31 | console.log('WRAP %j', process.execArgv.concat(argv)) 32 | sw.runMain() 33 | return 34 | } 35 | 36 | var t = require('tap') 37 | var unwrap = sw([__filename, 'parent']) 38 | 39 | var expect = 'WRAP ["{{FIXTURE}}","xyz"]\n' + 40 | '[]\n' + 41 | '["xyz"]\n' + 42 | 'EXIT [0,null]\n' 43 | 44 | // dummy for node v0.10 45 | if (!cp.spawnSync) { 46 | cp.spawnSync = function () { 47 | return { 48 | status: 0, 49 | signal: null, 50 | stdout: expect 51 | } 52 | } 53 | } 54 | 55 | t.test('spawn execPath', function (t) { 56 | t.plan(4) 57 | 58 | t.test('basic', function (t) { 59 | var child = cp.spawn(process.execPath, [fixture, 'xyz']) 60 | 61 | var out = '' 62 | child.stdout.on('data', function (c) { 63 | out += c 64 | }) 65 | child.on('close', function (code, signal) { 66 | t.equal(code, 0) 67 | t.equal(signal, null) 68 | t.equal(out, expect) 69 | t.end() 70 | }) 71 | }) 72 | 73 | t.test('basic sync', function (t) { 74 | var child = cp.spawnSync(process.execPath, [fixture, 'xyz']) 75 | 76 | t.equal(child.status, 0) 77 | t.equal(child.signal, null) 78 | t.equal(child.stdout.toString(), expect) 79 | t.end() 80 | }) 81 | 82 | t.test('SIGINT', { skip: winNoSig }, function (t) { 83 | var child = cp.spawn(process.execPath, [fixture, 'xyz']) 84 | 85 | var out = '' 86 | child.stdout.on('data', function (c) { 87 | out += c 88 | }) 89 | child.stdout.once('data', function () { 90 | child.kill('SIGINT') 91 | }) 92 | child.stderr.pipe(process.stderr) 93 | child.on('close', function (code, signal) { 94 | t.equal(code, 0) 95 | t.equal(signal, null) 96 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 97 | '[]\n' + 98 | '["xyz"]\n' + 99 | 'SIGINT\n' + 100 | 'EXIT [0,null]\n') 101 | t.end() 102 | }) 103 | }) 104 | 105 | t.test('SIGHUP', { skip: winNoSig }, function (t) { 106 | var child = cp.spawn(process.execPath, [fixture, 'xyz']) 107 | 108 | var out = '' 109 | child.stdout.on('data', function (c) { 110 | out += c 111 | child.kill('SIGHUP') 112 | }) 113 | child.on('close', function (code, signal) { 114 | t.equal(signal, 'SIGHUP') 115 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 116 | '[]\n' + 117 | '["xyz"]\n' + 118 | 'SIGHUP\n' + 119 | 'EXIT [null,"SIGHUP"]\n') 120 | t.end() 121 | }) 122 | }) 123 | }) 124 | 125 | t.test('spawn node', function (t) { 126 | t.plan(4) 127 | 128 | t.test('basic', function (t) { 129 | var child = cp.spawn('node', [fixture, 'xyz']) 130 | 131 | var out = '' 132 | child.stdout.on('data', function (c) { 133 | out += c 134 | }) 135 | child.on('close', function (code, signal) { 136 | t.equal(code, 0) 137 | t.equal(signal, null) 138 | t.equal(out, expect) 139 | t.end() 140 | }) 141 | }) 142 | 143 | t.test('basic sync', function (t) { 144 | var child = cp.spawnSync('node', [fixture, 'xyz']) 145 | 146 | t.equal(child.status, 0) 147 | t.equal(child.signal, null) 148 | t.equal(child.stdout.toString(), expect) 149 | t.end() 150 | }) 151 | 152 | t.test('SIGINT', { skip: winNoSig }, function (t) { 153 | var child = cp.spawn('node', [fixture, 'xyz']) 154 | 155 | var out = '' 156 | child.stdout.on('data', function (c) { 157 | out += c 158 | }) 159 | child.stdout.once('data', function () { 160 | child.kill('SIGINT') 161 | }) 162 | child.stderr.pipe(process.stderr) 163 | child.on('close', function (code, signal) { 164 | t.equal(code, 0) 165 | t.equal(signal, null) 166 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 167 | '[]\n' + 168 | '["xyz"]\n' + 169 | 'SIGINT\n' + 170 | 'EXIT [0,null]\n') 171 | t.end() 172 | }) 173 | }) 174 | 175 | t.test('SIGHUP', { skip: winNoSig }, function (t) { 176 | var child = cp.spawn('node', [fixture, 'xyz']) 177 | 178 | var out = '' 179 | child.stdout.on('data', function (c) { 180 | out += c 181 | child.kill('SIGHUP') 182 | }) 183 | child.on('close', function (code, signal) { 184 | t.equal(signal, 'SIGHUP') 185 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 186 | '[]\n' + 187 | '["xyz"]\n' + 188 | 'SIGHUP\n' + 189 | 'EXIT [null,"SIGHUP"]\n') 190 | t.end() 191 | }) 192 | }) 193 | }) 194 | 195 | t.test('exec execPath', function (t) { 196 | t.plan(4) 197 | 198 | t.test('basic', function (t) { 199 | var opt = IS_WINDOWS ? null : { shell: '/bin/bash' } 200 | var child = cp.exec('"' + process.execPath + '" "' + fixture + '" xyz', opt) 201 | 202 | var out = '' 203 | child.stdout.on('data', function (c) { 204 | out += c 205 | }) 206 | child.on('close', function (code, signal) { 207 | t.equal(code, 0) 208 | t.equal(signal, null) 209 | t.equal(out, expect) 210 | t.end() 211 | }) 212 | }) 213 | 214 | t.test('execPath wrapped with quotes', function (t) { 215 | var opt = IS_WINDOWS ? null : { shell: '/bin/bash' } 216 | var child = cp.exec(JSON.stringify(process.execPath) + ' ' + fixture + 217 | ' xyz', opt) 218 | 219 | var out = '' 220 | child.stdout.on('data', function (c) { 221 | out += c 222 | }) 223 | child.on('close', function (code, signal) { 224 | t.equal(code, 0) 225 | t.equal(signal, null) 226 | t.equal(out, expect) 227 | t.end() 228 | }) 229 | }) 230 | 231 | t.test('SIGINT', { skip: winNoSig }, function (t) { 232 | var child = cp.exec(process.execPath + ' ' + fixture + ' xyz', { shell: '/bin/bash' }) 233 | 234 | var out = '' 235 | child.stdout.on('data', function (c) { 236 | out += c 237 | }) 238 | child.stdout.once('data', function () { 239 | child.kill('SIGINT') 240 | }) 241 | child.stderr.pipe(process.stderr) 242 | child.on('close', function (code, signal) { 243 | t.equal(code, 0) 244 | t.equal(signal, null) 245 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 246 | '[]\n' + 247 | '["xyz"]\n' + 248 | 'SIGINT\n' + 249 | 'EXIT [0,null]\n') 250 | t.end() 251 | }) 252 | }) 253 | 254 | t.test('SIGHUP', { skip: winNoSig }, function (t) { 255 | var child = cp.exec(process.execPath + ' ' + fixture + ' xyz', { shell: '/bin/bash' }) 256 | 257 | var out = '' 258 | child.stdout.on('data', function (c) { 259 | out += c 260 | child.kill('SIGHUP') 261 | }) 262 | child.on('close', function (code, signal) { 263 | t.equal(signal, 'SIGHUP') 264 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 265 | '[]\n' + 266 | '["xyz"]\n' + 267 | 'SIGHUP\n' + 268 | 'EXIT [null,"SIGHUP"]\n') 269 | t.end() 270 | }) 271 | }) 272 | }) 273 | 274 | t.test('exec shebang', { skip: winNoShebang }, function (t) { 275 | t.plan(3) 276 | 277 | t.test('basic', function (t) { 278 | var child = cp.exec(fixture + ' xyz', { shell: '/bin/bash' }) 279 | 280 | var out = '' 281 | child.stdout.on('data', function (c) { 282 | out += c 283 | }) 284 | child.on('close', function (code, signal) { 285 | t.equal(code, 0) 286 | t.equal(signal, null) 287 | t.equal(out, expect) 288 | t.end() 289 | }) 290 | }) 291 | 292 | t.test('SIGHUP', function (t) { 293 | var child = cp.exec(fixture + ' xyz', { shell: '/bin/bash' }) 294 | 295 | var out = '' 296 | child.stdout.on('data', function (c) { 297 | out += c 298 | child.kill('SIGHUP') 299 | }) 300 | child.on('close', function (code, signal) { 301 | t.equal(signal, 'SIGHUP') 302 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 303 | '[]\n' + 304 | '["xyz"]\n' + 305 | 'SIGHUP\n' + 306 | 'EXIT [null,"SIGHUP"]\n') 307 | t.end() 308 | }) 309 | }) 310 | 311 | t.test('SIGINT', function (t) { 312 | var child = cp.exec(fixture + ' xyz', { shell: '/bin/bash' }) 313 | 314 | var out = '' 315 | child.stdout.on('data', function (c) { 316 | out += c 317 | }) 318 | child.stdout.once('data', function () { 319 | child.kill('SIGINT') 320 | }) 321 | child.stderr.pipe(process.stderr) 322 | child.on('close', function (code, signal) { 323 | t.equal(code, 0) 324 | t.equal(signal, null) 325 | t.equal(out, 'WRAP ["{{FIXTURE}}","xyz"]\n' + 326 | '[]\n' + 327 | '["xyz"]\n' + 328 | 'SIGINT\n' + 329 | 'EXIT [0,null]\n') 330 | t.end() 331 | }) 332 | }) 333 | }) 334 | 335 | // see: https://github.com/bcoe/nyc/issues/190 336 | t.test('Node 5.8.x + npm 3.7.x - spawn', { skip: winNoShebang }, function (t) { 337 | var npmdir = path.dirname(npmFixture) 338 | process.env.PATH = npmdir + ':' + (process.env.PATH || '') 339 | var child = cp.spawn('npm', ['xyz']) 340 | 341 | var out = '' 342 | child.stdout.on('data', function (c) { 343 | out += c 344 | }) 345 | child.on('close', function (code, signal) { 346 | t.equal(code, 0) 347 | t.equal(signal, null) 348 | t.true(~out.indexOf('xyz')) 349 | t.end() 350 | }) 351 | }) 352 | 353 | t.test('Node 5.8.x + npm 3.7.x - shell', { skip: winNoShebang }, function (t) { 354 | var npmdir = path.dirname(npmFixture) 355 | process.env.PATH = npmdir + ':' + (process.env.PATH || '') 356 | var child = cp.exec('npm xyz') 357 | 358 | var out = '' 359 | child.stdout.on('data', function (c) { 360 | out += c 361 | }) 362 | var err = '' 363 | child.stderr.on('data', function (c) { 364 | err += c 365 | }) 366 | child.on('close', function (code, signal) { 367 | t.equal(code, 0) 368 | t.equal(signal, null) 369 | t.true(~out.indexOf('xyz')) 370 | t.end() 371 | }) 372 | }) 373 | 374 | t.test('--harmony', function (t) { 375 | var node = process.execPath 376 | var child = cp.spawn(node, ['--harmony', fixture, 'xyz']) 377 | var out = '' 378 | child.stdout.on('data', function (c) { 379 | out += c 380 | }) 381 | child.on('close', function (code, signal) { 382 | t.equal(code, 0) 383 | t.equal(signal, null) 384 | t.equal(out, 'WRAP ["--harmony","{{FIXTURE}}","xyz"]\n' + 385 | '["--harmony"]\n' + 386 | '["xyz"]\n' + 387 | 'EXIT [0,null]\n') 388 | t.end() 389 | }) 390 | }) 391 | 392 | t.test('node exe with different name', function(t) { 393 | var fp = path.join(__dirname, 'fixtures', 'exething.exe') 394 | var data = fs.readFileSync(process.execPath) 395 | fs.writeFileSync(fp, data) 396 | fs.chmodSync(fp, '0775') 397 | var child = cp.spawn(process.execPath, [fixture, 'xyz']) 398 | 399 | var out = '' 400 | child.stdout.on('data', function (c) { 401 | out += c 402 | }) 403 | child.on('close', function (code, signal) { 404 | t.equal(code, 0) 405 | t.equal(signal, null) 406 | t.equal(out, expect) 407 | fs.unlinkSync(fp) 408 | t.end() 409 | }) 410 | }) 411 | 412 | t.test('unwrap', function (t) { 413 | unwrap() 414 | t.end() 415 | }) 416 | --------------------------------------------------------------------------------