OLD | NEW |
| (Empty) |
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 """Base class for all slave-side build steps. """ | |
6 | |
7 import config | |
8 # pylint: disable=W0611 | |
9 import flavor_utils | |
10 import imp | |
11 import multiprocessing | |
12 import os | |
13 import shlex | |
14 import signal | |
15 import subprocess | |
16 import sys | |
17 import time | |
18 import traceback | |
19 | |
20 from playback_dirs import LocalSkpPlaybackDirs | |
21 from playback_dirs import StorageSkpPlaybackDirs | |
22 | |
23 BUILDBOT_PATH = os.path.realpath(os.path.join( | |
24 os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir)) | |
25 | |
26 # Add important directories to the PYTHONPATH | |
27 sys.path.append(os.path.join(BUILDBOT_PATH)) | |
28 sys.path.append(os.path.join(BUILDBOT_PATH, 'site_config')) | |
29 sys.path.append(os.path.join(BUILDBOT_PATH, 'master')) | |
30 sys.path.insert(0, os.path.join(BUILDBOT_PATH, 'common')) | |
31 | |
32 import builder_name_schema | |
33 import slave_hosts_cfg | |
34 import slaves_cfg | |
35 | |
36 from py.utils import misc | |
37 | |
38 | |
39 DEFAULT_TIMEOUT = 4800 | |
40 DEFAULT_NO_OUTPUT_TIMEOUT = 3600 | |
41 DEFAULT_NUM_CORES = 2 | |
42 | |
43 | |
44 GM_EXPECTATIONS_FILENAME = 'expected-results.json' | |
45 GM_IGNORE_FAILURES_FILE = 'ignored-tests.txt' | |
46 | |
47 | |
48 # multiprocessing.Value doesn't accept boolean types, so we have to use an int. | |
49 INT_TRUE = 1 | |
50 INT_FALSE = 0 | |
51 build_step_stdout_has_written = multiprocessing.Value('i', INT_FALSE) | |
52 | |
53 | |
54 class BuildStepWarning(Exception): | |
55 pass | |
56 | |
57 | |
58 class BuildStepFailure(Exception): | |
59 pass | |
60 | |
61 | |
62 class BuildStepTimeout(Exception): | |
63 pass | |
64 | |
65 | |
66 class BuildStepLogger(object): | |
67 """ Override stdout so that we can keep track of when anything has been | |
68 logged. This enables timeouts based on how long the process has gone without | |
69 writing output. | |
70 """ | |
71 def __init__(self): | |
72 self.stdout = sys.stdout | |
73 sys.stdout = self | |
74 build_step_stdout_has_written.value = INT_FALSE | |
75 | |
76 def __del__(self): | |
77 sys.stdout = self.stdout | |
78 | |
79 def fileno(self): | |
80 return self.stdout.fileno() | |
81 | |
82 def write(self, data): | |
83 build_step_stdout_has_written.value = INT_TRUE | |
84 self.stdout.write(data) | |
85 | |
86 def flush(self): | |
87 self.stdout.flush() | |
88 | |
89 | |
90 def _GetBuildSlaveID(desired_slave_name): | |
91 """ Returns the index of the build slave in the list of build slaves running | |
92 on this machine, in the form of a string. """ | |
93 for host_dict in slave_hosts_cfg.SLAVE_HOSTS.itervalues(): | |
94 for slave_name, slave_id, _ in host_dict.slaves: | |
95 if slave_name == desired_slave_name: | |
96 return slave_id | |
97 raise Exception('No build slave found with name %s' % desired_slave_name) | |
98 | |
99 | |
100 class BuildStep(multiprocessing.Process): | |
101 | |
102 def __init__(self, args, attempts=1, timeout=DEFAULT_TIMEOUT, | |
103 no_output_timeout=DEFAULT_NO_OUTPUT_TIMEOUT): | |
104 """ Constructs a BuildStep instance. | |
105 | |
106 args: dictionary containing arguments to this BuildStep. | |
107 attempts: how many times to try this BuildStep before giving up. | |
108 timeout: maximum time allowed for this BuildStep. | |
109 no_output_timeout: maximum time allowed for this BuildStep to run without | |
110 any output. | |
111 """ | |
112 multiprocessing.Process.__init__(self) | |
113 | |
114 self._args = dict(args) | |
115 | |
116 self.timeout = timeout | |
117 self.no_output_timeout = no_output_timeout | |
118 self.attempts = attempts | |
119 | |
120 self._builder_name = args['builder_name'] | |
121 self._build_number = args['build_number'] | |
122 self._slavename = os.environ['TESTING_SLAVENAME'] | |
123 | |
124 # Change to the correct working directory. This is needed on Windows, where | |
125 # our path lengths would otherwise be too long. | |
126 if os.name == 'nt': | |
127 curdir = os.getcwd() | |
128 # The buildslave name and builder name combo is too long. Instead, obtain | |
129 # a unique buildslave ID. | |
130 buildslave_id = _GetBuildSlaveID(self._slavename) | |
131 workdir = os.path.join('C:\\', buildslave_id, self._builder_name, | |
132 curdir[curdir.rfind('build'):]) | |
133 print 'chdir to %s' % workdir | |
134 if not os.path.isdir(workdir): | |
135 os.makedirs(workdir) | |
136 os.chdir(workdir) | |
137 | |
138 # Add CWD to the PYTHONPATH | |
139 sys.path.append(os.getcwd()) | |
140 | |
141 self._configuration = args['configuration'] | |
142 if os.name == 'nt' and 'x86_64' in self._builder_name: | |
143 self._configuration += '_x64' | |
144 | |
145 self._target_platform = args['target_platform'] | |
146 self._deps_target_os = \ | |
147 None if args['deps_target_os'] == 'None' else args['deps_target_os'] | |
148 self._revision = \ | |
149 None if args['revision'] == 'None' or args['revision'] == 'HEAD' \ | |
150 else args['revision'] | |
151 self._got_revision = \ | |
152 None if args['got_revision'] == 'None' else args['got_revision'] | |
153 | |
154 # Import the flavor-specific build step utils module. | |
155 flavor = args.get('flavor', 'default') | |
156 try: | |
157 flavor_utils_module_name = '%s_build_step_utils' % flavor | |
158 flavor_utils_path = os.path.join(os.path.dirname(__file__), | |
159 'flavor_utils', | |
160 '%s.py' % flavor_utils_module_name) | |
161 flavor_utils_module = imp.load_source( | |
162 'flavor_utils.%s' % flavor_utils_module_name, flavor_utils_path) | |
163 flavor_utils_class_name = ''.join([part.title() for part in | |
164 flavor.split('_')]) | |
165 flavor_utils_class = getattr(flavor_utils_module, | |
166 '%sBuildStepUtils' % flavor_utils_class_name) | |
167 self._flavor_utils = flavor_utils_class(self) | |
168 except (ImportError, IOError) as e: | |
169 raise Exception('Unrecognized build flavor: %s\n%s' % (flavor, e)) | |
170 | |
171 # Trybots should use expectations from the corresponding waterfall bot. | |
172 # This fixes https://code.google.com/p/skia/issues/detail?id=1552 | |
173 gm_expected_subdir = builder_name_schema.GetWaterfallBot( | |
174 self._builder_name) | |
175 | |
176 # Figure out where we are going to store images generated by GM. | |
177 self._gm_actual_basedir = os.path.join(os.pardir, os.pardir, 'gm', 'actual') | |
178 self._gm_expected_dir = os.path.join('expectations', 'gm', | |
179 gm_expected_subdir) | |
180 self._gm_actual_dir = os.path.join(self._gm_actual_basedir, | |
181 self._builder_name) | |
182 self._dm_dir = os.path.join(os.pardir, os.pardir, 'dm') | |
183 | |
184 self._resource_dir = 'resources' | |
185 self._make_flags = shlex.split(args['make_flags'].replace('"', '')) | |
186 self._test_args = shlex.split(args['test_args'].replace('"', '')) | |
187 self._gm_args = shlex.split(args['gm_args'].replace('"', '')) | |
188 self._bench_args = shlex.split(args['bench_args'].replace('"', '')) | |
189 self._is_try = args['is_try'] == 'True' | |
190 | |
191 self._default_make_flags = [] | |
192 self._default_ninja_flags = [] | |
193 | |
194 # TODO(epoger): Throughout the buildbot code, we use various terms to refer | |
195 # to the same thing: "skps", "pictures", "replay", "playback". | |
196 # We should pick one of those terms, and rename things so that we are | |
197 # consistent. | |
198 # See https://codereview.chromium.org/295753002/ for additional discussion. | |
199 | |
200 # Adding the playback directory transfer objects. | |
201 self._local_playback_dirs = LocalSkpPlaybackDirs( | |
202 self._builder_name, | |
203 None if args['perf_output_basedir'] == 'None' | |
204 else args['perf_output_basedir']) | |
205 self._storage_playback_dirs = StorageSkpPlaybackDirs( | |
206 self._builder_name, | |
207 None if args['perf_output_basedir'] == 'None' | |
208 else args['perf_output_basedir']) | |
209 | |
210 self.skp_dir = self._local_playback_dirs.PlaybackSkpDir() | |
211 self.playback_actual_images_dir = ( | |
212 self._local_playback_dirs.PlaybackActualImagesDir()) | |
213 self.playback_actual_summaries_dir = ( | |
214 self._local_playback_dirs.PlaybackActualSummariesDir()) | |
215 self.playback_expected_summaries_dir = ( | |
216 self._local_playback_dirs.PlaybackExpectedSummariesDir()) | |
217 | |
218 # Figure out where we are going to store performance related data. | |
219 if args['perf_output_basedir'] != 'None': | |
220 self._perf_data_dir = os.path.join(args['perf_output_basedir'], | |
221 self._builder_name, 'data') | |
222 self._perf_graphs_dir = os.path.join(args['perf_output_basedir'], | |
223 self._builder_name, 'graphs') | |
224 self._perf_range_input_dir = os.path.join( | |
225 args['perf_output_basedir'], self._builder_name, 'expectations') | |
226 else: | |
227 self._perf_data_dir = None | |
228 self._perf_graphs_dir = None | |
229 self._perf_range_input_dir = None | |
230 self._skimage_in_dir = os.path.join(os.pardir, 'skimage_in') | |
231 | |
232 self._skimage_expected_dir = os.path.join('expectations', 'skimage') | |
233 | |
234 self._skimage_out_dir = os.path.join('out', self._configuration, | |
235 'skimage_out') | |
236 | |
237 self._device_dirs = self._flavor_utils.GetDeviceDirs() | |
238 | |
239 @property | |
240 def configuration(self): | |
241 return self._configuration | |
242 | |
243 @property | |
244 def builder_name(self): | |
245 return self._builder_name | |
246 | |
247 @property | |
248 def args(self): | |
249 return self._args | |
250 | |
251 # TODO(epoger): remove default_make_flags property once all builds use ninja | |
252 @property | |
253 def default_make_flags(self): | |
254 return self._default_make_flags | |
255 | |
256 @property | |
257 def default_ninja_flags(self): | |
258 return self._default_ninja_flags | |
259 | |
260 @property | |
261 def make_flags(self): | |
262 return self._make_flags | |
263 | |
264 @property | |
265 def perf_data_dir(self): | |
266 return self._perf_data_dir | |
267 | |
268 @property | |
269 def resource_dir(self): | |
270 return self._resource_dir | |
271 | |
272 @property | |
273 def skimage_in_dir(self): | |
274 return self._skimage_in_dir | |
275 | |
276 @property | |
277 def skimage_expected_dir(self): | |
278 return self._skimage_expected_dir | |
279 | |
280 @property | |
281 def skimage_out_dir(self): | |
282 return self._skimage_out_dir | |
283 | |
284 @property | |
285 def local_playback_dirs(self): | |
286 return self._local_playback_dirs | |
287 | |
288 def _PreRun(self): | |
289 """ Optional preprocessing step defined in the BuildStepUtils. """ | |
290 self._flavor_utils.PreRun() | |
291 | |
292 def _Run(self): | |
293 """ Code to be run in a given BuildStep. No return value; throws exception | |
294 on failure. Override this method in subclasses. | |
295 """ | |
296 raise Exception('Cannot instantiate abstract BuildStep') | |
297 | |
298 def run(self): | |
299 """ Internal method used by multiprocess.Process. _Run is provided to be | |
300 overridden instead of this method to ensure that this implementation always | |
301 runs. | |
302 """ | |
303 # If a BuildStep has exceeded its allotted time, the parent process needs to | |
304 # be able to kill the BuildStep process AND any which it has spawned, | |
305 # without harming itself. On posix platforms, the terminate() method is | |
306 # insufficient; it fails to kill the subprocesses launched by this process. | |
307 # So, we use use the setpgrp() function to set a new process group for the | |
308 # BuildStep process and its children and call os.killpg() to kill the group. | |
309 if os.name == 'posix': | |
310 os.setpgrp() | |
311 try: | |
312 self._Run() | |
313 except BuildStepWarning as e: | |
314 print e | |
315 sys.exit(config.Master.retcode_warnings) | |
316 | |
317 def _WaitFunc(self, attempt): | |
318 """ Waits a number of seconds depending upon the attempt number of a | |
319 retry-able BuildStep before making the next attempt. This can be overridden | |
320 by subclasses and should be defined for attempt in [0, self.attempts - 1] | |
321 | |
322 This default implementation is exponential; we double the wait time with | |
323 each attempt, starting with a 15-second pause between the first and second | |
324 attempts. | |
325 """ | |
326 base_secs = 15 | |
327 wait = base_secs * (2 ** attempt) | |
328 print 'Retrying in %d seconds...' % wait | |
329 time.sleep(wait) | |
330 | |
331 @staticmethod | |
332 def KillBuildStep(step): | |
333 """ Kills a running BuildStep. | |
334 | |
335 step: the running BuildStep instance to kill. | |
336 """ | |
337 # On posix platforms, the terminate() method is insufficient; it fails to | |
338 # kill the subprocesses launched by this process. So, we use use the | |
339 # setpgrp() function to set a new process group for the BuildStep process | |
340 # and its children and call os.killpg() to kill the group. | |
341 if os.name == 'posix': | |
342 os.killpg(os.getpgid(step.pid), signal.SIGTERM) | |
343 elif os.name == 'nt': | |
344 subprocess.call(['taskkill', '/F', '/T', '/PID', str(step.pid)]) | |
345 else: | |
346 step.terminate() | |
347 | |
348 @staticmethod | |
349 def RunBuildStep(StepType): | |
350 """ Run a BuildStep, possibly making multiple attempts and handling | |
351 timeouts. | |
352 | |
353 StepType: class type which subclasses BuildStep, indicating what step should | |
354 be run. StepType should override _Run(). | |
355 """ | |
356 # pylint: disable=W0612 | |
357 logger = BuildStepLogger() | |
358 args = misc.ArgsToDict(sys.argv) | |
359 attempt = 0 | |
360 while True: | |
361 step = StepType(args=args) | |
362 try: | |
363 start_time = time.time() | |
364 last_written_time = start_time | |
365 # pylint: disable=W0212 | |
366 step._PreRun() | |
367 step.start() | |
368 while step.is_alive(): | |
369 current_time = time.time() | |
370 if current_time - start_time > step.timeout: | |
371 BuildStep.KillBuildStep(step) | |
372 raise BuildStepTimeout('Build step exceeded timeout of %d seconds' % | |
373 step.timeout) | |
374 elif current_time - last_written_time > step.no_output_timeout: | |
375 BuildStep.KillBuildStep(step) | |
376 raise BuildStepTimeout( | |
377 'Build step exceeded %d seconds with no output' % | |
378 step.no_output_timeout) | |
379 time.sleep(1) | |
380 if build_step_stdout_has_written.value == INT_TRUE: | |
381 last_written_time = time.time() | |
382 print 'Build Step Finished.' | |
383 if step.exitcode == 0: | |
384 return 0 | |
385 elif step.exitcode == config.Master.retcode_warnings: | |
386 # A warning is considered to be an acceptable finishing state. | |
387 return config.Master.retcode_warnings | |
388 else: | |
389 raise BuildStepFailure('Build step failed.') | |
390 except Exception: | |
391 print traceback.format_exc() | |
392 if attempt + 1 >= step.attempts: | |
393 raise | |
394 # pylint: disable=W0212 | |
395 step._WaitFunc(attempt) | |
396 attempt += 1 | |
397 print '**** %s, attempt %d ****' % (StepType.__name__, attempt + 1) | |
OLD | NEW |