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