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

[cargo run] Should proxy signals to child process #2343

Closed
passcod opened this issue Jan 31, 2016 · 21 comments
Closed

[cargo run] Should proxy signals to child process #2343

passcod opened this issue Jan 31, 2016 · 21 comments

Comments

@passcod
Copy link

passcod commented Jan 31, 2016

From watchexec/cargo-watch#25, around this comment.
Also referring this short IRC conversation.

Background: Cargo Watch has improved behaviour when watching a Cargo project and running cargo run: on changes, it kills the child cargo run process and starts a new one. This is highly helpful when developing web servers and other applications which never return. For prior art, see nodemon.

Cargo Watch currently uses the stdlib's Child#kill() method on Windows, and libc::kill() on all other platforms (Unix), with a signal of 15 (SIGTERM).

Problem: cargo run creates a new child process. Killing the cargo run child does not kill the grand-child. Cargo Watch then proceeds to start a new cargo-run process, which attempts to start a new grand-child. We now have two of the target process, which is obviously unexpected and wrong. In the case of webservers, the first target process is still bound to its TCP port, preventing latter target processes from functioning, producing EADDRINUSE errors.

Proposal: To have cargo run proxy signals to its own child process, allowing Cargo Watch (and other applications opening cargo child commands) to send a SIGTERM (on Unix) or taskkill (on Windows, through the stdlib's implementation) directly to the target process. The target would then be responsible for its own shutdown.

Cargo Watch would not attempt to send further signals, instead waiting for the cargo run process to return normally.

cargo-run may want to SIGKILL its child if it does not seem to be responding to SIGTERM, but I will leave that up to Cargo maintainers.

I am also not familiar with the behaviours on Windows, so cannot comment on that aspect.

@LukasKalbertodt
Copy link
Member

/cc me

@kamalmarhubi
Copy link
Contributor

For Windows you'll probably want to use Job Objects, and you can see an example here for how to use them. The basic idea is that a bunch of processes are added to the job object and then when the object itself is closed it'll kill every process associated with it.

―@alexchriton in watchexec/cargo-watch#25 (comment)

@passcod
Copy link
Author

passcod commented Feb 1, 2016

I was wondering how programs hook into their shutdown on Windows. On Unix, the polite thing to do is to SIGTERM (or SIGINT) processes, because SIGTERM is trappable so you can do stuff like closing sockets, releasing ports, and saying goodbye instead of just ceasing to exist. Does Windows have similar capabilities?

@alexcrichton
Copy link
Member

Thanks for the report @passcod! Here's some thoughts of mine:

  • Dealing with signals on unix is a huge pain and incredibly difficult to get right. If at all possible I'd prefer to never touch them!
  • Along those lines, when doing a cargo run, we might arguably prefer to use exec rather than proxying signals. That'll mean that everything is "naturally proxied" and you get some other benefits like pid savings, memory savings, etc.
  • Cargo spawns lots of children (e.g. rustdoc, rustc, build scripts, test harnesses, etc), and proxying signals to all of them may be a bit too cumbersome. For a tool like cargo-watch I think the best way to go on Unix is definitely using setsid to create a new process group plus killing the entire process group.
  • On Windows, as far as I know, there are no equivalents of signals. The default TerminateProcess function is akin to SIGKILL. You can somewhat handle ctrl-c with SetConsoleCtrlHandler, but those seem more difficult to programmatically generated and seem simply rarer in general.
  • For windows, however, job objects are pretty handy for this sort of operation of managing process groups. The JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag means that entire process groups can be killed all at once.

So overall, my recommendation would be something along the lines of:

  • Cargo should use exec where possible (e.g. with cargo run) on Unix platforms to basically mimic "signal proxying"
  • Cargo should perhaps use job objects on Windows to ensure that the immediately spawned child is killed when Cargo itself is killed. We may even want the entire process tree to be killed when Cargo is killed, but that's a little more sketchy to me.
  • cargo-watch should kill the entire process group when spawning Cargo on Unix.
  • cargo-watch should probably put the entire cargo invocation in its own job object to be killed all at once on Windows (recursive children as well)

Does that all make sense?

@alexcrichton
Copy link
Member

One point about job objects on Windows which makes me a little uneasy is:

Windows 7, Windows Server 2008 R2, Windows XP with SP3, Windows Server 2008, Windows Vista, and Windows Server 2003: A process can be associated with only one job. Jobs cannot be nested. The ability to nest jobs was added in Windows 8 and Windows Server 2012.

Which to me means two things:

  1. Cargo would need to be quite defensive about using job objects on Windows, e.g. resilient to failures. Before Windows 8 it may be relatively routine for Cargo itself to already be part of a job object.
  2. This may be a breaking change for Cargo to start using job objects. Builds on pre-Windows 8 machines may already be using job objects for subprocesses and may not be resilient to failure, so they may not expect to already be part of a job object.

@passcod
Copy link
Author

passcod commented Feb 1, 2016

Is there an equivalent of exec on Windows for cargo to use on run commands? That would considerably simplify that aspect.

@passcod
Copy link
Author

passcod commented Feb 1, 2016

At least with respect to cargo-watch.

@alexcrichton
Copy link
Member

If there is I'm at least unaware of such a piece of functionality which is standard. Which is to say that I don't believe it exists.

@passcod
Copy link
Author

passcod commented Feb 1, 2016

For Unix, cargo watch will definitely use process groups, even though it might just become irrelevant if cargo goes to use exec. At least for backward compatibility with older cargo.

For Windows, I'll have to investigate/think a bit more. I probably do not want to forgo support for Windows < 8 in cargo-watch.

@sfackler
Copy link
Member

sfackler commented Feb 5, 2016

I've seen very weird behavior on OSX, where ctrl-cing a cargo run will sometimes kill the running process and sometimes leave it alive reparented to init. It's not totally clear to me what the conditions are that cause the behavior to change.

@alexcrichton
Copy link
Member

Cargo doesn't itself manage child processes at all, so any weird behavior is likely just a relic of whatever the shell is doing. If a child process manually goes into a new group then when the shell kills the entire process group the child keeps going (as it's part of a different group).

alexcrichton added a commit to alexcrichton/cargo that referenced this issue Feb 11, 2016
Currently it's somewhat surprising if you're using cargo and it's then ctrl-c'd.
The child processes that Cargo spawned are likely to still be running around in
the background as they're not killed as well, and this could cause output spew
or future build failures.

This situation is handled by default on Unix because ctrl-c will end up sending
a signal to the entire *process group*, which kills everything, but on Windows
we're not as lucky (just Cargo itself is killed). By using job objects on
Windows we can ensure that the entire tree dies instead of just the top Cargo
process.

cc rust-lang#2343
bors added a commit that referenced this issue Feb 11, 2016
Currently it's somewhat surprising if you're using cargo and it's then ctrl-c'd.
The child processes that Cargo spawned are likely to still be running around in
the background as they're not killed as well, and this could cause output spew
or future build failures.

This situation is handled by default on Unix because ctrl-c will end up sending
a signal to the entire *process group*, which kills everything, but on Windows
we're not as lucky (just Cargo itself is killed). By using job objects on
Windows we can ensure that the entire tree dies instead of just the top Cargo
process.

cc #2343
@alexcrichton
Copy link
Member

Cargo now uses job objects on Windows, so I think the only change still needed here is to use exec on Unix once it's stable.

@robinst
Copy link
Contributor

robinst commented Jun 30, 2016

CommandExt::exec is now stable in Rust 1.9, right?

@alexcrichton
Copy link
Member

It is indeed! This can probably only be used for cargo run, but it's the "most correct" function to call in that situation.

@robinst
Copy link
Contributor

robinst commented Jul 1, 2016

I'm looking at Cargo's process_builder.rs, its ProcessBuilder has an exec method. I changed it on Unix to use CommandExt::exec and it seems to work nicely for cargo run.

But as you said, other usages should not do a exec. Would you rename the other usages (maybe spawn), or introduce a new method that does CommandExt::exec with a new name (maybe exec_replace)?

@robinst
Copy link
Contributor

robinst commented Jul 1, 2016

I've opted for the second for now, as there's more code that uses "exec" (e.g. ExecEngine). See PR #2818.

@alexcrichton
Copy link
Member

Ah yeah adding a new exec_replace method is fine, thanks @robinst!

robinst added a commit to robinst/cargo that referenced this issue Oct 20, 2016
Before, we would spawn a child process for the program. One of the
problems with that is when killing the cargo process, the program
continues running.

With this change, the cargo process is replaced by the program, and
killing it works.
bors added a commit that referenced this issue Oct 20, 2016
Use CommandExt::exec for `cargo run` on Unix (#2343)

Before, we would spawn a child process for the program. One of the
problems with that is when killing the cargo process, the program
continues running.

With this change, the cargo process is replaced by the program, and
killing it works.

Before (`cargo run` is the parent):

> 502  7615  7614   0  2:26PM ttys012    0:00.12 /Users/rstocker/.multirust/toolchains/stable-x86_64-apple-darwin/bin/cargo run
> 502  7620  7615   0  2:26PM ttys012    0:00.01 target/debug/program

After (the shell is the parent):

> 502 81649 81648   0  5:27PM ttys012    0:00.69 -zsh
> 502  7739 81649   0  2:26PM ttys012    0:01.27 target/debug/program
Yamakaky added a commit to Yamakaky/dcpu that referenced this issue Oct 22, 2016
@Yamakaky
Copy link

Is this issue solved by the previous commits?

Yamakaky added a commit to Yamakaky/dcpu that referenced this issue Oct 22, 2016
@alexcrichton
Copy link
Member

Indeed!

@Yamakaky
Copy link

I imagine it will not be available until a few weeks?

@alexcrichton
Copy link
Member

This is likely already in the nightly Cargo builds, but yeah to get to stable it'll take awhile.

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

No branches or pull requests

7 participants