| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2013-2015 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 """Entry point for fully-annotated builds. | |
| 6 | |
| 7 This script is part of the effort to move all builds to annotator-based | |
| 8 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() | |
| 9 found in scripts/master/factory/annotator_factory.py executes a single | |
| 10 AddAnnotatedScript step. That step (found in annotator_commands.py) calls | |
| 11 this script with the build- and factory-properties passed on the command | |
| 12 line. | |
| 13 | |
| 14 The main mode of operation is for factory_properties to contain a single | |
| 15 property 'recipe' whose value is the basename (without extension) of a python | |
| 16 script in one of the following locations (looked up in this order): | |
| 17 * build_internal/scripts/slave-internal/recipes | |
| 18 * build_internal/scripts/slave/recipes | |
| 19 * build/scripts/slave/recipes | |
| 20 | |
| 21 For example, these factory_properties would run the 'run_presubmit' recipe | |
| 22 located in build/scripts/slave/recipes: | |
| 23 { 'recipe': 'run_presubmit' } | |
| 24 | |
| 25 TODO(vadimsh, iannucci, luqui): The following docs are very outdated. | |
| 26 | |
| 27 Annotated_run.py will then import the recipe and expect to call a function whose | |
| 28 signature is: | |
| 29 RunSteps(api, properties) -> None. | |
| 30 | |
| 31 properties is a merged view of factory_properties with build_properties. | |
| 32 | |
| 33 Items in iterable_of_things must be one of: | |
| 34 * A step dictionary (as accepted by annotator.py) | |
| 35 * A sequence of step dictionaries | |
| 36 * A step generator | |
| 37 Iterable_of_things is also permitted to be a raw step generator. | |
| 38 | |
| 39 A step generator is called with the following protocol: | |
| 40 * The generator is initialized with 'step_history' and 'failed'. | |
| 41 * Each iteration of the generator is passed the current value of 'failed'. | |
| 42 | |
| 43 On each iteration, a step generator may yield: | |
| 44 * A single step dictionary | |
| 45 * A sequence of step dictionaries | |
| 46 * If a sequence of dictionaries is yielded, and the first step dictionary | |
| 47 does not have a 'seed_steps' key, the first step will be augmented with | |
| 48 a 'seed_steps' key containing the names of all the steps in the sequence. | |
| 49 | |
| 50 For steps yielded by the generator, if annotated_run enters the failed state, | |
| 51 it will only continue to call the generator if the generator sets the | |
| 52 'keep_going' key on the steps which it has produced. Otherwise annotated_run | |
| 53 will cease calling the generator and move on to the next item in | |
| 54 iterable_of_things. | |
| 55 | |
| 56 'step_history' is an OrderedDict of {stepname -> StepData}, always representing | |
| 57 the current history of what steps have run, what they returned, and any | |
| 58 json data they emitted. Additionally, the OrderedDict has the following | |
| 59 convenience functions defined: | |
| 60 * last_step - Returns the last step that ran or None | |
| 61 * nth_step(n) - Returns the N'th step that ran or None | |
| 62 | |
| 63 'failed' is a boolean representing if the build is in a 'failed' state. | |
| 64 """ | |
| 65 | |
| 66 import collections | |
| 67 import contextlib | |
| 68 import copy | |
| 69 import functools | |
| 70 import json | |
| 71 import os | |
| 72 import subprocess | |
| 73 import sys | |
| 74 import threading | |
| 75 import traceback | |
| 76 | |
| 77 import cStringIO | |
| 78 | |
| 79 | |
| 80 from . import loader | |
| 81 from . import recipe_api | |
| 82 from . import recipe_test_api | |
| 83 from . import util | |
| 84 | |
| 85 | |
| 86 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
| 87 | |
| 88 BUILDBOT_MAGIC_ENV = set([ | |
| 89 'BUILDBOT_BLAMELIST', | |
| 90 'BUILDBOT_BRANCH', | |
| 91 'BUILDBOT_BUILDBOTURL', | |
| 92 'BUILDBOT_BUILDERNAME', | |
| 93 'BUILDBOT_BUILDNUMBER', | |
| 94 'BUILDBOT_CLOBBER', | |
| 95 'BUILDBOT_GOT_REVISION', | |
| 96 'BUILDBOT_MASTERNAME', | |
| 97 'BUILDBOT_REVISION', | |
| 98 'BUILDBOT_SCHEDULER', | |
| 99 'BUILDBOT_SLAVENAME', | |
| 100 ]) | |
| 101 | |
| 102 ENV_WHITELIST_WIN = BUILDBOT_MAGIC_ENV | set([ | |
| 103 'APPDATA', | |
| 104 'AWS_CREDENTIAL_FILE', | |
| 105 'BOTO_CONFIG', | |
| 106 'BUILDBOT_ARCHIVE_FORCE_SSH', | |
| 107 'CHROME_HEADLESS', | |
| 108 'CHROMIUM_BUILD', | |
| 109 'COMMONPROGRAMFILES', | |
| 110 'COMMONPROGRAMFILES(X86)', | |
| 111 'COMMONPROGRAMW6432', | |
| 112 'COMSPEC', | |
| 113 'COMPUTERNAME', | |
| 114 'DBUS_SESSION_BUS_ADDRESS', | |
| 115 'DEPOT_TOOLS_GIT_BLEEDING', | |
| 116 # TODO(maruel): Remove once everyone is on 2.7.5. | |
| 117 'DEPOT_TOOLS_PYTHON_275', | |
| 118 'DXSDK_DIR', | |
| 119 'GIT_USER_AGENT', | |
| 120 'HOME', | |
| 121 'HOMEDRIVE', | |
| 122 'HOMEPATH', | |
| 123 'LOCALAPPDATA', | |
| 124 'NUMBER_OF_PROCESSORS', | |
| 125 'OS', | |
| 126 'PATH', | |
| 127 'PATHEXT', | |
| 128 'PROCESSOR_ARCHITECTURE', | |
| 129 'PROCESSOR_ARCHITEW6432', | |
| 130 'PROCESSOR_IDENTIFIER', | |
| 131 'PROGRAMFILES', | |
| 132 'PROGRAMW6432', | |
| 133 'PWD', | |
| 134 'PYTHONPATH', | |
| 135 'SYSTEMDRIVE', | |
| 136 'SYSTEMROOT', | |
| 137 'TEMP', | |
| 138 'TESTING_MASTER', | |
| 139 'TESTING_MASTER_HOST', | |
| 140 'TESTING_SLAVENAME', | |
| 141 'TMP', | |
| 142 'USERNAME', | |
| 143 'USERDOMAIN', | |
| 144 'USERPROFILE', | |
| 145 'VS100COMNTOOLS', | |
| 146 'VS110COMNTOOLS', | |
| 147 'WINDIR', | |
| 148 ]) | |
| 149 | |
| 150 ENV_WHITELIST_POSIX = BUILDBOT_MAGIC_ENV | set([ | |
| 151 'AWS_CREDENTIAL_FILE', | |
| 152 'BOTO_CONFIG', | |
| 153 'CCACHE_DIR', | |
| 154 'CHROME_ALLOCATOR', | |
| 155 'CHROME_HEADLESS', | |
| 156 'CHROME_VALGRIND_NUMCPUS', | |
| 157 'DISPLAY', | |
| 158 'DISTCC_DIR', | |
| 159 'GIT_USER_AGENT', | |
| 160 'HOME', | |
| 161 'HOSTNAME', | |
| 162 'HTTP_PROXY', | |
| 163 'http_proxy', | |
| 164 'HTTPS_PROXY', | |
| 165 'LANG', | |
| 166 'LOGNAME', | |
| 167 'PAGER', | |
| 168 'PATH', | |
| 169 'PWD', | |
| 170 'PYTHONPATH', | |
| 171 'SHELL', | |
| 172 'SSH_AGENT_PID', | |
| 173 'SSH_AUTH_SOCK', | |
| 174 'SSH_CLIENT', | |
| 175 'SSH_CONNECTION', | |
| 176 'SSH_TTY', | |
| 177 'TESTING_MASTER', | |
| 178 'TESTING_MASTER_HOST', | |
| 179 'TESTING_SLAVENAME', | |
| 180 'USER', | |
| 181 'USERNAME', | |
| 182 ]) | |
| 183 | |
| 184 | |
| 185 def _isolate_environment(): | |
| 186 """Isolate the environment to a known subset set.""" | |
| 187 if sys.platform.startswith('win'): | |
| 188 whitelist = ENV_WHITELIST_WIN | |
| 189 elif sys.platform in ('darwin', 'posix', 'linux2'): | |
| 190 whitelist = ENV_WHITELIST_POSIX | |
| 191 else: | |
| 192 print ('WARNING: unknown platform %s, not isolating environment.' % | |
| 193 sys.platform) | |
| 194 return | |
| 195 | |
| 196 for k in os.environ.keys(): | |
| 197 if k not in whitelist: | |
| 198 del os.environ[k] | |
| 199 | |
| 200 | |
| 201 class StepPresentation(object): | |
| 202 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION')) | |
| 203 | |
| 204 def __init__(self): | |
| 205 self._finalized = False | |
| 206 | |
| 207 self._logs = collections.OrderedDict() | |
| 208 self._links = collections.OrderedDict() | |
| 209 self._perf_logs = collections.OrderedDict() | |
| 210 self._status = None | |
| 211 self._step_summary_text = '' | |
| 212 self._step_text = '' | |
| 213 self._properties = {} | |
| 214 | |
| 215 # (E0202) pylint bug: http://www.logilab.org/ticket/89092 | |
| 216 @property | |
| 217 def status(self): # pylint: disable=E0202 | |
| 218 return self._status | |
| 219 | |
| 220 @status.setter | |
| 221 def status(self, val): # pylint: disable=E0202 | |
| 222 assert not self._finalized | |
| 223 assert val in self.STATUSES | |
| 224 self._status = val | |
| 225 | |
| 226 @property | |
| 227 def step_text(self): | |
| 228 return self._step_text | |
| 229 | |
| 230 @step_text.setter | |
| 231 def step_text(self, val): | |
| 232 assert not self._finalized | |
| 233 self._step_text = val | |
| 234 | |
| 235 @property | |
| 236 def step_summary_text(self): | |
| 237 return self._step_summary_text | |
| 238 | |
| 239 @step_summary_text.setter | |
| 240 def step_summary_text(self, val): | |
| 241 assert not self._finalized | |
| 242 self._step_summary_text = val | |
| 243 | |
| 244 @property | |
| 245 def logs(self): | |
| 246 if not self._finalized: | |
| 247 return self._logs | |
| 248 else: | |
| 249 return copy.deepcopy(self._logs) | |
| 250 | |
| 251 @property | |
| 252 def links(self): | |
| 253 if not self._finalized: | |
| 254 return self._links | |
| 255 else: | |
| 256 return copy.deepcopy(self._links) | |
| 257 | |
| 258 @property | |
| 259 def perf_logs(self): | |
| 260 if not self._finalized: | |
| 261 return self._perf_logs | |
| 262 else: | |
| 263 return copy.deepcopy(self._perf_logs) | |
| 264 | |
| 265 @property | |
| 266 def properties(self): # pylint: disable=E0202 | |
| 267 if not self._finalized: | |
| 268 return self._properties | |
| 269 else: | |
| 270 return copy.deepcopy(self._properties) | |
| 271 | |
| 272 @properties.setter | |
| 273 def properties(self, val): # pylint: disable=E0202 | |
| 274 assert not self._finalized | |
| 275 assert isinstance(val, dict) | |
| 276 self._properties = val | |
| 277 | |
| 278 def finalize(self, annotator_step): | |
| 279 self._finalized = True | |
| 280 if self.step_text: | |
| 281 annotator_step.step_text(self.step_text) | |
| 282 if self.step_summary_text: | |
| 283 annotator_step.step_summary_text(self.step_summary_text) | |
| 284 for name, lines in self.logs.iteritems(): | |
| 285 annotator_step.write_log_lines(name, lines) | |
| 286 for name, lines in self.perf_logs.iteritems(): | |
| 287 annotator_step.write_log_lines(name, lines, perf=True) | |
| 288 for label, url in self.links.iteritems(): | |
| 289 annotator_step.step_link(label, url) | |
| 290 status_mapping = { | |
| 291 'WARNING': annotator_step.step_warnings, | |
| 292 'FAILURE': annotator_step.step_failure, | |
| 293 'EXCEPTION': annotator_step.step_exception, | |
| 294 } | |
| 295 status_mapping.get(self.status, lambda: None)() | |
| 296 for key, value in self._properties.iteritems(): | |
| 297 annotator_step.set_build_property(key, json.dumps(value, sort_keys=True)) | |
| 298 | |
| 299 | |
| 300 class StepDataAttributeError(AttributeError): | |
| 301 """Raised when a non-existent attributed is accessed on a StepData object.""" | |
| 302 def __init__(self, step, attr): | |
| 303 self.step = step | |
| 304 self.attr = attr | |
| 305 message = ('The recipe attempted to access missing step data "%s" for step ' | |
| 306 '"%s". Please examine that step for errors.' % (attr, step)) | |
| 307 super(StepDataAttributeError, self).__init__(message) | |
| 308 | |
| 309 | |
| 310 class StepData(object): | |
| 311 def __init__(self, step, retcode): | |
| 312 self._retcode = retcode | |
| 313 self._step = step | |
| 314 | |
| 315 self._presentation = StepPresentation() | |
| 316 self.abort_reason = None | |
| 317 | |
| 318 @property | |
| 319 def step(self): | |
| 320 return copy.deepcopy(self._step) | |
| 321 | |
| 322 @property | |
| 323 def retcode(self): | |
| 324 return self._retcode | |
| 325 | |
| 326 @property | |
| 327 def presentation(self): | |
| 328 return self._presentation | |
| 329 | |
| 330 def __getattr__(self, name): | |
| 331 raise StepDataAttributeError(self._step['name'], name) | |
| 332 | |
| 333 | |
| 334 # TODO(martiniss) update comment | |
| 335 # Result of 'render_step', fed into 'step_callback'. | |
| 336 Placeholders = collections.namedtuple( | |
| 337 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin']) | |
| 338 | |
| 339 | |
| 340 def render_step(step, step_test): | |
| 341 """Renders a step so that it can be fed to annotator.py. | |
| 342 | |
| 343 Args: | |
| 344 step_test: The test data json dictionary for this step, if any. | |
| 345 Passed through unaltered to each placeholder. | |
| 346 | |
| 347 Returns any placeholder instances that were found while rendering the step. | |
| 348 """ | |
| 349 # Process 'cmd', rendering placeholders there. | |
| 350 placeholders = collections.defaultdict(lambda: collections.defaultdict(list)) | |
| 351 new_cmd = [] | |
| 352 for item in step.get('cmd', []): | |
| 353 if isinstance(item, util.Placeholder): | |
| 354 module_name, placeholder_name = item.name_pieces | |
| 355 tdata = step_test.pop_placeholder(item.name_pieces) | |
| 356 new_cmd.extend(item.render(tdata)) | |
| 357 placeholders[module_name][placeholder_name].append((item, tdata)) | |
| 358 else: | |
| 359 new_cmd.append(item) | |
| 360 step['cmd'] = new_cmd | |
| 361 | |
| 362 # Process 'stdout', 'stderr' and 'stdin' placeholders, if given. | |
| 363 stdio_placeholders = {} | |
| 364 for key in ('stdout', 'stderr', 'stdin'): | |
| 365 placeholder = step.get(key) | |
| 366 tdata = None | |
| 367 if placeholder: | |
| 368 assert isinstance(placeholder, util.Placeholder), key | |
| 369 tdata = getattr(step_test, key) | |
| 370 placeholder.render(tdata) | |
| 371 assert placeholder.backing_file | |
| 372 step[key] = placeholder.backing_file | |
| 373 stdio_placeholders[key] = (placeholder, tdata) | |
| 374 | |
| 375 return Placeholders(cmd=placeholders, **stdio_placeholders) | |
| 376 | |
| 377 | |
| 378 def get_placeholder_results(step_result, placeholders): | |
| 379 class BlankObject(object): | |
| 380 pass | |
| 381 | |
| 382 # Placeholders inside step |cmd|. | |
| 383 for module_name, pholders in placeholders.cmd.iteritems(): | |
| 384 assert not hasattr(step_result, module_name) | |
| 385 o = BlankObject() | |
| 386 setattr(step_result, module_name, o) | |
| 387 | |
| 388 for placeholder_name, items in pholders.iteritems(): | |
| 389 lst = [ph.result(step_result.presentation, td) for ph, td in items] | |
| 390 setattr(o, placeholder_name+"_all", lst) | |
| 391 setattr(o, placeholder_name, lst[0]) | |
| 392 | |
| 393 # Placeholders that are used with IO redirection. | |
| 394 for key in ('stdout', 'stderr', 'stdin'): | |
| 395 assert not hasattr(step_result, key) | |
| 396 ph, td = getattr(placeholders, key) | |
| 397 result = ph.result(step_result.presentation, td) if ph else None | |
| 398 setattr(step_result, key, result) | |
| 399 | |
| 400 | |
| 401 def get_callable_name(func): | |
| 402 """Returns __name__ of a callable, handling functools.partial types.""" | |
| 403 if isinstance(func, functools.partial): | |
| 404 return get_callable_name(func.func) | |
| 405 else: | |
| 406 return func.__name__ | |
| 407 | |
| 408 | |
| 409 # Return value of run_steps and RecipeEngine.run. | |
| 410 RecipeExecutionResult = collections.namedtuple( | |
| 411 'RecipeExecutionResult', 'status_code steps_ran') | |
| 412 | |
| 413 | |
| 414 def run_steps(properties, | |
| 415 stream, | |
| 416 universe, | |
| 417 test_data=recipe_test_api.DisabledTestData()): | |
| 418 """Returns a tuple of (status_code, steps_ran). | |
| 419 | |
| 420 Only one of these values will be set at a time. This is mainly to support the | |
| 421 testing interface used by unittests/recipes_test.py. | |
| 422 """ | |
| 423 stream.honor_zero_return_code() | |
| 424 | |
| 425 # TODO(iannucci): Stop this when blamelist becomes sane data. | |
| 426 if ('blamelist_real' in properties and | |
| 427 'blamelist' in properties): | |
| 428 properties['blamelist'] = properties['blamelist_real'] | |
| 429 del properties['blamelist_real'] | |
| 430 | |
| 431 # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by | |
| 432 # 'patch_project'. 'root' had Rietveld knowing about the implementation of | |
| 433 # the builders. 'patch_project' lets the builder (recipe) decide its own | |
| 434 # destiny. | |
| 435 properties.pop('root', None) | |
| 436 | |
| 437 # TODO(iannucci): A much better way to do this would be to dynamically | |
| 438 # detect if the mirrors are actually available during the execution of the | |
| 439 # recipe. | |
| 440 if ('use_mirror' not in properties and ( | |
| 441 'TESTING_MASTERNAME' in os.environ or | |
| 442 'TESTING_SLAVENAME' in os.environ)): | |
| 443 properties['use_mirror'] = False | |
| 444 | |
| 445 engine = RecipeEngine(stream, properties, test_data) | |
| 446 | |
| 447 # Create all API modules and top level RunSteps function. It doesn't launch | |
| 448 # any recipe code yet; RunSteps needs to be called. | |
| 449 api = None | |
| 450 with stream.step('setup_build') as s: | |
| 451 assert 'recipe' in properties # Should be ensured by get_recipe_properties. | |
| 452 recipe = properties['recipe'] | |
| 453 | |
| 454 properties_to_print = properties.copy() | |
| 455 if 'use_mirror' in properties: | |
| 456 del properties_to_print['use_mirror'] | |
| 457 | |
| 458 run_recipe_help_lines = [ | |
| 459 'To repro this locally, run the following line from a build checkout:', | |
| 460 '', | |
| 461 './scripts/tools/run_recipe.py %s --properties-file - <<EOF' % recipe, | |
| 462 repr(properties_to_print), | |
| 463 'EOF', | |
| 464 '', | |
| 465 'To run on Windows, you can put the JSON in a file and redirect the', | |
| 466 'contents of the file into run_recipe.py, with the < operator.', | |
| 467 ] | |
| 468 | |
| 469 for line in run_recipe_help_lines: | |
| 470 s.step_log_line('run_recipe', line) | |
| 471 s.step_log_end('run_recipe') | |
| 472 | |
| 473 _isolate_environment() | |
| 474 | |
| 475 # Find and load the recipe to run. | |
| 476 try: | |
| 477 recipe_module = universe.load_recipe(recipe) | |
| 478 stream.emit('Running recipe with %s' % (properties,)) | |
| 479 prop_defs = recipe_module.PROPERTIES | |
| 480 | |
| 481 api = loader.create_recipe_api(recipe_module.LOADED_DEPS, | |
| 482 engine, | |
| 483 test_data) | |
| 484 | |
| 485 s.step_text('<br/>running recipe: "%s"' % recipe) | |
| 486 except loader.NoSuchRecipe as e: | |
| 487 s.step_text('<br/>recipe not found: %s' % e) | |
| 488 s.step_failure() | |
| 489 return RecipeExecutionResult(2, None) | |
| 490 | |
| 491 # Run the steps emitted by a recipe via the engine, emitting annotations | |
| 492 # into |stream| along the way. | |
| 493 return engine.run(recipe_module.RunSteps, api, prop_defs) | |
| 494 | |
| 495 | |
| 496 def _merge_envs(original, override): | |
| 497 """Merges two environments. | |
| 498 | |
| 499 Returns a new environment dict with entries from |override| overwriting | |
| 500 corresponding entries in |original|. Keys whose value is None will completely | |
| 501 remove the environment variable. Values can contain %(KEY)s strings, which | |
| 502 will be substituted with the values from the original (useful for amending, as | |
| 503 opposed to overwriting, variables like PATH). | |
| 504 """ | |
| 505 result = original.copy() | |
| 506 if not override: | |
| 507 return result | |
| 508 for k, v in override.items(): | |
| 509 if v is None: | |
| 510 if k in result: | |
| 511 del result[k] | |
| 512 else: | |
| 513 result[str(k)] = str(v) % original | |
| 514 return result | |
| 515 | |
| 516 | |
| 517 def _print_step(step, env, stream): | |
| 518 """Prints the step command and relevant metadata. | |
| 519 | |
| 520 Intended to be similar to the information that Buildbot prints at the | |
| 521 beginning of each non-annotator step. | |
| 522 """ | |
| 523 step_info_lines = [] | |
| 524 step_info_lines.append(' '.join(step['cmd'])) | |
| 525 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd())) | |
| 526 for key, value in sorted(step.items()): | |
| 527 if value is not None: | |
| 528 if callable(value): | |
| 529 # This prevents functions from showing up as: | |
| 530 # '<function foo at 0x7f523ec7a410>' | |
| 531 # which is tricky to test. | |
| 532 value = value.__name__+'(...)' | |
| 533 step_info_lines.append(' %s: %s' % (key, value)) | |
| 534 step_info_lines.append('full environment:') | |
| 535 for key, value in sorted(env.items()): | |
| 536 step_info_lines.append(' %s: %s' % (key, value)) | |
| 537 step_info_lines.append('') | |
| 538 stream.emit('\n'.join(step_info_lines)) | |
| 539 | |
| 540 | |
| 541 @contextlib.contextmanager | |
| 542 def _modify_lookup_path(path): | |
| 543 """Places the specified path into os.environ. | |
| 544 | |
| 545 Necessary because subprocess.Popen uses os.environ to perform lookup on the | |
| 546 supplied command, and only uses the |env| kwarg for modifying the environment | |
| 547 of the child process. | |
| 548 """ | |
| 549 saved_path = os.environ['PATH'] | |
| 550 try: | |
| 551 if path is not None: | |
| 552 os.environ['PATH'] = path | |
| 553 yield | |
| 554 finally: | |
| 555 os.environ['PATH'] = saved_path | |
| 556 | |
| 557 | |
| 558 def _normalize_change(change): | |
| 559 assert isinstance(change, dict), 'Change is not a dict' | |
| 560 change = change.copy() | |
| 561 | |
| 562 # Convert when_timestamp to UNIX timestamp. | |
| 563 when = change.get('when_timestamp') | |
| 564 if isinstance(when, datetime.datetime): | |
| 565 when = calendar.timegm(when.utctimetuple()) | |
| 566 change['when_timestamp'] = when | |
| 567 | |
| 568 return change | |
| 569 | |
| 570 | |
| 571 def _trigger_builds(step, trigger_specs): | |
| 572 assert trigger_specs is not None | |
| 573 for trig in trigger_specs: | |
| 574 builder_name = trig.get('builder_name') | |
| 575 if not builder_name: | |
| 576 raise ValueError('Trigger spec: builder_name is not set') | |
| 577 | |
| 578 changes = trig.get('buildbot_changes', []) | |
| 579 assert isinstance(changes, list), 'buildbot_changes must be a list' | |
| 580 changes = map(_normalize_change, changes) | |
| 581 | |
| 582 step.step_trigger(json.dumps({ | |
| 583 'builderNames': [builder_name], | |
| 584 'bucket': trig.get('bucket'), | |
| 585 'changes': changes, | |
| 586 'properties': trig.get('properties'), | |
| 587 }, sort_keys=True)) | |
| 588 | |
| 589 | |
| 590 def _run_annotated_step( | |
| 591 stream, name, cmd, cwd=None, env=None, allow_subannotations=False, | |
| 592 trigger_specs=None, nest_level=0, **kwargs): | |
| 593 """Runs a single step. | |
| 594 | |
| 595 Context: | |
| 596 stream: StructuredAnnotationStream to use to emit step | |
| 597 | |
| 598 Step parameters: | |
| 599 name: name of the step, will appear in buildbots waterfall | |
| 600 cmd: command to run, list of one or more strings | |
| 601 cwd: absolute path to working directory for the command | |
| 602 env: dict with overrides for environment variables | |
| 603 allow_subannotations: if True, lets the step emit its own annotations | |
| 604 trigger_specs: a list of trigger specifications, which are dict with keys: | |
| 605 properties: a dict of properties. | |
| 606 Buildbot requires buildername property. | |
| 607 | |
| 608 Known kwargs: | |
| 609 stdout: Path to a file to put step stdout into. If used, stdout won't appear | |
| 610 in annotator's stdout (and |allow_subannotations| is ignored). | |
| 611 stderr: Path to a file to put step stderr into. If used, stderr won't appear | |
| 612 in annotator's stderr. | |
| 613 stdin: Path to a file to read step stdin from. | |
| 614 | |
| 615 Returns the returncode of the step. | |
| 616 """ | |
| 617 if isinstance(cmd, basestring): | |
| 618 cmd = (cmd,) | |
| 619 cmd = map(str, cmd) | |
| 620 | |
| 621 # For error reporting. | |
| 622 step_dict = kwargs.copy() | |
| 623 step_dict.update({ | |
| 624 'name': name, | |
| 625 'cmd': cmd, | |
| 626 'cwd': cwd, | |
| 627 'env': env, | |
| 628 'allow_subannotations': allow_subannotations, | |
| 629 }) | |
| 630 step_env = _merge_envs(os.environ, env) | |
| 631 | |
| 632 step_annotation = stream.step(name) | |
| 633 step_annotation.step_started() | |
| 634 | |
| 635 if nest_level: | |
| 636 step_annotation.step_nest_level(nest_level) | |
| 637 | |
| 638 _print_step(step_dict, step_env, stream) | |
| 639 returncode = 0 | |
| 640 if cmd: | |
| 641 try: | |
| 642 # Open file handles for IO redirection based on file names in step_dict. | |
| 643 fhandles = { | |
| 644 'stdout': subprocess.PIPE, | |
| 645 'stderr': subprocess.PIPE, | |
| 646 'stdin': None, | |
| 647 } | |
| 648 for key in fhandles: | |
| 649 if key in step_dict: | |
| 650 fhandles[key] = open(step_dict[key], | |
| 651 'rb' if key == 'stdin' else 'wb') | |
| 652 | |
| 653 if sys.platform.startswith('win'): | |
| 654 # Windows has a bad habit of opening a dialog when a console program | |
| 655 # crashes, rather than just letting it crash. Therefore, when a program | |
| 656 # crashes on Windows, we don't find out until the build step times out. | |
| 657 # This code prevents the dialog from appearing, so that we find out | |
| 658 # immediately and don't waste time waiting for a user to close the | |
| 659 # dialog. | |
| 660 import ctypes | |
| 661 # SetErrorMode(SEM_NOGPFAULTERRORBOX). For more information, see: | |
| 662 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx | |
| 663 ctypes.windll.kernel32.SetErrorMode(0x0002) | |
| 664 # CREATE_NO_WINDOW. For more information, see: | |
| 665 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx | |
| 666 creationflags = 0x8000000 | |
| 667 else: | |
| 668 creationflags = 0 | |
| 669 | |
| 670 with _modify_lookup_path(step_env.get('PATH')): | |
| 671 proc = subprocess.Popen( | |
| 672 cmd, | |
| 673 env=step_env, | |
| 674 cwd=cwd, | |
| 675 universal_newlines=True, | |
| 676 creationflags=creationflags, | |
| 677 **fhandles) | |
| 678 | |
| 679 # Safe to close file handles now that subprocess has inherited them. | |
| 680 for handle in fhandles.itervalues(): | |
| 681 if isinstance(handle, file): | |
| 682 handle.close() | |
| 683 | |
| 684 outlock = threading.Lock() | |
| 685 def filter_lines(lock, allow_subannotations, inhandle, outhandle): | |
| 686 while True: | |
| 687 line = inhandle.readline() | |
| 688 if not line: | |
| 689 break | |
| 690 lock.acquire() | |
| 691 try: | |
| 692 if not allow_subannotations and line.startswith('@@@'): | |
| 693 outhandle.write('!') | |
| 694 outhandle.write(line) | |
| 695 outhandle.flush() | |
| 696 finally: | |
| 697 lock.release() | |
| 698 | |
| 699 # Pump piped stdio through filter_lines. IO going to files on disk is | |
| 700 # not filtered. | |
| 701 threads = [] | |
| 702 for key in ('stdout', 'stderr'): | |
| 703 if fhandles[key] == subprocess.PIPE: | |
| 704 inhandle = getattr(proc, key) | |
| 705 outhandle = getattr(sys, key) | |
| 706 threads.append(threading.Thread( | |
| 707 target=filter_lines, | |
| 708 args=(outlock, allow_subannotations, inhandle, outhandle))) | |
| 709 | |
| 710 for th in threads: | |
| 711 th.start() | |
| 712 proc.wait() | |
| 713 for th in threads: | |
| 714 th.join() | |
| 715 returncode = proc.returncode | |
| 716 except OSError: | |
| 717 # File wasn't found, error will be reported to stream when the exception | |
| 718 # crosses the context manager. | |
| 719 step_annotation.step_exception_occured(*sys.exc_info()) | |
| 720 raise | |
| 721 | |
| 722 # TODO(martiniss) move logic into own module? | |
| 723 if trigger_specs: | |
| 724 _trigger_builds(step_annotation, trigger_specs) | |
| 725 | |
| 726 return step_annotation, returncode | |
| 727 | |
| 728 class RecipeEngine(object): | |
| 729 """ | |
| 730 Knows how to execute steps emitted by a recipe, holds global state such as | |
| 731 step history and build properties. Each recipe module API has a reference to | |
| 732 this object. | |
| 733 | |
| 734 Recipe modules that are aware of the engine: | |
| 735 * properties - uses engine.properties. | |
| 736 * step_history - uses engine.step_history. | |
| 737 * step - uses engine.create_step(...). | |
| 738 | |
| 739 """ | |
| 740 def __init__(self, stream, properties, test_data): | |
| 741 self._stream = stream | |
| 742 self._properties = properties | |
| 743 self._test_data = test_data | |
| 744 self._step_history = collections.OrderedDict() | |
| 745 | |
| 746 self._previous_step_annotation = None | |
| 747 self._previous_step_result = None | |
| 748 self._api = None | |
| 749 | |
| 750 @property | |
| 751 def properties(self): | |
| 752 return self._properties | |
| 753 | |
| 754 @property | |
| 755 def previous_step_result(self): | |
| 756 """Allows api.step to get the active result from any context.""" | |
| 757 return self._previous_step_result | |
| 758 | |
| 759 def _emit_results(self): | |
| 760 """Internal helper used to emit results.""" | |
| 761 annotation = self._previous_step_annotation | |
| 762 step_result = self._previous_step_result | |
| 763 | |
| 764 self._previous_step_annotation = None | |
| 765 self._previous_step_result = None | |
| 766 | |
| 767 if not annotation or not step_result: | |
| 768 return | |
| 769 | |
| 770 step_result.presentation.finalize(annotation) | |
| 771 if self._test_data.enabled: | |
| 772 val = annotation.stream.getvalue() | |
| 773 lines = filter(None, val.splitlines()) | |
| 774 if lines: | |
| 775 # note that '~' sorts after 'z' so that this will be last on each | |
| 776 # step. also use _step to get access to the mutable step | |
| 777 # dictionary. | |
| 778 # pylint: disable=w0212 | |
| 779 step_result._step['~followup_annotations'] = lines | |
| 780 annotation.step_ended() | |
| 781 | |
| 782 def run_step(self, step): | |
| 783 """ | |
| 784 Runs a step. | |
| 785 | |
| 786 Args: | |
| 787 step: The step to run. | |
| 788 | |
| 789 Returns: | |
| 790 A StepData object containing the result of running the step. | |
| 791 """ | |
| 792 ok_ret = step.pop('ok_ret') | |
| 793 infra_step = step.pop('infra_step') | |
| 794 nest_level = step.pop('step_nest_level') | |
| 795 | |
| 796 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) | |
| 797 step_test = self._test_data.pop_step_test_data(step['name'], | |
| 798 test_data_fn) | |
| 799 placeholders = render_step(step, step_test) | |
| 800 | |
| 801 self._step_history[step['name']] = step | |
| 802 self._emit_results() | |
| 803 | |
| 804 step_result = None | |
| 805 | |
| 806 if not self._test_data.enabled: | |
| 807 self._previous_step_annotation, retcode = _run_annotated_step( | |
| 808 self._stream, nest_level=nest_level, **step) | |
| 809 | |
| 810 step_result = StepData(step, retcode) | |
| 811 self._stream.step_cursor(step['name']) | |
| 812 else: | |
| 813 self._previous_step_annotation = annotation = self._stream.step( | |
| 814 step['name']) | |
| 815 annotation.step_started() | |
| 816 try: | |
| 817 annotation.stream = cStringIO.StringIO() | |
| 818 if nest_level: | |
| 819 annotation.step_nest_level(nest_level) | |
| 820 | |
| 821 step_result = StepData(step, step_test.retcode) | |
| 822 except OSError: | |
| 823 exc_type, exc_value, exc_tb = sys.exc_info() | |
| 824 trace = traceback.format_exception(exc_type, exc_value, exc_tb) | |
| 825 trace_lines = ''.join(trace).split('\n') | |
| 826 annotation.write_log_lines('exception', filter(None, trace_lines)) | |
| 827 annotation.step_exception() | |
| 828 | |
| 829 get_placeholder_results(step_result, placeholders) | |
| 830 self._previous_step_result = step_result | |
| 831 | |
| 832 if step_result.retcode in ok_ret: | |
| 833 step_result.presentation.status = 'SUCCESS' | |
| 834 return step_result | |
| 835 else: | |
| 836 if not infra_step: | |
| 837 state = 'FAILURE' | |
| 838 exc = recipe_api.StepFailure | |
| 839 else: | |
| 840 state = 'EXCEPTION' | |
| 841 exc = recipe_api.InfraFailure | |
| 842 | |
| 843 step_result.presentation.status = state | |
| 844 if step_test.enabled: | |
| 845 # To avoid cluttering the expectations, don't emit this in testmode. | |
| 846 self._previous_step_annotation.emit( | |
| 847 'step returned non-zero exit code: %d' % step_result.retcode) | |
| 848 | |
| 849 raise exc(step['name'], step_result) | |
| 850 | |
| 851 | |
| 852 def run(self, steps_function, api, prop_defs): | |
| 853 """Run a recipe represented by top level RunSteps function. | |
| 854 | |
| 855 This function blocks until recipe finishes. | |
| 856 | |
| 857 Args: | |
| 858 steps_function: function that runs the steps. | |
| 859 api: The api, with loaded module dependencies. | |
| 860 Used by the some special modules. | |
| 861 prop_defs: Property definitions for this recipe. | |
| 862 | |
| 863 Returns: | |
| 864 RecipeExecutionResult with status code and list of steps ran. | |
| 865 """ | |
| 866 self._api = api | |
| 867 retcode = None | |
| 868 final_result = None | |
| 869 | |
| 870 try: | |
| 871 try: | |
| 872 retcode = loader.invoke_with_properties( | |
| 873 steps_function, api._engine.properties, prop_defs, api=api) | |
| 874 assert retcode is None, ( | |
| 875 "Non-None return from RunSteps is not supported yet") | |
| 876 | |
| 877 assert not self._test_data.enabled or not self._test_data.step_data, ( | |
| 878 "Unconsumed test data! %s" % (self._test_data.step_data,)) | |
| 879 finally: | |
| 880 self._emit_results() | |
| 881 except recipe_api.StepFailure as f: | |
| 882 retcode = f.retcode or 1 | |
| 883 final_result = { | |
| 884 "name": "$final_result", | |
| 885 "reason": f.reason, | |
| 886 "status_code": retcode | |
| 887 } | |
| 888 except StepDataAttributeError as ex: | |
| 889 unexpected_exception = self._test_data.is_unexpected_exception(ex) | |
| 890 | |
| 891 retcode = -1 | |
| 892 final_result = { | |
| 893 "name": "$final_result", | |
| 894 "reason": "Invalid Step Data Access: %r" % ex, | |
| 895 "status_code": retcode | |
| 896 } | |
| 897 | |
| 898 with self._stream.step('Invalid Step Data Access') as s: | |
| 899 s.step_exception() | |
| 900 s.write_log_lines('exception', traceback.format_exc().splitlines()) | |
| 901 | |
| 902 if unexpected_exception: | |
| 903 raise | |
| 904 | |
| 905 except Exception as ex: | |
| 906 unexpected_exception = self._test_data.is_unexpected_exception(ex) | |
| 907 | |
| 908 retcode = -1 | |
| 909 final_result = { | |
| 910 "name": "$final_result", | |
| 911 "reason": "Uncaught Exception: %r" % ex, | |
| 912 "status_code": retcode | |
| 913 } | |
| 914 | |
| 915 with self._stream.step('Uncaught Exception') as s: | |
| 916 s.step_exception() | |
| 917 s.write_log_lines('exception', traceback.format_exc().splitlines()) | |
| 918 | |
| 919 if unexpected_exception: | |
| 920 raise | |
| 921 | |
| 922 if final_result is not None: | |
| 923 self._step_history[final_result['name']] = final_result | |
| 924 | |
| 925 return RecipeExecutionResult(retcode, self._step_history) | |
| 926 | |
| 927 def create_step(self, step): # pylint: disable=R0201 | |
| 928 """Called by step module to instantiate a new step. | |
| 929 | |
| 930 Args: | |
| 931 step: ConfigGroup object with information about the step, see | |
| 932 recipe_modules/step/config.py. | |
| 933 | |
| 934 Returns: | |
| 935 Opaque engine specific object that is understood by 'run_steps' method. | |
| 936 """ | |
| 937 return step.as_jsonish() | |
| 938 | |
| 939 | |
| OLD | NEW |