# HG changeset patch # User Doug Simon # Date 1324590697 -3600 # Node ID f3a50640333b198ae0fcd671fe04f7e316d9a0e3 # Parent b26279781d956b634fe2f4df0a332aaf7abe988d Added support for specifying a timeout when running an external command. diff -r b26279781d95 -r f3a50640333b mxtool/mx.py --- a/mxtool/mx.py Wed Dec 21 17:24:39 2011 +0100 +++ b/mxtool/mx.py Thu Dec 22 22:51:37 2011 +0100 @@ -87,20 +87,12 @@ # # Values can use environment variables with Bash syntax (e.g. ${HOME}). -import sys -import os -import subprocess +import sys, os, errno, time, subprocess, shlex, types, urllib2, contextlib, StringIO, zipfile +import shutil, fnmatch, re, xml.dom.minidom from collections import Callable from threading import Thread from argparse import ArgumentParser, REMAINDER from os.path import join, dirname, exists, getmtime, isabs, expandvars, isdir -import shlex -import types -import urllib2 -import contextlib -import StringIO -import zipfile -import shutil, fnmatch, re, xml.dom.minidom DEFAULT_JAVA_ARGS = '-ea -Xss2m -Xmx1g' @@ -462,6 +454,7 @@ self.add_argument('--Ja', action='append', dest='java_args_sfx', help='suffix Java VM arguments (e.g. --Ja @-dsa)', metavar='@', default=[]) self.add_argument('--user-home', help='users home directory', metavar='', default=os.path.expanduser('~')) self.add_argument('--java-home', help='JDK installation directory (must be JDK 6 or later)', metavar='') + self.add_argument('--timeout', help='Timeout (in seconds) for subprocesses', type=int, default=0, metavar='') def _parse_cmd_line(self, args=None): if args is None: @@ -506,10 +499,33 @@ def run_java(args, nonZeroIsFatal=True, out=None, err=None, cwd=None): return run(java().format_cmd(args), nonZeroIsFatal=nonZeroIsFatal, out=out, err=err, cwd=cwd) -def run(args, nonZeroIsFatal=True, out=None, err=None, cwd=None): +def _waitWithTimeout(process, args, timeout): + def _waitpid(pid): + while True: + try: + return os.waitpid(pid, os.WNOHANG) + except OSError, e: + if e.errno == errno.EINTR: + continue + raise + + end = time.time() + timeout + delay = 0.0005 + while True: + (pid, _) = _waitpid(process.pid) + if pid == process.pid: + return process.wait() + remaining = end - time.time() + if remaining <= 0: + process.kill() + abort('Process timed out after {0} seconds: {1}'.format(timeout, ' '.join(args))) + delay = min(delay * 2, remaining, .05) + time.sleep(delay) + +def run(args, nonZeroIsFatal=True, out=None, err=None, cwd=None, timeout=None): """ Run a command in a subprocess, wait for it to complete and return the exit status of the process. - If the exit status is non-zero and `nonZeroIsFatal` is true, then the program is exited with + If the exit status is non-zero and `nonZeroIsFatal` is true, then mx is exited with the same exit status. Each line of the standard output and error streams of the subprocess are redirected to the provided out and err functions if they are not None. @@ -522,8 +538,11 @@ if _opts.verbose: log(' '.join(args)) + if timeout is None and _opts.timeout != 0: + timeout = _opts.timeout + try: - if out is None and err is None: + if out is None and err is None and timeout is None: retcode = subprocess.call(args, cwd=cwd) else: def redirect(stream, f): @@ -539,7 +558,12 @@ t = Thread(target=redirect, args=(p.stderr, err)) t.daemon = True # thread dies with the program t.start() - retcode = p.wait() + if timeout is None: + retcode = p.wait() + else: + if get_os() == 'windows': + abort('Use of timeout not (yet) supported on Windows') + retcode = _waitWithTimeout(p, args, timeout) except OSError as e: log('Error executing \'' + ' '.join(args) + '\': ' + str(e)) if _opts.verbose: