blob: 058a30d1ce28b050bae70d07b910674dbf4c949f [file] [log] [blame]
Craig Tillerc2c79212015-02-16 12:00:01 -08001# 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 Nobleddef2462015-01-06 18:08:25 -080030"""Run a group of subprocesses and then finish."""
31
Craig Tiller71735182015-01-15 17:07:13 -080032import hashlib
Nicolas Nobleddef2462015-01-06 18:08:25 -080033import multiprocessing
Craig Tiller71735182015-01-15 17:07:13 -080034import os
Craig Tiller5058c692015-04-08 09:42:04 -070035import platform
Craig Tiller336ad502015-02-24 14:46:02 -080036import signal
Nicolas Nobleddef2462015-01-06 18:08:25 -080037import subprocess
38import sys
ctiller3040cb72015-01-07 12:13:17 -080039import tempfile
40import time
Nicolas Nobleddef2462015-01-06 18:08:25 -080041
ctiller3040cb72015-01-07 12:13:17 -080042
ctiller94e5dde2015-01-09 10:41:59 -080043_DEFAULT_MAX_JOBS = 16 * multiprocessing.cpu_count()
Nicolas Nobleddef2462015-01-06 18:08:25 -080044
45
Craig Tiller336ad502015-02-24 14:46:02 -080046# setup a signal handler so that signal.pause registers 'something'
47# when a child finishes
48# not using futures and threading to avoid a dependency on subprocess32
Craig Tiller5058c692015-04-08 09:42:04 -070049if platform.system() == "Windows":
50 pass
51else:
52 have_alarm = False
53 def alarm_handler(unused_signum, unused_frame):
54 global have_alarm
55 have_alarm = False
56
57 signal.signal(signal.SIGCHLD, lambda unused_signum, unused_frame: None)
58 signal.signal(signal.SIGALRM, alarm_handler)
Craig Tiller336ad502015-02-24 14:46:02 -080059
60
ctiller3040cb72015-01-07 12:13:17 -080061_SUCCESS = object()
62_FAILURE = object()
63_RUNNING = object()
64_KILLED = object()
65
66
Craig Tiller3b083062015-01-12 13:51:28 -080067_COLORS = {
Nicolas Noble044db742015-01-14 16:57:24 -080068 'red': [ 31, 0 ],
69 'green': [ 32, 0 ],
70 'yellow': [ 33, 0 ],
71 'lightgray': [ 37, 0],
72 'gray': [ 30, 1 ],
Craig Tiller3b083062015-01-12 13:51:28 -080073 }
74
75
76_BEGINNING_OF_LINE = '\x1b[0G'
77_CLEAR_LINE = '\x1b[2K'
78
79
80_TAG_COLOR = {
81 'FAILED': 'red',
Craig Tillere1d0d1c2015-02-27 08:54:23 -080082 'TIMEOUT': 'red',
Craig Tiller3b083062015-01-12 13:51:28 -080083 'PASSED': 'green',
Nicolas Noble044db742015-01-14 16:57:24 -080084 'START': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -080085 'WAITING': 'yellow',
Nicolas Noble044db742015-01-14 16:57:24 -080086 'SUCCESS': 'green',
87 'IDLE': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -080088 }
89
90
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +020091def message(tag, msg, explanatory_text=None, do_newline=False):
92 if message.old_tag == tag and message.old_msg == msg and not explanatory_text:
93 return
94 message.old_tag = tag
95 message.old_msg = msg
Craig Tiller5058c692015-04-08 09:42:04 -070096 if platform.system() == 'Windows':
97 if explanatory_text:
98 print explanatory_text
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +020099 print '%s: %s' % (tag, msg)
Craig Tiller5058c692015-04-08 09:42:04 -0700100 return
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800101 try:
102 sys.stdout.write('%s%s%s\x1b[%d;%dm%s\x1b[0m: %s%s' % (
103 _BEGINNING_OF_LINE,
104 _CLEAR_LINE,
105 '\n%s' % explanatory_text if explanatory_text is not None else '',
106 _COLORS[_TAG_COLOR[tag]][1],
107 _COLORS[_TAG_COLOR[tag]][0],
108 tag,
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +0200109 msg,
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800110 '\n' if do_newline or explanatory_text is not None else ''))
111 sys.stdout.flush()
112 except:
113 pass
Craig Tiller3b083062015-01-12 13:51:28 -0800114
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +0200115message.old_tag = ""
116message.old_msg = ""
Craig Tiller3b083062015-01-12 13:51:28 -0800117
Craig Tiller71735182015-01-15 17:07:13 -0800118def which(filename):
119 if '/' in filename:
120 return filename
121 for path in os.environ['PATH'].split(os.pathsep):
122 if os.path.exists(os.path.join(path, filename)):
123 return os.path.join(path, filename)
124 raise Exception('%s not found' % filename)
125
126
Craig Tiller547db2b2015-01-30 14:08:39 -0800127class JobSpec(object):
128 """Specifies what to run for a job."""
129
Jan Tattermusche8243592015-04-17 14:14:01 -0700130 def __init__(self, cmdline, shortname=None, environ=None, hash_targets=None, cwd=None, shell=False):
Craig Tiller547db2b2015-01-30 14:08:39 -0800131 """
132 Arguments:
133 cmdline: a list of arguments to pass as the command line
134 environ: a dictionary of environment variables to set in the child process
135 hash_targets: which files to include in the hash representing the jobs version
136 (or empty, indicating the job should not be hashed)
137 """
murgatroid99132ce6a2015-03-04 17:29:14 -0800138 if environ is None:
139 environ = {}
140 if hash_targets is None:
141 hash_targets = []
Craig Tiller547db2b2015-01-30 14:08:39 -0800142 self.cmdline = cmdline
143 self.environ = environ
144 self.shortname = cmdline[0] if shortname is None else shortname
145 self.hash_targets = hash_targets or []
Craig Tiller5058c692015-04-08 09:42:04 -0700146 self.cwd = cwd
Jan Tattermusche8243592015-04-17 14:14:01 -0700147 self.shell = shell
Craig Tiller547db2b2015-01-30 14:08:39 -0800148
149 def identity(self):
150 return '%r %r %r' % (self.cmdline, self.environ, self.hash_targets)
151
152 def __hash__(self):
153 return hash(self.identity())
154
155 def __cmp__(self, other):
156 return self.identity() == other.identity()
157
158
ctiller3040cb72015-01-07 12:13:17 -0800159class Job(object):
160 """Manages one job."""
161
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100162 def __init__(self, spec, bin_hash, newline_on_success, travis):
Craig Tiller547db2b2015-01-30 14:08:39 -0800163 self._spec = spec
Craig Tiller71735182015-01-15 17:07:13 -0800164 self._bin_hash = bin_hash
ctiller3040cb72015-01-07 12:13:17 -0800165 self._tempfile = tempfile.TemporaryFile()
Craig Tiller547db2b2015-01-30 14:08:39 -0800166 env = os.environ.copy()
167 for k, v in spec.environ.iteritems():
168 env[k] = v
Craig Tiller9d6139a2015-02-26 15:24:43 -0800169 self._start = time.time()
Craig Tiller547db2b2015-01-30 14:08:39 -0800170 self._process = subprocess.Popen(args=spec.cmdline,
ctiller3040cb72015-01-07 12:13:17 -0800171 stderr=subprocess.STDOUT,
Craig Tiller547db2b2015-01-30 14:08:39 -0800172 stdout=self._tempfile,
Craig Tiller5058c692015-04-08 09:42:04 -0700173 cwd=spec.cwd,
Jan Tattermusche8243592015-04-17 14:14:01 -0700174 shell=spec.shell,
Craig Tiller547db2b2015-01-30 14:08:39 -0800175 env=env)
ctiller3040cb72015-01-07 12:13:17 -0800176 self._state = _RUNNING
Nicolas Noble044db742015-01-14 16:57:24 -0800177 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100178 self._travis = travis
Craig Tillerb84728d2015-02-26 15:40:39 -0800179 message('START', spec.shortname, do_newline=self._travis)
ctiller3040cb72015-01-07 12:13:17 -0800180
Craig Tiller71735182015-01-15 17:07:13 -0800181 def state(self, update_cache):
ctiller3040cb72015-01-07 12:13:17 -0800182 """Poll current state of the job. Prints messages at completion."""
183 if self._state == _RUNNING and self._process.poll() is not None:
Craig Tiller9d6139a2015-02-26 15:24:43 -0800184 elapsed = time.time() - self._start
ctiller3040cb72015-01-07 12:13:17 -0800185 if self._process.returncode != 0:
186 self._state = _FAILURE
187 self._tempfile.seek(0)
188 stdout = self._tempfile.read()
Craig Tillerd0ffe142015-05-19 21:51:13 -0700189 message('FAILED', '%s [ret=%d, pid=%d]' % (
190 self._spec.shortname, self._process.returncode, self._process.pid),
191 stdout, do_newline=True)
ctiller3040cb72015-01-07 12:13:17 -0800192 else:
193 self._state = _SUCCESS
Craig Tiller9d6139a2015-02-26 15:24:43 -0800194 message('PASSED', '%s [time=%.1fsec]' % (self._spec.shortname, elapsed),
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100195 do_newline=self._newline_on_success or self._travis)
Craig Tiller547db2b2015-01-30 14:08:39 -0800196 if self._bin_hash:
197 update_cache.finished(self._spec.identity(), self._bin_hash)
Craig Tiller9b3cc742015-02-26 22:25:03 -0800198 elif self._state == _RUNNING and time.time() - self._start > 300:
Craig Tiller84216782015-05-12 09:43:54 -0700199 self._tempfile.seek(0)
200 stdout = self._tempfile.read()
201 message('TIMEOUT', self._spec.shortname, stdout, do_newline=True)
Craig Tiller9b3cc742015-02-26 22:25:03 -0800202 self.kill()
ctiller3040cb72015-01-07 12:13:17 -0800203 return self._state
204
205 def kill(self):
206 if self._state == _RUNNING:
207 self._state = _KILLED
208 self._process.terminate()
209
210
Nicolas Nobleddef2462015-01-06 18:08:25 -0800211class Jobset(object):
212 """Manages one run of jobs."""
213
Craig Tiller533b1a22015-05-29 08:41:29 -0700214 def __init__(self, check_cancelled, maxjobs, newline_on_success, travis,
215 stop_on_failure, cache):
ctiller3040cb72015-01-07 12:13:17 -0800216 self._running = set()
217 self._check_cancelled = check_cancelled
218 self._cancelled = False
Nicolas Nobleddef2462015-01-06 18:08:25 -0800219 self._failures = 0
Craig Tiller738c3342015-01-12 14:28:33 -0800220 self._completed = 0
ctiller94e5dde2015-01-09 10:41:59 -0800221 self._maxjobs = maxjobs
Nicolas Noble044db742015-01-14 16:57:24 -0800222 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100223 self._travis = travis
Craig Tiller71735182015-01-15 17:07:13 -0800224 self._cache = cache
Craig Tiller533b1a22015-05-29 08:41:29 -0700225 self._stop_on_failure = stop_on_failure
Craig Tiller74e770d2015-06-11 09:38:09 -0700226 self._hashes = {}
Nicolas Nobleddef2462015-01-06 18:08:25 -0800227
Craig Tiller547db2b2015-01-30 14:08:39 -0800228 def start(self, spec):
ctiller3040cb72015-01-07 12:13:17 -0800229 """Start a job. Return True on success, False on failure."""
ctiller94e5dde2015-01-09 10:41:59 -0800230 while len(self._running) >= self._maxjobs:
ctiller3040cb72015-01-07 12:13:17 -0800231 if self.cancelled(): return False
232 self.reap()
233 if self.cancelled(): return False
Craig Tiller547db2b2015-01-30 14:08:39 -0800234 if spec.hash_targets:
Craig Tiller74e770d2015-06-11 09:38:09 -0700235 if spec.identity() in self._hashes:
236 bin_hash = self._hashes[spec.identity()]
237 else:
238 bin_hash = hashlib.sha1()
239 for fn in spec.hash_targets:
240 with open(which(fn)) as f:
241 bin_hash.update(f.read())
242 bin_hash = bin_hash.hexdigest()
243 self._hashes[spec.identity()] = bin_hash
Craig Tiller547db2b2015-01-30 14:08:39 -0800244 should_run = self._cache.should_run(spec.identity(), bin_hash)
245 else:
246 bin_hash = None
247 should_run = True
248 if should_run:
Craig Tiller5058c692015-04-08 09:42:04 -0700249 try:
250 self._running.add(Job(spec,
251 bin_hash,
252 self._newline_on_success,
253 self._travis))
254 except:
255 message('FAILED', spec.shortname)
256 self._cancelled = True
257 return False
ctiller3040cb72015-01-07 12:13:17 -0800258 return True
Nicolas Nobleddef2462015-01-06 18:08:25 -0800259
ctiller3040cb72015-01-07 12:13:17 -0800260 def reap(self):
261 """Collect the dead jobs."""
262 while self._running:
263 dead = set()
264 for job in self._running:
Craig Tiller71735182015-01-15 17:07:13 -0800265 st = job.state(self._cache)
ctiller3040cb72015-01-07 12:13:17 -0800266 if st == _RUNNING: continue
Craig Tiller533b1a22015-05-29 08:41:29 -0700267 if st == _FAILURE or st == _KILLED:
268 self._failures += 1
269 if self._stop_on_failure:
270 self._cancelled = True
271 for job in self._running:
272 job.kill()
ctiller3040cb72015-01-07 12:13:17 -0800273 dead.add(job)
Craig Tiller74e770d2015-06-11 09:38:09 -0700274 break
ctiller3040cb72015-01-07 12:13:17 -0800275 for job in dead:
Craig Tiller738c3342015-01-12 14:28:33 -0800276 self._completed += 1
ctiller3040cb72015-01-07 12:13:17 -0800277 self._running.remove(job)
Craig Tiller3b083062015-01-12 13:51:28 -0800278 if dead: return
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100279 if (not self._travis):
280 message('WAITING', '%d jobs running, %d complete, %d failed' % (
281 len(self._running), self._completed, self._failures))
Craig Tiller5058c692015-04-08 09:42:04 -0700282 if platform.system() == 'Windows':
283 time.sleep(0.1)
284 else:
285 global have_alarm
286 if not have_alarm:
287 have_alarm = True
288 signal.alarm(10)
289 signal.pause()
ctiller3040cb72015-01-07 12:13:17 -0800290
291 def cancelled(self):
292 """Poll for cancellation."""
293 if self._cancelled: return True
294 if not self._check_cancelled(): return False
295 for job in self._running:
296 job.kill()
297 self._cancelled = True
298 return True
299
300 def finish(self):
301 while self._running:
302 if self.cancelled(): pass # poll cancellation
303 self.reap()
304 return not self.cancelled() and self._failures == 0
Nicolas Nobleddef2462015-01-06 18:08:25 -0800305
306
ctiller3040cb72015-01-07 12:13:17 -0800307def _never_cancelled():
308 return False
309
310
Craig Tiller71735182015-01-15 17:07:13 -0800311# cache class that caches nothing
312class NoCache(object):
313 def should_run(self, cmdline, bin_hash):
314 return True
315
316 def finished(self, cmdline, bin_hash):
317 pass
318
319
Nicolas Nobleb09078f2015-01-14 18:06:05 -0800320def run(cmdlines,
321 check_cancelled=_never_cancelled,
322 maxjobs=None,
Craig Tiller71735182015-01-15 17:07:13 -0800323 newline_on_success=False,
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100324 travis=False,
David Garcia Quintase90cd372015-05-31 18:15:26 -0700325 infinite_runs=False,
Craig Tiller533b1a22015-05-29 08:41:29 -0700326 stop_on_failure=False,
Craig Tiller71735182015-01-15 17:07:13 -0800327 cache=None):
ctiller94e5dde2015-01-09 10:41:59 -0800328 js = Jobset(check_cancelled,
Nicolas Noble044db742015-01-14 16:57:24 -0800329 maxjobs if maxjobs is not None else _DEFAULT_MAX_JOBS,
Craig Tiller533b1a22015-05-29 08:41:29 -0700330 newline_on_success, travis, stop_on_failure,
Craig Tiller71735182015-01-15 17:07:13 -0800331 cache if cache is not None else NoCache())
Craig Tillerb84728d2015-02-26 15:40:39 -0800332 for cmdline in cmdlines:
ctiller3040cb72015-01-07 12:13:17 -0800333 if not js.start(cmdline):
334 break
335 return js.finish()