This is an archive of the discontinued Mercurial Phabricator instance.

ui: add an uninterruptable context manager that can block SIGINT
ClosedPublic

Authored by durin42 on Jun 12 2018, 11:31 AM.

Details

Summary

The blocking of SIGINT is not done by default, but my hope is that we
will one day. This was inspired by Facebook's "nointerrupt" extension,
which is a bit more heavy-handed than this (whole commands are treated
as unsafe to interrupt). A future patch will enable this for varying
bits of Mercurial that are performing unsafe operations.

It's intentional that the KeyboardInterrupt is raised as the context
manager exits: during the span of the context manager interrupting
Mercurial could lead to data loss, but typically those spans are
fairly narrow, so we can let the unsafe block complete and then
terminate hg (which will leave the repo in a consistent state, even if
it's not the user's desired state).

.. api::

New context manager ``ui.uninterruptable()`` to mark portions of a command
as potentially unsafe places to interrupt Mercurial with Control-C or
similar.

Diff Detail

Repository
rHG Mercurial
Lint
Automatic diff as part of commit; lint not applicable.
Unit
Automatic diff as part of commit; unit tests not applicable.

Event Timeline

durin42 created this revision.Jun 12 2018, 11:31 AM
martinvonz added inline comments.
mercurial/ui.py
347–350

it took me a while to parse these conditions. perhaps it's clearer like this?

enabled = self.configbool('experimental', 'nointerrupt')
if (enabled and
    self.configbool('experimental', 'nointerrupt-interactiveonly')):
    enabled = self.interactive()
348

nit: does "inter" mean "interrupt" or "interactive" here? since both make sense in this context, it might be better to spell it out

357

nit: maybe disable*d*siginthandler()? (i would suggest warningsiginthandler(), but it's too easy to read the "warning" as noun and not as verb that i meant it to be)

durin42 updated this revision to Diff 9033.Jun 12 2018, 5:31 PM

Good suggestions, integrated them. :)

yuja added a subscriber: yuja.Jun 14 2018, 11:35 AM

LGTM by a non-googler.

