| 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 |