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

durin42 (Augie Fackler) phabricator at mercurial-scm.org
Tue Jul 3 13:40:58 EDT 2018


This revision was automatically updated to reflect the committed changes.
Closed by commit rHG313a940d49a3: ui: add an uninterruptable context manager that can block SIGINT (authored by durin42, committed by ).

CHANGED PRIOR TO COMMIT
  https://phab.mercurial-scm.org/D3716?vs=9410&id=9411#toc

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D3716?vs=9410&id=9411

REVISION DETAIL
  https://phab.mercurial-scm.org/D3716

AFFECTED FILES
  mercurial/configitems.py
  mercurial/ui.py
  mercurial/utils/procutil.py
  tests/test-nointerrupt.t

CHANGE DETAILS

diff --git a/tests/test-nointerrupt.t b/tests/test-nointerrupt.t
new file mode 100644
--- /dev/null
+++ b/tests/test-nointerrupt.t
@@ -0,0 +1,83 @@
+Dummy extension simulating unsafe long running command
+  $ cat > sleepext.py <<EOF
+  > import time
+  > import itertools
+  > 
+  > from mercurial import registrar
+  > from mercurial.i18n import _
+  > 
+  > cmdtable = {}
+  > command = registrar.command(cmdtable)
+  > 
+  > @command(b'sleep', [], _(b'TIME'), norepo=True)
+  > def sleep(ui, sleeptime=b"1", **opts):
+  >     with ui.uninterruptable():
+  >         for _i in itertools.repeat(None, int(sleeptime)):
+  >             time.sleep(1)
+  >         ui.warn(b"end of unsafe operation\n")
+  >     ui.warn(b"%s second(s) passed\n" % sleeptime)
+  > EOF
+
+Kludge to emulate timeout(1) which is not generally available.
+  $ cat > timeout.py <<EOF
+  > from __future__ import print_function
+  > import argparse
+  > import signal
+  > import subprocess
+  > import sys
+  > import time
+  > 
+  > ap = argparse.ArgumentParser()
+  > ap.add_argument('-s', nargs=1, default='SIGTERM')
+  > ap.add_argument('duration', nargs=1, type=int)
+  > ap.add_argument('argv', nargs='*')
+  > opts = ap.parse_args()
+  > try:
+  >     sig = int(opts.s[0])
+  > except ValueError:
+  >     sname = opts.s[0]
+  >     if not sname.startswith('SIG'):
+  >         sname = 'SIG' + sname
+  >     sig = getattr(signal, sname)
+  > proc = subprocess.Popen(opts.argv)
+  > time.sleep(opts.duration[0])
+  > proc.poll()
+  > if proc.returncode is None:
+  >     proc.send_signal(sig)
+  >     proc.wait()
+  >     sys.exit(124)
+  > EOF
+
+Set up repository
+  $ hg init repo
+  $ cd repo
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > sleepext = ../sleepext.py
+  > EOF
+
+Test ctrl-c
+  $ python $TESTTMP/timeout.py -s INT 1 hg sleep 2
+  interrupted!
+  [124]
+
+  $ cat >> $HGRCPATH << EOF
+  > [experimental]
+  > nointerrupt = yes
+  > EOF
+
+  $ python $TESTTMP/timeout.py -s INT 1 hg sleep 2
+  interrupted!
+  [124]
+
+  $ cat >> $HGRCPATH << EOF
+  > [experimental]
+  > nointerrupt-interactiveonly = False
+  > EOF
+
+  $ python $TESTTMP/timeout.py -s INT 1 hg sleep 2
+  shutting down cleanly
+  press ^C again to terminate immediately (dangerous)
+  end of unsafe operation
+  interrupted!
+  [124]
diff --git a/mercurial/utils/procutil.py b/mercurial/utils/procutil.py
--- a/mercurial/utils/procutil.py
+++ b/mercurial/utils/procutil.py
@@ -415,3 +415,36 @@
     finally:
         if prevhandler is not None:
             signal.signal(signal.SIGCHLD, prevhandler)
+
+ at contextlib.contextmanager
+def uninterruptable(warn):
+    """Inhibit SIGINT handling on a region of code.
+
+    Note that if this is called in a non-main thread, it turns into a no-op.
+
+    Args:
+      warn: A callable which takes no arguments, and returns True if the
+            previous signal handling should be restored.
+    """
+
+    oldsiginthandler = [signal.getsignal(signal.SIGINT)]
+    shouldbail = []
+
+    def disabledsiginthandler(*args):
+        if warn():
+            signal.signal(signal.SIGINT, oldsiginthandler[0])
+            del oldsiginthandler[0]
+        shouldbail.append(True)
+
+    try:
+        try:
+            signal.signal(signal.SIGINT, disabledsiginthandler)
+        except ValueError:
+            # wrong thread, oh well, we tried
+            del oldsiginthandler[0]
+        yield
+    finally:
+        if oldsiginthandler:
+            signal.signal(signal.SIGINT, oldsiginthandler[0])
+        if shouldbail:
+            raise KeyboardInterrupt
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -224,6 +224,7 @@
         self._colormode = None
         self._terminfoparams = {}
         self._styles = {}
+        self._uninterruptible = False
 
         if src:
             self.fout = src.fout
@@ -334,6 +335,37 @@
             self._blockedtimes[key + '_blocked'] += \
                 (util.timer() - starttime) * 1000
 
+    @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"))
+            return True
+        with procutil.uninterruptable(warn):
+            try:
+                self._uninterruptible = True
+                yield
+            finally:
+                self._uninterruptible = False
+
     def formatter(self, topic, opts):
         return formatter.formatter(self, self, topic, opts)
 
diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -560,6 +560,9 @@
 coreconfigitem('experimental', 'mergedriver',
     default=None,
 )
+coreconfigitem('experimental', 'nointerrupt', default=False)
+coreconfigitem('experimental', 'nointerrupt-interactiveonly', default=True)
+
 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
     default=False,
 )



To: durin42, #hg-reviewers, indygreg
Cc: spectral, indygreg, yuja, martinvonz, mercurial-devel


More information about the Mercurial-devel mailing list