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
Workaround so only one CTRL-C is required for a new prompt in --gui=qt #3219
Conversation
Only implemented for posix systems. On Windows SIGINT kills python, and signal.CTRL_C_EVENT doesn't do anything.
why the sleep? Can it work with no delay? |
Also, move any stdlib imports to the top of the file. |
It is unreliable without sleep because sometimes the signal comes through before it can exit the ctypes callback. Though I've tested it with sleeps as short as .01 sec and it worked fine. |
I am -1 on this. Adding threads into the mix is just asking for problems...in general with try to avoid threads at all possible cost and I I don't view this semi-mis-feature as warranting it. |
Using threads does worry me a little bit, since I don't really understand it, but for this Qt stuff, I'm inclined to let people with proposed fixes make them, since this is not an area of the code that has any relevance to me. |
I've switched to python Timers though it doesn't really address your concern since Timer is a subclass of Thread. Qt timers won't work in this case because the eventloop is disabled after the first Ctrl-C. |
args=[pid, signal.SIGINT] | ||
) | ||
|
||
timer.start() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would happen if you did join here - I've never understood how this particular bit works, where does the code need to be for this to be properly handled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way ipython seem to integrate with qt eventloop is by registering a callback that gets executed periodically when Python is waiting for input. The callback runs the qt eventloop for 50 ms so all the mouse/windowing stuff gets handled. The problem is you can't raise exceptions in this callback, so CTRL-C won't work at all.
The original author got around this by catching the SIGINT, and the disabling the callback (which means the next CTRL-C will be caught by python proper and not the callback).
I tried to get around that by making it send a ctrl-c to itself after the first ctrl-c, but this SIGINT has to arrive after it exits the inputhook callback, hence the need for threads/timers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming that the join I mentioned above is not appropriate, let's keep track of the timer instance, and cancel (Timer.cancel()
) the previous one before starting the next, just so we can be sure we don't have more than one waiting, however unlikely that may be with 10ms timeout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks for the explanation - I figured it was something like that, I just wanted to understand.
OK, I've been going over this with a fine-tooth comb for a few hours, trying alternative modes that will simulate deadlocks, introduce delays in the os.kill call by wrapping it in functions that make blocking calls, etc. I went as far as doing something this nasty: def stopme():
import numpy as np
import sys
print('blocking work first...')
sys.stdout.flush()
n = 2000
a = np.random.random((n,n))
np.linalg.svd(a)
print('all done! ^C now...')
sys.stdout.flush()
os.kill(pid, signal.SIGINT)
if(not sigint_timer[0]):
sigint_timer[0] = threading.Timer(.01, stopme) and it still behaves as expected. The worst that happens, if the user frantically hits Furthermore, I tested this by forcing my system to work under a load over 12, which is the kind of scenario that often makes weird problems in threading appear, as context switching gets sluggish (and my machine was indeed pretty miserable). I actually think this is one of those cases where using threads is a reasonable thing to do: threads aren't evil per se, they are just very delicate instruments that need to be used very judiciously. But the logic here is extremely simple, and the failure modes can't corrupt state, produce non-deterministic output that will later cause problems, or deadlock. So I don't see any problem with it. Because this logic is a little delicate, though, I'd like to see some comments in there. See inline review. Once those small cosmetic issues are addressed, I'm happy to merge it, unless anyone else can point out an actual failure mode that would make this a problem. |
mgr.clear_inputhook() | ||
if(os.name == 'posix'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a short descriptive comment before this if
that indicates what the intent is and why it's done with os.kill
and a timer. It will help anyone who in the future has to revisit this code.
@fperez I found if I mash CTRL-C and Enter, I can get the git master to generate a traceback as well (which I'm assuming is the same traceback you're seeing). It appears to be because the |
Oh, certainly: mad mashing of the keyboard will produce a visible traceback, but it doesn't (as far as I have seen) crash ipython itself. It's always true that if you hit C-C fast enough, you can also crash IPython itself. Simply try running:
in one shell where 10291 was the PID of another IPython process. It pretty quickly died :) That's just the reality, a mad sequence of interrupts can eventually leak out of the nested But I can't find any pattern where this PR makes it worse or more brittle than it already is, and the fact that these pathological abuses can be done, doesn't worry me. So, do you see any remaining issues? |
@@ -66,6 +71,8 @@ def create_inputhook_qt4(mgr, app=None): | |||
|
|||
got_kbdint = [False] | |||
|
|||
sigint_timer = [None] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason that sigint_timer
needs to be a list? Everywhere it looks like you always access the first element. Why not just set it to None here and use it as a scalar elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
He's following the pattern we already had with got_kbdinit
: because they are grabbed from the surrounding scope, and the nonlocal
keyword is only python-3, this is the only way to do it.
I considered asking a full re-work of this code into a class to better manage the state, but I'm more worried about doing a big refactor for a small change, that can potentially introduce new bugs inadvertently (especially with code as delicate as this).
Once we switch to python3 only, we can just use nonlocal
and get rid of this hack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we could also turn those things into module globals, since code like this is never going to be reused by more than one thing at the same time... A module global would be a bit less ugly than the list trick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I'm always worried of turning a simple PR into a big refactoring job...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was thinking that a module global is the best approach. Otherwise, there is a hack to get around a very non-obvious aspect of the language.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, now that there's two, it might be better to make them module globals. @siyuz, could you move both got_kbdint
and sigint_timer
to being scalar module globals? Just assign them to None
in a well-marked block at the top, and use the keyword global
inside the function.
That's a good balance between asking for a full class refactoring and at least removing this ugly hack.
@siyuz, once that's done I'll gladly merge this. Thanks for your patience with the review process!
I think this is ready to go, @fperez any other things that need to be done? |
Yup. The Merging now. |
Workaround so only one CTRL-C is required for a new prompt in --gui=qt.
Workaround so only one CTRL-C is required for a new prompt in --gui=qt.
I have a habit of using CTRL-C to clear lines in a terminal (and MATLAB), so it is really annoying to have to press it twice in ipython.
This is a kludgy, posix only workaround, and I only tested it for Python 2.7 on Linux and Windows.