| Craig Tiller | c2c7921 | 2015-02-16 12:00:01 -0800 | [diff] [blame] | 1 | # Copyright 2015, Google Inc. | 
|  | 2 | # All rights reserved. | 
|  | 3 | # | 
|  | 4 | # Redistribution and use in source and binary forms, with or without | 
|  | 5 | # modification, are permitted provided that the following conditions are | 
|  | 6 | # met: | 
|  | 7 | # | 
|  | 8 | #     * Redistributions of source code must retain the above copyright | 
|  | 9 | # notice, this list of conditions and the following disclaimer. | 
|  | 10 | #     * Redistributions in binary form must reproduce the above | 
|  | 11 | # copyright notice, this list of conditions and the following disclaimer | 
|  | 12 | # in the documentation and/or other materials provided with the | 
|  | 13 | # distribution. | 
|  | 14 | #     * Neither the name of Google Inc. nor the names of its | 
|  | 15 | # contributors may be used to endorse or promote products derived from | 
|  | 16 | # this software without specific prior written permission. | 
|  | 17 | # | 
|  | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 
|  | 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 
|  | 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
|  | 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | 
|  | 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
|  | 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | 
|  | 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
|  | 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
|  | 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 
|  | 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
|  | 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
|  | 29 |  | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 30 | """Run a group of subprocesses and then finish.""" | 
|  | 31 |  | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 32 | import hashlib | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 33 | import multiprocessing | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 34 | import os | 
| Craig Tiller | 5058c69 | 2015-04-08 09:42:04 -0700 | [diff] [blame] | 35 | import platform | 
| Craig Tiller | 336ad50 | 2015-02-24 14:46:02 -0800 | [diff] [blame] | 36 | import signal | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 37 | import string | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 38 | import subprocess | 
|  | 39 | import sys | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 40 | import tempfile | 
|  | 41 | import time | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 42 | import xml.etree.cElementTree as ET | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 43 |  | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 44 |  | 
| ctiller | 94e5dde | 2015-01-09 10:41:59 -0800 | [diff] [blame] | 45 | _DEFAULT_MAX_JOBS = 16 * multiprocessing.cpu_count() | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 46 |  | 
|  | 47 |  | 
| Craig Tiller | 336ad50 | 2015-02-24 14:46:02 -0800 | [diff] [blame] | 48 | # setup a signal handler so that signal.pause registers 'something' | 
|  | 49 | # when a child finishes | 
|  | 50 | # not using futures and threading to avoid a dependency on subprocess32 | 
| Craig Tiller | 5058c69 | 2015-04-08 09:42:04 -0700 | [diff] [blame] | 51 | if platform.system() == "Windows": | 
|  | 52 | pass | 
|  | 53 | else: | 
|  | 54 | have_alarm = False | 
|  | 55 | def alarm_handler(unused_signum, unused_frame): | 
|  | 56 | global have_alarm | 
|  | 57 | have_alarm = False | 
|  | 58 |  | 
|  | 59 | signal.signal(signal.SIGCHLD, lambda unused_signum, unused_frame: None) | 
|  | 60 | signal.signal(signal.SIGALRM, alarm_handler) | 
| Craig Tiller | 336ad50 | 2015-02-24 14:46:02 -0800 | [diff] [blame] | 61 |  | 
|  | 62 |  | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 63 | _SUCCESS = object() | 
|  | 64 | _FAILURE = object() | 
|  | 65 | _RUNNING = object() | 
|  | 66 | _KILLED = object() | 
|  | 67 |  | 
|  | 68 |  | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 69 | _COLORS = { | 
| Nicolas Noble | 044db74 | 2015-01-14 16:57:24 -0800 | [diff] [blame] | 70 | 'red': [ 31, 0 ], | 
|  | 71 | 'green': [ 32, 0 ], | 
|  | 72 | 'yellow': [ 33, 0 ], | 
|  | 73 | 'lightgray': [ 37, 0], | 
|  | 74 | 'gray': [ 30, 1 ], | 
| Craig Tiller | d7e09c3 | 2015-09-25 11:33:39 -0700 | [diff] [blame] | 75 | 'purple': [ 35, 0 ], | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 76 | } | 
|  | 77 |  | 
|  | 78 |  | 
|  | 79 | _BEGINNING_OF_LINE = '\x1b[0G' | 
|  | 80 | _CLEAR_LINE = '\x1b[2K' | 
|  | 81 |  | 
|  | 82 |  | 
|  | 83 | _TAG_COLOR = { | 
|  | 84 | 'FAILED': 'red', | 
| Craig Tiller | d7e09c3 | 2015-09-25 11:33:39 -0700 | [diff] [blame] | 85 | 'FLAKE': 'purple', | 
| Craig Tiller | 3dc1e4f | 2015-09-25 11:46:56 -0700 | [diff] [blame] | 86 | 'TIMEOUT_FLAKE': 'purple', | 
| Masood Malekghassemi | e5f7002 | 2015-06-29 09:20:26 -0700 | [diff] [blame] | 87 | 'WARNING': 'yellow', | 
| Craig Tiller | e1d0d1c | 2015-02-27 08:54:23 -0800 | [diff] [blame] | 88 | 'TIMEOUT': 'red', | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 89 | 'PASSED': 'green', | 
| Nicolas Noble | 044db74 | 2015-01-14 16:57:24 -0800 | [diff] [blame] | 90 | 'START': 'gray', | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 91 | 'WAITING': 'yellow', | 
| Nicolas Noble | 044db74 | 2015-01-14 16:57:24 -0800 | [diff] [blame] | 92 | 'SUCCESS': 'green', | 
|  | 93 | 'IDLE': 'gray', | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 94 | } | 
|  | 95 |  | 
|  | 96 |  | 
| Nicolas "Pixel" Noble | 99768ac | 2015-05-13 02:34:06 +0200 | [diff] [blame] | 97 | def message(tag, msg, explanatory_text=None, do_newline=False): | 
|  | 98 | if message.old_tag == tag and message.old_msg == msg and not explanatory_text: | 
|  | 99 | return | 
|  | 100 | message.old_tag = tag | 
|  | 101 | message.old_msg = msg | 
| Craig Tiller | 23d2f3f | 2015-02-24 15:23:32 -0800 | [diff] [blame] | 102 | try: | 
| Craig Tiller | 9f3b2d7 | 2015-08-25 11:50:57 -0700 | [diff] [blame] | 103 | if platform.system() == 'Windows' or not sys.stdout.isatty(): | 
|  | 104 | if explanatory_text: | 
|  | 105 | print explanatory_text | 
|  | 106 | print '%s: %s' % (tag, msg) | 
|  | 107 | return | 
| vjpai | a29d2d7 | 2015-07-08 10:31:15 -0700 | [diff] [blame] | 108 | sys.stdout.write('%s%s%s\x1b[%d;%dm%s\x1b[0m: %s%s' % ( | 
|  | 109 | _BEGINNING_OF_LINE, | 
|  | 110 | _CLEAR_LINE, | 
|  | 111 | '\n%s' % explanatory_text if explanatory_text is not None else '', | 
|  | 112 | _COLORS[_TAG_COLOR[tag]][1], | 
|  | 113 | _COLORS[_TAG_COLOR[tag]][0], | 
|  | 114 | tag, | 
|  | 115 | msg, | 
|  | 116 | '\n' if do_newline or explanatory_text is not None else '')) | 
| Craig Tiller | 23d2f3f | 2015-02-24 15:23:32 -0800 | [diff] [blame] | 117 | sys.stdout.flush() | 
|  | 118 | except: | 
|  | 119 | pass | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 120 |  | 
| Nicolas "Pixel" Noble | 99768ac | 2015-05-13 02:34:06 +0200 | [diff] [blame] | 121 | message.old_tag = "" | 
|  | 122 | message.old_msg = "" | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 123 |  | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 124 | def which(filename): | 
|  | 125 | if '/' in filename: | 
|  | 126 | return filename | 
|  | 127 | for path in os.environ['PATH'].split(os.pathsep): | 
|  | 128 | if os.path.exists(os.path.join(path, filename)): | 
|  | 129 | return os.path.join(path, filename) | 
|  | 130 | raise Exception('%s not found' % filename) | 
|  | 131 |  | 
|  | 132 |  | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 133 | class JobSpec(object): | 
|  | 134 | """Specifies what to run for a job.""" | 
|  | 135 |  | 
| Jan Tattermusch | 725835a | 2015-08-01 21:02:35 -0700 | [diff] [blame] | 136 | def __init__(self, cmdline, shortname=None, environ=None, hash_targets=None, | 
| Craig Tiller | 95cc07b | 2015-09-28 13:41:30 -0700 | [diff] [blame] | 137 | cwd=None, shell=False, timeout_seconds=5*60, flake_retries=0, | 
|  | 138 | timeout_retries=0): | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 139 | """ | 
|  | 140 | Arguments: | 
|  | 141 | cmdline: a list of arguments to pass as the command line | 
|  | 142 | environ: a dictionary of environment variables to set in the child process | 
|  | 143 | hash_targets: which files to include in the hash representing the jobs version | 
|  | 144 | (or empty, indicating the job should not be hashed) | 
|  | 145 | """ | 
| murgatroid99 | 132ce6a | 2015-03-04 17:29:14 -0800 | [diff] [blame] | 146 | if environ is None: | 
|  | 147 | environ = {} | 
|  | 148 | if hash_targets is None: | 
|  | 149 | hash_targets = [] | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 150 | self.cmdline = cmdline | 
|  | 151 | self.environ = environ | 
|  | 152 | self.shortname = cmdline[0] if shortname is None else shortname | 
|  | 153 | self.hash_targets = hash_targets or [] | 
| Craig Tiller | 5058c69 | 2015-04-08 09:42:04 -0700 | [diff] [blame] | 154 | self.cwd = cwd | 
| Jan Tattermusch | e824359 | 2015-04-17 14:14:01 -0700 | [diff] [blame] | 155 | self.shell = shell | 
| Jan Tattermusch | 725835a | 2015-08-01 21:02:35 -0700 | [diff] [blame] | 156 | self.timeout_seconds = timeout_seconds | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 157 | self.flake_retries = flake_retries | 
| Craig Tiller | bfc8a06 | 2015-09-28 14:40:21 -0700 | [diff] [blame] | 158 | self.timeout_retries = timeout_retries | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 159 |  | 
|  | 160 | def identity(self): | 
|  | 161 | return '%r %r %r' % (self.cmdline, self.environ, self.hash_targets) | 
|  | 162 |  | 
|  | 163 | def __hash__(self): | 
|  | 164 | return hash(self.identity()) | 
|  | 165 |  | 
|  | 166 | def __cmp__(self, other): | 
|  | 167 | return self.identity() == other.identity() | 
|  | 168 |  | 
|  | 169 |  | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 170 | class Job(object): | 
|  | 171 | """Manages one job.""" | 
|  | 172 |  | 
| Craig Tiller | f53d9c8 | 2015-08-04 14:19:43 -0700 | [diff] [blame] | 173 | def __init__(self, spec, bin_hash, newline_on_success, travis, add_env, xml_report): | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 174 | self._spec = spec | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 175 | self._bin_hash = bin_hash | 
| Nicolas Noble | 044db74 | 2015-01-14 16:57:24 -0800 | [diff] [blame] | 176 | self._newline_on_success = newline_on_success | 
| Nicolas "Pixel" Noble | a7df3f9 | 2015-02-26 22:07:04 +0100 | [diff] [blame] | 177 | self._travis = travis | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 178 | self._add_env = add_env.copy() | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 179 | self._xml_test = ET.SubElement(xml_report, 'testcase', | 
|  | 180 | name=self._spec.shortname) if xml_report is not None else None | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 181 | self._retries = 0 | 
| Craig Tiller | 95cc07b | 2015-09-28 13:41:30 -0700 | [diff] [blame] | 182 | self._timeout_retries = 0 | 
| Jan Tattermusch | 91ad018 | 2015-10-01 09:22:03 -0700 | [diff] [blame^] | 183 | self._suppress_failure_message = False | 
| Craig Tiller | b84728d | 2015-02-26 15:40:39 -0800 | [diff] [blame] | 184 | message('START', spec.shortname, do_newline=self._travis) | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 185 | self.start() | 
|  | 186 |  | 
|  | 187 | def start(self): | 
|  | 188 | self._tempfile = tempfile.TemporaryFile() | 
|  | 189 | env = dict(os.environ) | 
|  | 190 | env.update(self._spec.environ) | 
|  | 191 | env.update(self._add_env) | 
|  | 192 | self._start = time.time() | 
|  | 193 | self._process = subprocess.Popen(args=self._spec.cmdline, | 
|  | 194 | stderr=subprocess.STDOUT, | 
|  | 195 | stdout=self._tempfile, | 
|  | 196 | cwd=self._spec.cwd, | 
|  | 197 | shell=self._spec.shell, | 
|  | 198 | env=env) | 
|  | 199 | self._state = _RUNNING | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 200 |  | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 201 | def state(self, update_cache): | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 202 | """Poll current state of the job. Prints messages at completion.""" | 
|  | 203 | if self._state == _RUNNING and self._process.poll() is not None: | 
| Craig Tiller | 9d6139a | 2015-02-26 15:24:43 -0800 | [diff] [blame] | 204 | elapsed = time.time() - self._start | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 205 | self._tempfile.seek(0) | 
|  | 206 | stdout = self._tempfile.read() | 
|  | 207 | filtered_stdout = filter(lambda x: x in string.printable, stdout.decode(errors='ignore')) | 
| Nicolas "Pixel" Noble | 4a5a8f3 | 2015-08-13 19:43:00 +0200 | [diff] [blame] | 208 | # TODO: looks like jenkins master is slow because parsing the junit results XMLs is not | 
|  | 209 | # implemented efficiently. This is an experiment to workaround the issue by making sure | 
|  | 210 | # results.xml file is small enough. | 
|  | 211 | filtered_stdout = filtered_stdout[-128:] | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 212 | if self._xml_test is not None: | 
|  | 213 | self._xml_test.set('time', str(elapsed)) | 
|  | 214 | ET.SubElement(self._xml_test, 'system-out').text = filtered_stdout | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 215 | if self._process.returncode != 0: | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 216 | if self._retries < self._spec.flake_retries: | 
|  | 217 | message('FLAKE', '%s [ret=%d, pid=%d]' % ( | 
| Craig Tiller | d0ffe14 | 2015-05-19 21:51:13 -0700 | [diff] [blame] | 218 | self._spec.shortname, self._process.returncode, self._process.pid), | 
|  | 219 | stdout, do_newline=True) | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 220 | self._retries += 1 | 
|  | 221 | self.start() | 
|  | 222 | else: | 
|  | 223 | self._state = _FAILURE | 
| Jan Tattermusch | 91ad018 | 2015-10-01 09:22:03 -0700 | [diff] [blame^] | 224 | if not self._suppress_failure_message: | 
|  | 225 | message('FAILED', '%s [ret=%d, pid=%d]' % ( | 
|  | 226 | self._spec.shortname, self._process.returncode, self._process.pid), | 
|  | 227 | stdout, do_newline=True) | 
| Craig Tiller | 91318bc | 2015-09-24 08:58:39 -0700 | [diff] [blame] | 228 | if self._xml_test is not None: | 
|  | 229 | ET.SubElement(self._xml_test, 'failure', message='Failure').text | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 230 | else: | 
|  | 231 | self._state = _SUCCESS | 
| Craig Tiller | 95cc07b | 2015-09-28 13:41:30 -0700 | [diff] [blame] | 232 | message('PASSED', '%s [time=%.1fsec; retries=%d;%d]' % ( | 
|  | 233 | self._spec.shortname, elapsed, self._retries, self._timeout_retries), | 
|  | 234 | do_newline=self._newline_on_success or self._travis) | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 235 | if self._bin_hash: | 
|  | 236 | update_cache.finished(self._spec.identity(), self._bin_hash) | 
| Jan Tattermusch | 725835a | 2015-08-01 21:02:35 -0700 | [diff] [blame] | 237 | elif self._state == _RUNNING and time.time() - self._start > self._spec.timeout_seconds: | 
| Craig Tiller | 8421678 | 2015-05-12 09:43:54 -0700 | [diff] [blame] | 238 | self._tempfile.seek(0) | 
|  | 239 | stdout = self._tempfile.read() | 
| Nicolas "Pixel" Noble | f716c0c | 2015-07-12 01:26:17 +0200 | [diff] [blame] | 240 | filtered_stdout = filter(lambda x: x in string.printable, stdout.decode(errors='ignore')) | 
| Craig Tiller | 95cc07b | 2015-09-28 13:41:30 -0700 | [diff] [blame] | 241 | if self._timeout_retries < self._spec.timeout_retries: | 
| Craig Tiller | 3dc1e4f | 2015-09-25 11:46:56 -0700 | [diff] [blame] | 242 | message('TIMEOUT_FLAKE', self._spec.shortname, stdout, do_newline=True) | 
| Craig Tiller | 95cc07b | 2015-09-28 13:41:30 -0700 | [diff] [blame] | 243 | self._timeout_retries += 1 | 
| Craig Tiller | 3dc1e4f | 2015-09-25 11:46:56 -0700 | [diff] [blame] | 244 | self._process.terminate() | 
|  | 245 | self.start() | 
|  | 246 | else: | 
|  | 247 | message('TIMEOUT', self._spec.shortname, stdout, do_newline=True) | 
|  | 248 | self.kill() | 
|  | 249 | if self._xml_test is not None: | 
|  | 250 | ET.SubElement(self._xml_test, 'system-out').text = filtered_stdout | 
|  | 251 | ET.SubElement(self._xml_test, 'error', message='Timeout') | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 252 | return self._state | 
|  | 253 |  | 
|  | 254 | def kill(self): | 
|  | 255 | if self._state == _RUNNING: | 
|  | 256 | self._state = _KILLED | 
|  | 257 | self._process.terminate() | 
|  | 258 |  | 
| Jan Tattermusch | 91ad018 | 2015-10-01 09:22:03 -0700 | [diff] [blame^] | 259 | def suppress_failure_message(self): | 
|  | 260 | self._suppress_failure_message = True | 
|  | 261 |  | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 262 |  | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 263 | class Jobset(object): | 
|  | 264 | """Manages one run of jobs.""" | 
|  | 265 |  | 
| Craig Tiller | 533b1a2 | 2015-05-29 08:41:29 -0700 | [diff] [blame] | 266 | def __init__(self, check_cancelled, maxjobs, newline_on_success, travis, | 
| Craig Tiller | f53d9c8 | 2015-08-04 14:19:43 -0700 | [diff] [blame] | 267 | stop_on_failure, add_env, cache, xml_report): | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 268 | self._running = set() | 
|  | 269 | self._check_cancelled = check_cancelled | 
|  | 270 | self._cancelled = False | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 271 | self._failures = 0 | 
| Craig Tiller | 738c334 | 2015-01-12 14:28:33 -0800 | [diff] [blame] | 272 | self._completed = 0 | 
| ctiller | 94e5dde | 2015-01-09 10:41:59 -0800 | [diff] [blame] | 273 | self._maxjobs = maxjobs | 
| Nicolas Noble | 044db74 | 2015-01-14 16:57:24 -0800 | [diff] [blame] | 274 | self._newline_on_success = newline_on_success | 
| Nicolas "Pixel" Noble | a7df3f9 | 2015-02-26 22:07:04 +0100 | [diff] [blame] | 275 | self._travis = travis | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 276 | self._cache = cache | 
| Craig Tiller | 533b1a2 | 2015-05-29 08:41:29 -0700 | [diff] [blame] | 277 | self._stop_on_failure = stop_on_failure | 
| Craig Tiller | 74e770d | 2015-06-11 09:38:09 -0700 | [diff] [blame] | 278 | self._hashes = {} | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 279 | self._xml_report = xml_report | 
| Craig Tiller | f53d9c8 | 2015-08-04 14:19:43 -0700 | [diff] [blame] | 280 | self._add_env = add_env | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 281 |  | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 282 | def start(self, spec): | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 283 | """Start a job. Return True on success, False on failure.""" | 
| ctiller | 94e5dde | 2015-01-09 10:41:59 -0800 | [diff] [blame] | 284 | while len(self._running) >= self._maxjobs: | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 285 | if self.cancelled(): return False | 
|  | 286 | self.reap() | 
|  | 287 | if self.cancelled(): return False | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 288 | if spec.hash_targets: | 
| Craig Tiller | 74e770d | 2015-06-11 09:38:09 -0700 | [diff] [blame] | 289 | if spec.identity() in self._hashes: | 
|  | 290 | bin_hash = self._hashes[spec.identity()] | 
|  | 291 | else: | 
|  | 292 | bin_hash = hashlib.sha1() | 
|  | 293 | for fn in spec.hash_targets: | 
|  | 294 | with open(which(fn)) as f: | 
|  | 295 | bin_hash.update(f.read()) | 
|  | 296 | bin_hash = bin_hash.hexdigest() | 
|  | 297 | self._hashes[spec.identity()] = bin_hash | 
| Craig Tiller | 547db2b | 2015-01-30 14:08:39 -0800 | [diff] [blame] | 298 | should_run = self._cache.should_run(spec.identity(), bin_hash) | 
|  | 299 | else: | 
|  | 300 | bin_hash = None | 
|  | 301 | should_run = True | 
|  | 302 | if should_run: | 
| Craig Tiller | f53d9c8 | 2015-08-04 14:19:43 -0700 | [diff] [blame] | 303 | self._running.add(Job(spec, | 
|  | 304 | bin_hash, | 
|  | 305 | self._newline_on_success, | 
|  | 306 | self._travis, | 
|  | 307 | self._add_env, | 
|  | 308 | self._xml_report)) | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 309 | return True | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 310 |  | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 311 | def reap(self): | 
|  | 312 | """Collect the dead jobs.""" | 
|  | 313 | while self._running: | 
|  | 314 | dead = set() | 
|  | 315 | for job in self._running: | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 316 | st = job.state(self._cache) | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 317 | if st == _RUNNING: continue | 
| Craig Tiller | 533b1a2 | 2015-05-29 08:41:29 -0700 | [diff] [blame] | 318 | if st == _FAILURE or st == _KILLED: | 
|  | 319 | self._failures += 1 | 
|  | 320 | if self._stop_on_failure: | 
|  | 321 | self._cancelled = True | 
|  | 322 | for job in self._running: | 
|  | 323 | job.kill() | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 324 | dead.add(job) | 
| Craig Tiller | 74e770d | 2015-06-11 09:38:09 -0700 | [diff] [blame] | 325 | break | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 326 | for job in dead: | 
| Craig Tiller | 738c334 | 2015-01-12 14:28:33 -0800 | [diff] [blame] | 327 | self._completed += 1 | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 328 | self._running.remove(job) | 
| Craig Tiller | 3b08306 | 2015-01-12 13:51:28 -0800 | [diff] [blame] | 329 | if dead: return | 
| Nicolas "Pixel" Noble | a7df3f9 | 2015-02-26 22:07:04 +0100 | [diff] [blame] | 330 | if (not self._travis): | 
|  | 331 | message('WAITING', '%d jobs running, %d complete, %d failed' % ( | 
|  | 332 | len(self._running), self._completed, self._failures)) | 
| Craig Tiller | 5058c69 | 2015-04-08 09:42:04 -0700 | [diff] [blame] | 333 | if platform.system() == 'Windows': | 
|  | 334 | time.sleep(0.1) | 
|  | 335 | else: | 
|  | 336 | global have_alarm | 
|  | 337 | if not have_alarm: | 
|  | 338 | have_alarm = True | 
|  | 339 | signal.alarm(10) | 
|  | 340 | signal.pause() | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 341 |  | 
|  | 342 | def cancelled(self): | 
|  | 343 | """Poll for cancellation.""" | 
|  | 344 | if self._cancelled: return True | 
|  | 345 | if not self._check_cancelled(): return False | 
|  | 346 | for job in self._running: | 
|  | 347 | job.kill() | 
|  | 348 | self._cancelled = True | 
|  | 349 | return True | 
|  | 350 |  | 
|  | 351 | def finish(self): | 
|  | 352 | while self._running: | 
|  | 353 | if self.cancelled(): pass  # poll cancellation | 
|  | 354 | self.reap() | 
|  | 355 | return not self.cancelled() and self._failures == 0 | 
| Nicolas Noble | ddef246 | 2015-01-06 18:08:25 -0800 | [diff] [blame] | 356 |  | 
|  | 357 |  | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 358 | def _never_cancelled(): | 
|  | 359 | return False | 
|  | 360 |  | 
|  | 361 |  | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 362 | # cache class that caches nothing | 
|  | 363 | class NoCache(object): | 
|  | 364 | def should_run(self, cmdline, bin_hash): | 
|  | 365 | return True | 
|  | 366 |  | 
|  | 367 | def finished(self, cmdline, bin_hash): | 
|  | 368 | pass | 
|  | 369 |  | 
|  | 370 |  | 
| Nicolas Noble | b09078f | 2015-01-14 18:06:05 -0800 | [diff] [blame] | 371 | def run(cmdlines, | 
|  | 372 | check_cancelled=_never_cancelled, | 
|  | 373 | maxjobs=None, | 
| Craig Tiller | 7173518 | 2015-01-15 17:07:13 -0800 | [diff] [blame] | 374 | newline_on_success=False, | 
| Nicolas "Pixel" Noble | a7df3f9 | 2015-02-26 22:07:04 +0100 | [diff] [blame] | 375 | travis=False, | 
| David Garcia Quintas | e90cd37 | 2015-05-31 18:15:26 -0700 | [diff] [blame] | 376 | infinite_runs=False, | 
| Craig Tiller | 533b1a2 | 2015-05-29 08:41:29 -0700 | [diff] [blame] | 377 | stop_on_failure=False, | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 378 | cache=None, | 
| Craig Tiller | f53d9c8 | 2015-08-04 14:19:43 -0700 | [diff] [blame] | 379 | xml_report=None, | 
|  | 380 | add_env={}): | 
| ctiller | 94e5dde | 2015-01-09 10:41:59 -0800 | [diff] [blame] | 381 | js = Jobset(check_cancelled, | 
| Nicolas Noble | 044db74 | 2015-01-14 16:57:24 -0800 | [diff] [blame] | 382 | maxjobs if maxjobs is not None else _DEFAULT_MAX_JOBS, | 
| Craig Tiller | f53d9c8 | 2015-08-04 14:19:43 -0700 | [diff] [blame] | 383 | newline_on_success, travis, stop_on_failure, add_env, | 
| Nicolas "Pixel" Noble | 5937b5b | 2015-06-26 02:04:12 +0200 | [diff] [blame] | 384 | cache if cache is not None else NoCache(), | 
|  | 385 | xml_report) | 
| Craig Tiller | b84728d | 2015-02-26 15:40:39 -0800 | [diff] [blame] | 386 | for cmdline in cmdlines: | 
| ctiller | 3040cb7 | 2015-01-07 12:13:17 -0800 | [diff] [blame] | 387 | if not js.start(cmdline): | 
|  | 388 | break | 
|  | 389 | return js.finish() |