vorner / signal-hook

Rust library allowing to register multiple handlers for the same signal

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Calling `deregister` with mio should remove signal hooks

chrisduerr opened this issue · comments

So I've been playing with the mio crate for signal_hook and have the desire to register a signal, listen for it, then unregister it again at a later point. It seems like currently the recommended behavior for doing that is to call signal_hook::cleanup::cleanup_signal(SIGNAL).

I personally find this a bit confusing, given that mio has a built-in deregister function. Wouldn't it make sense for the mio backend to register signal hooks on register and reregister and to reset them to the default on deregister?

I'm also a bit confused about the structure of the API, since calling signal_hook::unregister registers an empty action instead of resetting it to the OS defaults. Wouldn't it make more sense to have this usecase covered by register instead?

It might also make things a bit cleaner to have methods take references to structs, instead of passing structs as parameters, so SigId::unregister instead of unregister(id: SigId), since that would probably make the "flow" of the API a bit more clear? The way it is right now, I just see an unregister method and wonder how I get a SigId so I can remove a signal, which doesn't make much sense.

Hello

I'm not entirely sure about where your confusion comes from, but I'll try a guess.

So, signal hook has these low-level register and unregister calls. These are really low level and allow manipulating the hooks directly and when you register, you get the SigId for future deregistration.

Then there's mio with its Evented trait, that has the register/reregister/deregister. There's a bit of clash of terminology around that in signal-hook-mio, but both kind of developed independently of each other and eventually „met“ each other in the signal-hook-mio. These are, however, fully independent of each other, the register of signal hook has nothing to do at all with register from mio.

How this crate is intended to be used:

  • You create the Signals, with bunch of signals to watch, or add them later on. The struct internally holds the relevant SigIds, you don't have to touch them.
  • Then, this thing can be plugged into mio with poll.register. That in turn uses the Evented::register and similar. These, however, are just the integration into watching for readability. Same as if you create a mio::TcpStream, you don't connect or disconnect by registering or deregistering it for polling.
  • Once you get the readability event, you „read“ the available signals by calling signals.pending on it.
  • If you no longer want the signals, you simply drop the Signals structure. It unregisters signals on drop, using the internally-held SigIds.

As for actually removing signals. It's not exactly true that unregister „registers“ an empty action. Signal hook keeps possibly multiple hooks for the same signal. It can happen that if you remove all of them, then nothing is left to be called in the signal handler. But it is not reset to OS default for two reasons:

  • The general habit of many libraries that touch signal handling is to call sigaction and chain-call the previously installed action. Signal-hook does the same and it could be possible to return that one. But there's no possible way to know if someone actually does this to us. That could, quite accidentally, remove some other, completely unrelated, signal action from the chain.
  • By adding and removing hooks, getting „accidentally“ to empty state and having it to reset to some other action would be inconsistent behaviour.

Therefore, calling a cleanup_signal is an explicit action left up to the application developer, because they are the only ones knowing this can safely be done.

Now, unless I've guessed wrong about where your confusion comes from, I'd like to close the issue. While I agree that both the situation with terminology clash and the situation about removing signals are not exactly ideal, I don't think there's a better alternative (I hope I've explained why what you propose doesn't seem to be one to me). If this still doesn't answer your concerns, can you share some code and be a bit more specific about them?

Thank you

These are, however, fully independent of each other, the register of signal hook has nothing to do at all with register from mio.

And I think this exactly is the problem.

If you no longer want the signals, you simply drop the Signals structure. It unregisters signals on drop, using the internally-held SigIds.

Sure, but I am not able to remove any signals. All I can do is recreate the signal handler entirely if I want to change them, which requires reregistering the mio source and does not cause any signal handlers to be called in the future. How is one supposed to mutate the signals with the mio crate?

And I think this exactly is the problem.

As in, you now see the misunderstanding, or in, you'd want to tie them together? As for the latter, it would seem really weird to me (and I'm not sure if it's even possible). As I look at it, the TcpStream doesn't disconnect on deregister either. It still can receive and send data, you just don't get notified about it. It probably should be the same ‒ if you deregister, you'd still receive the signals and queue them, but not get notified of them.

Sure, but I am not able to remove any signals.

If I understand you correctly, you're saying that the Handle has add_signal, but not a remove_signal? That's true. That's across all the signal-hook crates and it's mostly because there are some challenges and nobody felt it was useful enough to go and solve them (unregistering signals seems to be much less common need than registering them). As the API goes, I'm ok if you want to add a remove_signal, but I'd want you to consider all kinds of possible race conditions when doing so in the PR and be sure it doesn't break anything (it might turn out it actually works in some simple way out of the box, but I would at least want the author to say why it happens to work and why no problem can happen).

What you can do now is create multiple Signals structure, each one for its own signal. Then you can just drop the whole relevant Signals structure. You can even have multiple for the same signal, for different parts of the application, for example. Having multiple signals inside the same structure is mostly relevant to the blocking iterator inside signal-hook, mio can be used to select between independent ones.

