Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use spawn instead of execFile #27

Merged
merged 8 commits into from
Apr 28, 2016

Conversation

jamestalmage
Copy link
Contributor

@jamestalmage jamestalmage commented Apr 26, 2016

childProcess.execFile pays no attention opts.stdio. This means users can't set stdio options when calling exca(). You can set stdio options when calling execa.spawn(), but that does not return a promise.

Rather than having two separate methods execa() and execa.spawn() - I think we should work towards a single method that allows you to configure how it behaves via stdio options. opts.stdio or opts.stdin, etc; Those should determine whether or not the stream is buffered and made part of the promise result, or if they are just streamed (or sent to an Observable), or ignored.

To that end, I've reimplemented much of the functionality of child_process.execFile. execFile is really just a wrapper around spawn anyways. It's main utility is that in concats stdio and stdout. Which is pretty easy for us to achieve on our own using get-stream.

This simplifies the API as we can pretty much deprecate execa.spawn(). And it offers more control over the behavior as well. Want to use streams/observables for stdout because you know it will contain lots of data and you don't want to waste memory buffering? Would it be nice if it still buffered stderr for you though? This makes that doable.

Remaining items to be feature complete with execFile:

  • timeout support (should be easy to add back).
  • errname resolution. See TODO note on that. I'm not sure we want to use process.bind('uv'). I don't really know what it does.
  • some stream cleanup code (see childProces.execFile's internal kill method. It calls stream.destroy(). I am not sure how that will affect get-stream)
  • add a bunch of tests around the logic copied out of child_process. The new code has nearly 100% coverage, but that's deceptive. Most of these aren't terribly consequential (Error properties: message, killed, code, etc).

Going forward, we'll still need to bikeshed on a few API details, like instead of just ignore, pipe and inherit, we should add buffer and observable.

childProcess.execFile pays no attention `opts.stdio`.

`execa.spawn` allows you to set `opts.stdio` options, but it doesn't return a promise.

I think we should forgoe using `childProcess.execFile` and just use spawn. We can reimplement much of `execFile`s utility ourself.

Rather than having two separate methods `spawn` and `execFile` - I think we should work towards a single method. What options the user sets for `opts.stdio` or `opts.stdin`, etc; Those should determine whether or not the stream is buffered and made part of the promise result, or if they are just streamed (or sent to an Observable), or ignored. This simplifies the API, and offers potentially far more control. Want to use streams/observables for `stdout` because you know it will contain lots of data and you don't want to buffer, but it would nice to just have `stderr` buffered for you to print in case of an error? That would be doable this way, and really separates it from child_process.

childProcess.execFile is really just a wrapper around spawn anyways. It's main utility is that in concats `stdio` and `stdout`. Which is pretty easy for us to achieve on our own using `get-stream`

This reimplements nearly all of the functionality of execFile.

Remaining items to be feature complete with execFile:
- [ ] timeout support (should be easy to add back).
- [ ] errname resolution. See TODO note on that. I'm not sure we want to use `process.bind('uv')`. I don't really know what it does.
- [ ] some stream cleanup code (see childProces.execFile's internal `kill` method. It calls `stream.destroy()`. I am not sure how that will affect `get-stream`)
@sindresorhus
Copy link
Owner

sindresorhus commented Apr 26, 2016

Very cool! I like this a lot. Will give a more thorough review and response later today.

This will also make it easier to achieve #5.

@jamestalmage
Copy link
Contributor Author

Can't figure out why Windows is failing. If someone else could give troubleshooting it a go, it would be appreciated

@jamestalmage
Copy link
Contributor Author

Using the technique described here, I'm getting the following output:

This Branch(failing):

Z:\WebstormProjects\execa>node node_modules/ava/cli.js
spawn called
[ 'C:\\Windows\\system32\\cmd.exe',
  [ '/s',
    '/c',
    '"node "Z:\\WebstormProjects\\execa\\fixtures\\error-message.js""' ],
  { maxBuffer: 10485760,
    stripEof: true,
    preferLocal: true,
    encoding: 'utf8',
    windowsVerbatimArguments: true,
    env: { Path: 'Z:\\WebstormProjects\\execa\\node_modules\\.bin;Z:\\WebstormPr
ojects\\node_modules\\.bin;Z:\\node_modules\\.bin;C:\\Program Files\\nodejs;Z:\\
WebstormProjects\\execa\\fixtures;C:\\Program Files\\Parallels\\Parallels Tools\
\Applications;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\
Windows\\System32\\WindowsPowerShell\\v1.0\\;c:\\Program Files\\Microsoft SQL Se
rver\\100\\Tools\\Binn\\;c:\\Program Files\\Microsoft SQL Server\\100\\DTS\\Binn
\\;C:\\Program Files\\Microsoft SQL Server\\110\\Tools\\Binn\\;C:\\Users\\Admini
strator\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs;C:\\Program Files\\Git\
\cmd;C:\\Program Files\\OpenVPN\\bin;C:\\Users\\Administrator\\AppData\\Roaming\
\npm;C:\\Users\\Administrator\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs'
} } ]

   1 failed

  1. include stdout in errors for improved debugging

  t.regex(err.message, /stdout/)
              |
              "spawn undefined ENOENT"

      _callee6$ (test.js:52:4)

Z:\WebstormProjects\execa>

Master (passing)

Z:\WebstormProjects\execa>node node_modules/ava/cli.js
execFile called
[ 'C:\\Windows\\system32\\cmd.exe',
  [ '/s',
    '/c',
    '"node "Z:\\WebstormProjects\\execa\\fixtures\\error-message.js""' ],
  { maxBuffer: 10485760,
    stripEof: true,
    preferLocal: true,
    encoding: 'utf8',
    windowsVerbatimArguments: true,
    env: { Path: 'Z:\\WebstormProjects\\execa\\node_modules\\.bin;Z:\\WebstormPr
ojects\\node_modules\\.bin;Z:\\node_modules\\.bin;C:\\Program Files\\nodejs;Z:\\
WebstormProjects\\execa\\fixtures;C:\\Program Files\\Parallels\\Parallels Tools\
\Applications;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\
Windows\\System32\\WindowsPowerShell\\v1.0\\;c:\\Program Files\\Microsoft SQL Se
rver\\100\\Tools\\Binn\\;c:\\Program Files\\Microsoft SQL Server\\100\\DTS\\Binn
\\;C:\\Program Files\\Microsoft SQL Server\\110\\Tools\\Binn\\;C:\\Users\\Admini
strator\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs;C:\\Program Files\\Git\
\cmd;C:\\Program Files\\OpenVPN\\bin;C:\\Users\\Administrator\\AppData\\Roaming\
\npm;C:\\Users\\Administrator\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs'
} } ]

   1 passed

Z:\WebstormProjects\execa>

@jamestalmage
Copy link
Contributor Author

jamestalmage commented Apr 26, 2016

I just... I can't... I don't get it. 49065ca

Why does this make the tests pass?
Could you possibly make the error output any more cryptic?

I don't understand you Windows! We are not friends anymore.
@jamestalmage
Copy link
Contributor Author

Previous commit did not trigger travis. Force pushed another.

@jamestalmage
Copy link
Contributor Author

@sindresorhus - How do you want to proceed with this?

Do you want me to keep pushing commits until it's feature complete with execFile? (the only thing of real consequence is timeout support - which I don't think is widely used).

My only concern is that it's already a pretty major refactor, and I would like to do it in smaller chunks if possible. We can convert my check list from the OP to issues, and just work to fix them before the next release.

@sindresorhus
Copy link
Owner

sindresorhus commented Apr 27, 2016

I just... I can't... I don't get it. 49065ca

Super-weird... I tried to debug this on my Windows VM, but still no idea why it happen. Fwiw it doesn't happen with childProcess.spawn().


return spawned;
};
module.exports.spawn = util.deprecate(module.exports, 'execa.spawn: just use execa instead.');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execa.spawn() is deprecated. Use execa() instead.

