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 |