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 |