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

Stop reading from stdin after programmatic API finishes #253

Merged
merged 12 commits into from
Aug 8, 2021
Merged

Stop reading from stdin after programmatic API finishes #253

merged 12 commits into from
Aug 8, 2021

Conversation

brandonchinn178
Copy link
Contributor

@brandonchinn178 brandonchinn178 commented Dec 25, 2020

Fixes #252 by explicitly calling .pause() on the input stream after all commands have finished. This seems to be the only way to fix the problem, and there doesn't seem to be a way to reset the process.stdin state completely to the state before concurrently ran (see NodeJS docs).

Refactored some parts of the code in order to make this change more drop-in:

  • Added BaseHandler class that all handlers extend from
  • Add onFinish to BaseHandler which runs after all commands have finished
  • Add FakeHandler to tests that extend BaseHandler instead of ad-hoc handler objects in concurrently.spec.js
  • Use an actual Readable in input-handler tests instead of EventEmitter

The CLI has not changed in this PR, but the programmatic API now has two new options:

  • handleInput: an alias for the old inputStream: process.stdin. Just thought this would be nice, since 99% of the time, inputStream is set to process.stdin
  • pauseInputStreamOnFinish: Some people might have a use-case where they want to read from process.stdin further after running concurrently programmatically. In this rare instance, they'd have to explicitly set this option to false, or explicitly call process.stdin.resume() afterwards. I can't imagine this being a common use-case, so I think this explicitness is a good trade-off.

Repro steps

  1. Write start.js as specified in Programmatic usage errors on Ctrl+C when specifying process.stdin #252, except replace require('concurrently') with require('.'). Put this file in the root of the repo.
  2. node start.js + Ctrl-C should no longer hang / wait for Ctrl-D

@brandonchinn178
Copy link
Contributor Author

@kimmobrunfeldt It seems like Node 8 reached EOL on Jan 1. Can we remove Node 8 from the CI pipeline? Apparently Promise.finally() was added in Node 10

@gustavohenke
Copy link
Member

Hey @brandonchinn178 👋
I've maintaining the package more than Kimmo actually -- I've bumped the minimum version to 10 on master.
Can you rebase and try again?

@gustavohenke gustavohenke self-requested a review January 7, 2021 02:19
Copy link
Member

@gustavohenke gustavohenke left a comment

Choose a reason for hiding this comment

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

Hey! I'm sorry that I took this long to look at your PR.
Your analysis is very good, and it may be unveiling the reason for some other issues reported in that the processes hang forever!

onFinish() {
if (this.inputStream && this.pauseInputStreamOnFinish) {
// https://github.com/kimmobrunfeldt/concurrently/issues/252
this.inputStream.pause();
Copy link
Member

Choose a reason for hiding this comment

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

Rather than pausing, WDYT if this called .unref() instead?
We can then get rid of one the pauseInputStreamOnFinish option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unref is only a function on net.Socket, but not readable Stream, so it doesn't work if stdin is a file.

$ concurrently ... < foo
(node:51227) UnhandledPromiseRejectionWarning: TypeError: process.stdin.unref is not a function
...

We could update the docs to say "inputStream is either net.Socket or readable Stream" and then have this clean up check if the unref function exists and calls that if it does, but I think that adds too much complexity. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Ah yes, of course it couldn't be this simple 🤦 OK. Need to think a bit more.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I think we could be smart and pause the input stream if it has no more listeners and it either

  • was paused before; or
  • was not flowing before.

Although, since I haven't been able to give much attention to concurrently, I'd say just introducing the option is probably best.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's been so long since I worked on this PR, honestly, I can't provide any more insights. (it seems like you are similarly distracted as well) If you're suggesting merging the PR as-is, that would be preferable by me. Are there any more blocking concerns you'd like me to address first?

Copy link
Member

Choose a reason for hiding this comment

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

Just the handler interface, as per #253 (comment)

/**
* A hook called when all commands have finished (either successful or not).
*/
onFinish() {}
Copy link
Member

Choose a reason for hiding this comment

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

WDYT if handle returned an optional cleanup function, along with the mapped commands?
Having an onFinish method kinda implies that the instance is single use.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Personally, I like this better:

  • It keeps handle as a pure transformation Commands => Commands
  • It makes the reduce logic a bit more complicated in the main function
  • It might also be a breaking change, if any third-party APIs write their own handlers
    • The only way to not make it a breaking change would be to allow handle to return either Commands or { commands, cleanup }, which is additional complexity for the caller to handle

But it's your call

Copy link
Member

Choose a reason for hiding this comment

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

For this one I think it's better to introduce a breaking change.
As you correctly stated, third parties can pass their own handlers - and it could become a pain point to manage state across multiple runs of the same instance.

Some other uses I can see for this cleanup function:

  • Removing the RxJS subscription (which also removes the event from the inputStream, though doesn't make the process exit if it's stdin)
  • Removing listeners on signals in the KillOnSignal handler (which can also prevent the process from exiting)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that makes sense. I can update the handle api if you want. Are there any docs that I'd need to update as well?

Copy link
Member

Choose a reason for hiding this comment

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

Nope, no docs on creating custom handlers to this date 😞

@brandonchinn178
Copy link
Contributor Author

@gustavohenke any progress on this?

@brandonchinn178
Copy link
Contributor Author

@gustavohenke How's that?

Copy link
Member

@gustavohenke gustavohenke left a comment

Choose a reason for hiding this comment

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

Looks very good to me! Thanks for doing this 🏆

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.

Programmatic usage errors on Ctrl+C when specifying process.stdin
2 participants