+ @contextlib.contextmanager
+ def unsafeoperation(self):
+ """Mark an operation as unsafe.

Nit: the word "unsafe" seems too obscure. Maybe it could be "uninterruptible"
or "noninterruptible"?

+ try:
+ self._oldsiginthandler = signal.getsignal(signal.SIGINT)
+ signal.signal(signal.SIGINT, disabledsiginthandler)

signal.signal() may raise ValueError if it's called from sub thread.

  • a/mercurial/configitems.py

+++ b/mercurial/configitems.py
@@ -560,6 +560,17 @@
coreconfigitem('experimental', 'mergedriver',

default=None,

)
+coreconfigitem('experimental', 'nointerrupt', default=False)
+_nointmsg = """
+==========================
+Interrupting Mercurial may leave your repo in a bad state.
+If you really want to interrupt your current command, press
+CTRL-C again.
+==========================
+""".strip()
+coreconfigitem('experimental', 'nointerrupt-message', default=_nointmsg)

Nit: perhaps we'll need to move the default message to ui.py to make it
translatable.

FWIW, I've added a similar function to lock.py at d77c3b023393. My version
would be too strict to block user interrupt casually, but maybe we can
factor out a utility function for both of these use cases?

indygreg requested changes to this revision.Jun 16 2018, 4:07 PM
indygreg added a subscriber: indygreg.

I agree with @yuja that we should move this to util.py or one of its siblings and rename it to uninterruptable or some such.

That being said, signal handlers are process global. And we do need to maintain persistent state so things don't get out of whack if we have overlapping calls, potentially from multiple threads (e.g. in the case of hgweb). So maybe ui.py - or even a global variable in that module - is the proper place for such state.

Also, I wonder if we shouldn't delay signal handling instead of ignoring it. e.g. our new signal handler would print that the signal was received and then raise KeyboardInterrupt at context manager exit time. Otherwise, the handling of SIGINT is timing dependent and doesn't consistently result in an aborted process.

This revision now requires changes to proceed.Jun 16 2018, 4:07 PM
yuja added a comment.Jun 17 2018, 12:53 AM
I agree with @yuja that we should move this to `util.py` or one of its

siblings and rename it to uninterruptable or some such.

procutil.py would be a better place.

That being said, signal handlers are process global. And we do need to
maintain persistent state so things don't get out of whack if we have
overlapping calls, potentially from multiple threads (e.g. in the case
of hgweb). So maybe ui.py - or even a global variable in that module -
is the proper place for such state.

I think ui-level blocking flag is fine. We won't have to care about
overlapped requests from threads because signal.signal() just doesn't
work in sub threads.

What I had in mind was something like the following:

class ui:
    @contextmanager
    def uninterruptable(self):
        if already blocked or not enabled:
            yield
            return
        with procutil.uninterruptable(warn=self.warn, onlysigint=True,
                                      allowdoublesigint=True):
            set blocked
            yield
            clear blocked

I agree with @yuja that we should move this to util.py or one of its siblings and rename it to uninterruptable or some such.
That being said, signal handlers are process global. And we do need to maintain persistent state so things don't get out of whack if we have overlapping calls, potentially from multiple threads (e.g. in the case of hgweb). So maybe ui.py - or even a global variable in that module - is the proper place for such state.
Also, I wonder if we shouldn't delay signal handling instead of ignoring it. e.g. our new signal handler would print that the signal was received and then raise KeyboardInterrupt at context manager exit time. Otherwise, the handling of SIGINT is timing dependent and doesn't consistently result in an aborted process.

I like this idea. I think bazel does something similar, I know blaze does: one ctrl-c says something like "Shutting down...", three will do a rather forceful shutdown (not kill-9 forceful, but it won't wait for it to get to a convenient spot). Something like that would work well here imho.

durin42 edited the summary of this revision. (Show Details)Jun 27 2018, 11:47 AM
durin42 retitled this revision from ui: add an unsafeoperation context manager that can block SIGINT to ui: add an uninterruptable context manager that can block SIGINT.
durin42 updated this revision to Diff 9326.

I'm not in love with uninterruptible as a name (I think unsafeoperation is more explicit) but I don't feel strongly. The rest of the suggestions I liked, so the series is updated and ready for another look.

yuja added a comment.Jun 28 2018, 8:16 AM

+ @contextlib.contextmanager
+ def uninterruptable(self):
+ """Mark an operation as unsafe.
+
+ Most operations on a repository are safe to interrupt, but a
+ few are risky (for example repair.strip). This context manager
+ lets you advise Mercurial that something risky is happening so
+ that control-C etc can be blocked if desired.
+ """
+ enabled = self.configbool('experimental', 'nointerrupt')
+ if (enabled and
+ self.configbool('experimental', 'nointerrupt-interactiveonly')):
+ enabled = self.interactive()
+ if self._uninterruptible or not enabled:
+ # if nointerrupt support is turned off, the process isn't
+ # interactive, or we're already in an uninterruptable
+ # block, do nothing.
+ yield
+ return
+ def warn():
+ self.warn(_("shutting down cleanly\n"))
+ self.warn(
+ _("press ^C again to terminate immediately (dangerous)\n"))

Missed return True ?

Other that that, the patch looks good to me.

durin42 updated this revision to Diff 9410.Jul 3 2018, 12:39 PM
martinvonz added inline comments.Jul 3 2018, 12:48 PM
mercurial/ui.py
369

dropping this line (or maybe it was the one above, I'm not sure ;)) in flight

This revision was automatically updated to reflect the committed changes.

Do we need a different strategy for Windows, or just eat the error? (See the unsupported signal message)
https://buildbot.mercurial-scm.org/builders/Win7%20x86_64%20hg%20tests/builds/808/steps/run-tests.py%20%28python%202.7.13%29/logs/stdio

I'm not sure. We could probably just not support this on windows for now, or at least disable the test on Windows?

Do we need a different strategy for Windows, or just eat the error? (See the unsupported signal message)
https://buildbot.mercurial-scm.org/builders/Win7%20x86_64%20hg%20tests/builds/808/steps/run-tests.py%20%28python%202.7.13%29/logs/stdio

I'm not sure. We could probably just not support this on windows for now, or at least disable the test on Windows?

I’m ok with disabling the test as long as it doesn’t explode on Windows without turning the experimental knobs. It sounded like future would would depend on this, and I wasn’t sure if that landed yet.

Do we need a different strategy for Windows, or just eat the error? (See the unsupported signal message)
https://buildbot.mercurial-scm.org/builders/Win7%20x86_64%20hg%20tests/builds/808/steps/run-tests.py%20%28python%202.7.13%29/logs/stdio

I'm not sure. We could probably just not support this on windows for now, or at least disable the test on Windows?

I’m ok with disabling the test as long as it doesn’t explode on Windows without turning the experimental knobs. It sounded like future would would depend on this, and I wasn’t sure if that landed yet.

We’ve already made some use of it in core - anything that calls strip will hit this context manager, as will the (experimental) narrow extension.