All I can do is recreate the signal handler entirely if I want to change them, which requires reregistering the mio source and does not cause any signal handlers to be called in the future.

I don't understand what you mean by „does not cause any signal handlers to be called“. Can you demonstrate what you mean?

As in, you now see the misunderstanding, or in, you'd want to tie them together? As for the latter, it would seem really weird to me (and I'm not sure if it's even possible). As I look at it, the TcpStream doesn't disconnect on deregister either. It still can receive and send data, you just don't get notified about it. It probably should be the same ‒ if you deregister, you'd still receive the signals and queue them, but not get notified of them.

As in the lack of integration seems to cause problems that I do not see a solution for with the current API.

I'd want you to consider all kinds of possible race conditions when doing so in the PR and be sure it doesn't break anything

That just sounds like being setup for failure. Removing a signal is a perfectly normal usecase for signal handling, I don't see why this is dismissed as something so outlandish.

I don't understand what you mean by „does not cause any signal handlers to be called“. Can you demonstrate what you mean?

Sure, take this code (compressed):

// Setup mio
let mut signals = Signals::new(&[SIGTSTP])?;
poll.registry().register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?;

// start mio loop...

// Attempt to remove the sigtstp signal and self-trigger it again.
fn handle_sigtstp() {
    signal_hook::cleanup_signal(libc::SIGTSTP).expect("unexpected signal state");
    unsafe {
        libc::kill(process::id() as i32, SIGTSTP);
    }
}

// Register custom sigtstp handler again.
fn handle_sigcont() {
    signals.add_signal(libc::SIGTSTP).unwrap();
}

That's basically the gist of what I want to do. I've tried several different things like dropping all the signals and re-creating them and of course I've also attempted to reregister to mio after the events have changed. But none of this produced working code, the SIGTSTP handler is never called again once it is removed.

This has lead to me reading the documentation for cleanup_signal:

Once called, registering new hooks for this signal has no further effect (they'll appear to be registered, but they won't be called by the signal).

So I've made the assumption that this kind of stuff doesn't seem to work? Which raised the question how does one make it work with mio. I've sifted through the API with all its different register/unregister methods and beyond the trivial setup signals and start mio, I'm not sure how this is supposed to be interacted with.

How does one remove a signal and add it again? signals.reregister() doesn't do it and signals.remove/add doesn't seem to do it either. That's why I'm confused about the interaction with mio. It seems nebulous to me how one is supposed to interact with the mio signal handling, rather than just setting it up and hoping it will be good enough for the rest of the application lifetime.

As in the lack of integration seems to cause problems that I do not see a solution for with the current API.

I believe it's not the lack of integration, but lack of parts of the API. See below.

That just sounds like being setup for failure. Removing a signal is a perfectly normal usecase for signal handling, I don't see why this is dismissed as something so outlandish.

You seem to misunderstand what I'm trying to say (maybe I'm not saying it the best possible way).

  • Signal handling is hard, so any such change is hard and needs certain amount of care. Usually more care than people suspect before submitting a PR. So I'm trying to warn in the sense „I'll be happy to have a PR implementing it, but expect it to take a while and expect some pointy questions in the form of "is that actually safe, will that always work?"“. If the add_signal didn't exist and someone wanted to add it, I would also want all kinds of race conditions considered. I want the code that is already there to work, not almost-always-work.
  • More people need adding signals than removing signals. I maintain the library in my free time, so I prefer covering my own needs first. Other people also seem to solve primarily their use cases. I do not dismiss the use case, I agree with it, but nobody needed it yet, or wasn't motivated enough to write it.

All in all, you can either submit a PR (with the warning that it might be some amount of work), or might wait until I manage to squeeze it into my schedule, but that might take some time.

Now, to your example (I'm finally starting to see what you're trying to do):

The cleanup_signal, as it is implemented now, is just the bare minimal implementation to support the use case „I press CTRL+C once to get a graceful shutdown, but the signal handler is completely removed at that point, so when it gets stuck and I CTRL+C again, it just terminates right away.“ People do it at shutdown, so it being irreversible is not a problem. However, the idea is (and always was) to eventually make it reversible, so that if you cleanup_signal, and register a new one again, it would plug the signal handler back in. This just haven't happened yet, because again, nobody was motivated enough to pick it up. That's actually what #30 is about.

As mentioned above, the cleanup_signal can't happen automatically and one would still need to do that by hand to get the default signal handler.

However, if you simply wanted to pause receiving/handling that signal for a while, without returning to the default, you could just do something like:

// Setup mio
let mut signals = None;
handle_sigcont()?;


// start mio loop...

// Attempt to remove the sigtstp signal and self-trigger it again.
fn handle_sigtstp() {
    signals = None;
    // Whatever else
}

// Register custom sigtstp handler again.
fn handle_sigcont() -> Result<(), Error> {
    signals = Some(Signals::new(&[SIGTSTP])?);
    poll.registry().register(signals.as_ref_mut().unwrap(), SIGNAL_TOKEN, Interest::READABLE)?;
}

If you do need returning to the default handler in between, this is what you'd need to do:

  • Solve #30 first, as it is the actually missing part. That's not about mio or integration, it's simply part that's haven't been finished.
  • You'd still need to tear down and recreate (or add the remove_signal call to Handle) the Signals, because that structure would otherwise „believe“ the library is still initialized to receive the signal and would not re-register itself (mostly, cleanup_signal removes all the hooks without letting owners of the hooks know).

However, if you simply wanted to pause receiving/handling that signal for a while, without returning to the default, you could just do something like:

The entire goal is to go back to the default, so that the application is actually suspended. So just not reacting to it doesn't help.

Solve #30 first, as it is the actually missing part. That's not about mio or integration, it's simply part that's haven't been finished.

Sure, by solving #30 it would probably make this library usable for me. Though I still find its API questionable.

Do you want to close this issue and open a separate, clean one for allowing the removal of signals from mio's Signals structure? Since this issue has been a bit of a tangent about all kinds of stuff.

Though I still find its API questionable.

The whole signal handling API is a nightmare, to be honest, and some of it leaks. I think the fact that resetting to default doesn't happen on its own is caused by the fact that you, as an application developer may make the assumption you're the only one around touching signals. But inside a library I can't make that assumption, I have to let the application developer make that claim through the API.

Do you want to close this issue and open a separate, clean one for allowing the removal of signals from mio's Signals structure? Since this issue has been a bit of a tangent about all kinds of stuff.

I think I'll do that. The #30 is the must-have for your use case, I'll add another one for the remove_signal, that is in a sense a nice-to-have (the code would be nicer with it, but can be made work with just #30).

Do you want to give one (or both) of them a try, or shall I try to find some time for them eventually?

Once again, thanks for the example, I wasn't getting what the actual problem was until then.

But inside a library I can't make that assumption, I have to let the application developer make that claim through the API.

It's not only about the signal handling part though. It's also about a lack of clear structure and transparency within the library.

Things like having the register and unregister methods available in the root, while taking and creating a SigId, which makes it difficult to see from the docs how the API is supposed to be used.

It also seems like SigId is not an accurate handle either, at least I can't see a Drop impl that would call unregister and it's Copy. So I'd assume that unregister must be called manually. That's okay, but something that isn't immediately obvious.

Then there's the 4 different functions to remove a signal. You have unregister, which takes a SigId, cleanup_raw, which removes the OS signal handler but doesn't remove the hooks, cleanup_signal which removes both and unregister_signal which just removes the hooks without touching the signal handling. The amount of time it takes to read through all of these just to figure out you can't actually unregister a signal cleanly is nothing short of infuriating.

Of course it's fine to have things unimplemented and work in progress, nothing just immediately exists and is perfect. But I personally found that the features missing from signal_hook were amplified by an API that did not make it transparent if the thing I want to do is unsupported, or if I'm just doing it in the wrong way. When there's about a dozen different combinations to try, that just makes troubleshooting very difficult.

Do you want to give one (or both) of them a try, or shall I try to find some time for them eventually?

I can look into #30, but I'm not sure how far I'll get on that since I didn't plan to go for a detour. If it's non-trivial I'm probably going to look for an alternative solution to my problem.

It's not only about the signal handling part though. It's also about a lack of clear structure and transparency within the library.

I think I see what you mean. The library did grow over time a bit, and it is result of some experimentation. Some restructuring, or at least better documentation might actually help. That's probably a topic for yet another issue.

I think I would have a workaround for your problem, though:

What you're trying to do (if I understand correctly) is not unregister a signal. What you're trying to do is invoke the default signal handler, right? Now, the POSIX API doesn't really have a good answer to that, but the workaround seems to be something in form (pseudocode):

current_signal = signal(SIG_DFL);
raise();
signal(current_signal);

That has bunch of problems, including things like „What if I get the signal from someone else in between“ (this is not atomic operation and it affects other threads and so on), but you could still do it while keeping signal-hook around (as it, „steal“ the signal handler from it, not tell it about it at all, and return its signal handler back once you're done). Signal hook can still do the multiplexing (multiple hooks for the same signal; which you maybe don't need) and wrapping it in an API that integrates into mio.

Now, the POSIX API doesn't really have a good answer to that, but the workaround seems to be something in form (pseudocode):

Yes, that code looks like it should work by sidestepping signal-hook completely. That would be a bit unfortunate though. But in my particular application that wouldn't cause any problems at all.

That would be a bit unfortunate though.

That's the reason why I cal it a workaround, not a solution. I would want to support your use case, but I suspect that will take some time to get to, this is meant for the meantime so you're not blocked on it.