##// END OF EJS Templates
ui: add an uninterruptable context manager that can block SIGINT...
Augie Fackler -
r38545:313a940d default
parent child Browse files
Show More
@@ -0,0 +1,83 b''
1 Dummy extension simulating unsafe long running command
2 $ cat > sleepext.py <<EOF
3 > import time
4 > import itertools
5 >
6 > from mercurial import registrar
7 > from mercurial.i18n import _
8 >
9 > cmdtable = {}
10 > command = registrar.command(cmdtable)
11 >
12 > @command(b'sleep', [], _(b'TIME'), norepo=True)
13 > def sleep(ui, sleeptime=b"1", **opts):
14 > with ui.uninterruptable():
15 > for _i in itertools.repeat(None, int(sleeptime)):
16 > time.sleep(1)
17 > ui.warn(b"end of unsafe operation\n")
18 > ui.warn(b"%s second(s) passed\n" % sleeptime)
19 > EOF
20
21 Kludge to emulate timeout(1) which is not generally available.
22 $ cat > timeout.py <<EOF
23 > from __future__ import print_function
24 > import argparse
25 > import signal
26 > import subprocess
27 > import sys
28 > import time
29 >
30 > ap = argparse.ArgumentParser()
31 > ap.add_argument('-s', nargs=1, default='SIGTERM')
32 > ap.add_argument('duration', nargs=1, type=int)
33 > ap.add_argument('argv', nargs='*')
34 > opts = ap.parse_args()
35 > try:
36 > sig = int(opts.s[0])
37 > except ValueError:
38 > sname = opts.s[0]
39 > if not sname.startswith('SIG'):
40 > sname = 'SIG' + sname
41 > sig = getattr(signal, sname)
42 > proc = subprocess.Popen(opts.argv)
43 > time.sleep(opts.duration[0])
44 > proc.poll()
45 > if proc.returncode is None:
46 > proc.send_signal(sig)
47 > proc.wait()
48 > sys.exit(124)
49 > EOF
50
51 Set up repository
52 $ hg init repo
53 $ cd repo
54 $ cat >> $HGRCPATH << EOF
55 > [extensions]
56 > sleepext = ../sleepext.py
57 > EOF
58
59 Test ctrl-c
60 $ python $TESTTMP/timeout.py -s INT 1 hg sleep 2
61 interrupted!
62 [124]
63
64 $ cat >> $HGRCPATH << EOF
65 > [experimental]
66 > nointerrupt = yes
67 > EOF
68
69 $ python $TESTTMP/timeout.py -s INT 1 hg sleep 2
70 interrupted!
71 [124]
72
73 $ cat >> $HGRCPATH << EOF
74 > [experimental]
75 > nointerrupt-interactiveonly = False
76 > EOF
77
78 $ python $TESTTMP/timeout.py -s INT 1 hg sleep 2
79 shutting down cleanly
80 press ^C again to terminate immediately (dangerous)
81 end of unsafe operation
82 interrupted!
83 [124]
@@ -560,6 +560,9 b" coreconfigitem('experimental', 'httppost"
560 coreconfigitem('experimental', 'mergedriver',
560 coreconfigitem('experimental', 'mergedriver',
561 default=None,
561 default=None,
562 )
562 )
563 coreconfigitem('experimental', 'nointerrupt', default=False)
564 coreconfigitem('experimental', 'nointerrupt-interactiveonly', default=True)
565
563 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
566 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
564 default=False,
567 default=False,
565 )
568 )
@@ -224,6 +224,7 b' class ui(object):'
224 self._colormode = None
224 self._colormode = None
225 self._terminfoparams = {}
225 self._terminfoparams = {}
226 self._styles = {}
226 self._styles = {}
227 self._uninterruptible = False
227
228
228 if src:
229 if src:
229 self.fout = src.fout
230 self.fout = src.fout
@@ -334,6 +335,37 b' class ui(object):'
334 self._blockedtimes[key + '_blocked'] += \
335 self._blockedtimes[key + '_blocked'] += \
335 (util.timer() - starttime) * 1000
336 (util.timer() - starttime) * 1000
336
337
338 @contextlib.contextmanager
339 def uninterruptable(self):
340 """Mark an operation as unsafe.
341
342 Most operations on a repository are safe to interrupt, but a
343 few are risky (for example repair.strip). This context manager
344 lets you advise Mercurial that something risky is happening so
345 that control-C etc can be blocked if desired.
346 """
347 enabled = self.configbool('experimental', 'nointerrupt')
348 if (enabled and
349 self.configbool('experimental', 'nointerrupt-interactiveonly')):
350 enabled = self.interactive()
351 if self._uninterruptible or not enabled:
352 # if nointerrupt support is turned off, the process isn't
353 # interactive, or we're already in an uninterruptable
354 # block, do nothing.
355 yield
356 return
357 def warn():
358 self.warn(_("shutting down cleanly\n"))
359 self.warn(
360 _("press ^C again to terminate immediately (dangerous)\n"))
361 return True
362 with procutil.uninterruptable(warn):
363 try:
364 self._uninterruptible = True
365 yield
366 finally:
367 self._uninterruptible = False
368
337 def formatter(self, topic, opts):
369 def formatter(self, topic, opts):
338 return formatter.formatter(self, self, topic, opts)
370 return formatter.formatter(self, self, topic, opts)
339
371
@@ -415,3 +415,36 b' def rundetached(args, condfn):'
415 finally:
415 finally:
416 if prevhandler is not None:
416 if prevhandler is not None:
417 signal.signal(signal.SIGCHLD, prevhandler)
417 signal.signal(signal.SIGCHLD, prevhandler)
418
419 @contextlib.contextmanager
420 def uninterruptable(warn):
421 """Inhibit SIGINT handling on a region of code.
422
423 Note that if this is called in a non-main thread, it turns into a no-op.
424
425 Args:
426 warn: A callable which takes no arguments, and returns True if the
427 previous signal handling should be restored.
428 """
429
430 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
431 shouldbail = []
432
433 def disabledsiginthandler(*args):
434 if warn():
435 signal.signal(signal.SIGINT, oldsiginthandler[0])
436 del oldsiginthandler[0]
437 shouldbail.append(True)
438
439 try:
440 try:
441 signal.signal(signal.SIGINT, disabledsiginthandler)
442 except ValueError:
443 # wrong thread, oh well, we tried
444 del oldsiginthandler[0]
445 yield
446 finally:
447 if oldsiginthandler:
448 signal.signal(signal.SIGINT, oldsiginthandler[0])
449 if shouldbail:
450 raise KeyboardInterrupt
General Comments 0
You need to be logged in to leave comments. Login now