@sindresorhus
Copy link
Owner

This looks really good. Let's merge now and open issues for what's remaining. I too prefer work done in smaller chunks. And you've already covered to important parts. Can you update the readme, removing spawn and any other changes?

@kevva
Copy link
Contributor

kevva commented Apr 27, 2016

The tests pass on Windows for me (even with process.exit(1)). I'm not running Windows on a VM though but as a real OS.

@sindresorhus
Copy link
Owner

@kevva What Windows & Node.js version?

@sindresorhus sindresorhus changed the title [WIP]: just use spawn instead of execFile Use spawn instead of execFile Apr 27, 2016
@kevva
Copy link
Contributor

kevva commented Apr 27, 2016

@sindresorhus, never mind. Ran it in bash. Trying to run it in cmd gave me spawn undefined ENOENT too.

@jamestalmage
Copy link
Contributor Author

Should we open an issue on the Node repo about the weird process.exit behavior? I haven't tried creating a minimal reproduction yet. That would be a first step.

@sindresorhus
Copy link
Owner

Definitely, if you can recreate it. Sounds like a horrible bug if valid.

@kevva
Copy link
Contributor

kevva commented Apr 27, 2016

I did create a simple script exiting with error code 1 and got ENOENT too. Same result using the core childProcess.spawn. Feels like a bug.

@jamestalmage
Copy link
Contributor Author

@kevva - Can you share the script. I'm not able to reproduce with a simple spawn.

@jamestalmage jamestalmage merged commit 31a8d87 into sindresorhus:master Apr 28, 2016
@jamestalmage jamestalmage deleted the just-use-spawn branch April 28, 2016 22:04
@jamestalmage jamestalmage mentioned this pull request Apr 28, 2016
@sindresorhus
Copy link
Owner

🙌 🎉

@kevva
Copy link
Contributor

kevva commented Apr 29, 2016

👏

@derimagia
Copy link

Make sure when you add links to lines on github you press y to get the current commit -

execa/index.js

Line 160 in 56a20b1

// https://github.com/nodejs/node/blob/master/lib/child_process.js#L203
is outdated at the moment.

@sindresorhus sindresorhus mentioned this pull request Jun 29, 2016
jamestalmage added a commit to jamestalmage/execa that referenced this pull request Jul 15, 2016
Fixes sindresorhus#34

- Adds some tests for functionality copied from `child_process` in sindresorhus#27.
- Normalises the result and error objects further. Each now has `killed`, `signal` and `cmd` properties.
- NYC was not ignoring the `fixtures` directory. Fixed that, and configured nyc in package.json instead of via CLI.
@jamestalmage jamestalmage mentioned this pull request Jul 15, 2016
@sindresorhus
Copy link
Owner

@jamestalmage One thing we didn't consider here is that if the user does execa('echo', ['unicorns']).stdout.pipe(process.stdout);, to use it like they used child_process.spawn, it will still buffer the result, waste memory, and eventually hit the max buffer. We should probably have a buffer: false option or something, right?

@jamestalmage
Copy link
Contributor Author

Yeah. Or we could automatically discard the buffer if they pipe.

@sindresorhus
Copy link
Owner

Was thinking about that too, but can we detect that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants