Kill subprocesses on KeyboardInterrupt.
SVN traps SIGINT and attempts to clean itself up, but this results in hangs
waiting for TCP. This patch does two things: daemonizes worker threads so they
are culled when the main thread dies (is ctrl-C'd) and keeps track of spawned
subprocesses to kill any remaining ones when the main program is ctrl-C'd.
A user ctrl-C'ing gclient has to manually terminate hung SVN processes, so this
introduces no extra data loss or hazard. stracing a hung SVN process shows that
it is indeed hanging on TCP reads after receiving a SIGINT, implying there is an
underlying but in the SVN binary.
Review URL: https://chromiumcodereview.appspot.com/14759006
git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@198205 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/gclient_utils.py b/gclient_utils.py
index 041813b..bf9803e 100644
--- a/gclient_utils.py
+++ b/gclient_utils.py
@@ -323,6 +323,56 @@
return Annotated(fileobj)
+GCLIENT_CHILDREN = []
+GCLIENT_CHILDREN_LOCK = threading.Lock()
+
+
+class GClientChildren(object):
+ @staticmethod
+ def add(popen_obj):
+ with GCLIENT_CHILDREN_LOCK:
+ GCLIENT_CHILDREN.append(popen_obj)
+
+ @staticmethod
+ def remove(popen_obj):
+ with GCLIENT_CHILDREN_LOCK:
+ GCLIENT_CHILDREN.remove(popen_obj)
+
+ @staticmethod
+ def _attemptToKillChildren():
+ global GCLIENT_CHILDREN
+ with GCLIENT_CHILDREN_LOCK:
+ zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
+
+ for zombie in zombies:
+ try:
+ zombie.kill()
+ except OSError:
+ pass
+
+ with GCLIENT_CHILDREN_LOCK:
+ GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
+
+ @staticmethod
+ def _areZombies():
+ with GCLIENT_CHILDREN_LOCK:
+ return bool(GCLIENT_CHILDREN)
+
+ @staticmethod
+ def KillAllRemainingChildren():
+ GClientChildren._attemptToKillChildren()
+
+ if GClientChildren._areZombies():
+ time.sleep(0.5)
+ GClientChildren._attemptToKillChildren()
+
+ with GCLIENT_CHILDREN_LOCK:
+ if GCLIENT_CHILDREN:
+ print >> sys.stderr, 'Could not kill the following subprocesses:'
+ for zombie in GCLIENT_CHILDREN:
+ print >> sys.stderr, ' ', zombie.pid
+
+
def CheckCallAndFilter(args, stdout=None, filter_fn=None,
print_stdout=None, call_filter_on_first_line=False,
**kwargs):
@@ -344,6 +394,8 @@
args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
**kwargs)
+ GClientChildren.add(kid)
+
# Do a flush of stdout before we begin reading from the subprocess2's stdout
stdout.flush()
@@ -375,6 +427,11 @@
if len(in_line):
filter_fn(in_line)
rv = kid.wait()
+
+ # Don't put this in a 'finally,' since the child may still run if we get an
+ # exception.
+ GClientChildren.remove(kid)
+
except KeyboardInterrupt:
print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
raise
@@ -657,6 +714,7 @@
self.index = index
self.args = args
self.kwargs = kwargs
+ self.daemon = True
def run(self):
"""Runs in its own thread."""
@@ -664,18 +722,23 @@
work_queue = self.kwargs['work_queue']
try:
self.item.run(*self.args, **self.kwargs)
+ except KeyboardInterrupt:
+ logging.info('Caught KeyboardInterrupt in thread %s' % self.item.name)
+ logging.info(str(sys.exc_info()))
+ work_queue.exceptions.put(sys.exc_info())
+ raise
except Exception:
# Catch exception location.
logging.info('Caught exception in thread %s' % self.item.name)
logging.info(str(sys.exc_info()))
work_queue.exceptions.put(sys.exc_info())
- logging.info('_Worker.run(%s) done' % self.item.name)
-
- work_queue.ready_cond.acquire()
- try:
- work_queue.ready_cond.notifyAll()
finally:
- work_queue.ready_cond.release()
+ logging.info('_Worker.run(%s) done' % self.item.name)
+ work_queue.ready_cond.acquire()
+ try:
+ work_queue.ready_cond.notifyAll()
+ finally:
+ work_queue.ready_cond.release()
def GetEditor(git, git_editor=None):