Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import calendar | 5 import calendar |
| 6 import collections | 6 import collections |
| 7 import contextlib | 7 import contextlib |
| 8 import datetime | 8 import datetime |
| 9 import itertools | 9 import itertools |
| 10 import json | 10 import json |
| 11 import os | 11 import os |
| 12 import pprint | 12 import pprint |
| 13 import re | 13 import re |
| 14 import StringIO | 14 import StringIO |
| 15 import sys | 15 import sys |
| 16 import tempfile | 16 import tempfile |
| 17 import time | 17 import time |
| 18 import traceback | 18 import traceback |
| 19 | 19 |
| 20 from . import recipe_api | 20 from . import recipe_api |
| 21 from . import recipe_test_api | 21 from . import recipe_test_api |
| 22 from . import stream | 22 from . import stream |
| 23 from . import types | 23 from . import types |
| 24 from . import util | 24 from . import util |
| 25 | 25 |
| 26 from libs import luci_context as luci_ctx | |
| 27 | |
| 26 import subprocess42 | 28 import subprocess42 |
| 27 | 29 |
| 28 | 30 |
| 29 if sys.platform == "win32": | 31 if sys.platform == "win32": |
| 30 # Windows has a bad habit of opening a dialog when a console program | 32 # Windows has a bad habit of opening a dialog when a console program |
| 31 # crashes, rather than just letting it crash. Therefore, when a | 33 # crashes, rather than just letting it crash. Therefore, when a |
| 32 # program crashes on Windows, we don't find out until the build step | 34 # program crashes on Windows, we don't find out until the build step |
| 33 # times out. This code prevents the dialog from appearing, so that we | 35 # times out. This code prevents the dialog from appearing, so that we |
| 34 # find out immediately and don't waste time waiting for a user to | 36 # find out immediately and don't waste time waiting for a user to |
| 35 # close the dialog. | 37 # close the dialog. |
| (...skipping 168 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 204 'stderr': step_stream, | 206 'stderr': step_stream, |
| 205 'stdin': None, | 207 'stdin': None, |
| 206 } | 208 } |
| 207 for key in handles: | 209 for key in handles: |
| 208 fileName = getattr(step_config, key) | 210 fileName = getattr(step_config, key) |
| 209 if fileName: | 211 if fileName: |
| 210 handles[key] = open(fileName, 'rb' if key == 'stdin' else 'wb') | 212 handles[key] = open(fileName, 'rb' if key == 'stdin' else 'wb') |
| 211 # The subprocess will inherit and close these handles. | 213 # The subprocess will inherit and close these handles. |
| 212 retcode = self._run_cmd( | 214 retcode = self._run_cmd( |
| 213 cmd=step_config.cmd, timeout=step_config.timeout, handles=handles, | 215 cmd=step_config.cmd, timeout=step_config.timeout, handles=handles, |
| 214 env=step_env, cwd=step_config.cwd) | 216 env=step_env, luci_context=rendered_step.config.luci_context, |
| 217 cwd=step_config.cwd) | |
| 215 except subprocess42.TimeoutExpired as e: | 218 except subprocess42.TimeoutExpired as e: |
| 216 #FIXME: Make this respect the infra_step argument | 219 #FIXME: Make this respect the infra_step argument |
| 217 step_stream.set_step_status('FAILURE') | 220 step_stream.set_step_status('FAILURE') |
| 218 raise recipe_api.StepTimeout(step_config.name, e.timeout) | 221 raise recipe_api.StepTimeout(step_config.name, e.timeout) |
| 219 except OSError: | 222 except OSError: |
| 220 with step_stream.new_log_stream('exception') as l: | 223 with step_stream.new_log_stream('exception') as l: |
| 221 trace = traceback.format_exc().splitlines() | 224 trace = traceback.format_exc().splitlines() |
| 222 for line in trace: | 225 for line in trace: |
| 223 l.write_line(line) | 226 l.write_line(line) |
| 224 step_stream.set_step_status('EXCEPTION') | 227 step_stream.set_step_status('EXCEPTION') |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 300 yield 'in dir %s:' % (cwd,) | 303 yield 'in dir %s:' % (cwd,) |
| 301 for key, value in sorted(step.config._asdict().items()): | 304 for key, value in sorted(step.config._asdict().items()): |
| 302 if value is not None: | 305 if value is not None: |
| 303 yield ' %s: %s' % (key, self._render_step_value(value)) | 306 yield ' %s: %s' % (key, self._render_step_value(value)) |
| 304 yield 'full environment:' | 307 yield 'full environment:' |
| 305 for key, value in sorted(env.items()): | 308 for key, value in sorted(env.items()): |
| 306 yield ' %s: %s' % (key, value) | 309 yield ' %s: %s' % (key, value) |
| 307 yield '' | 310 yield '' |
| 308 stream.output_iter(step_stream, gen_step_prelude()) | 311 stream.output_iter(step_stream, gen_step_prelude()) |
| 309 | 312 |
| 310 def _run_cmd(self, cmd, timeout, handles, env, cwd): | 313 def _run_cmd(self, cmd, timeout, handles, env, luci_context, cwd): |
| 311 """Runs cmd (subprocess-style). | 314 """Runs cmd (subprocess-style). |
| 312 | 315 |
| 313 Args: | 316 Args: |
| 314 cmd: a subprocess-style command list, with command first then args. | 317 cmd: a subprocess-style command list, with command first then args. |
| 315 handles: A dictionary from ('stdin', 'stdout', 'stderr'), each value | 318 handles: A dictionary from ('stdin', 'stdout', 'stderr'), each value |
| 316 being *either* a stream.StreamEngine.Stream or a python file object | 319 being *either* a stream.StreamEngine.Stream or a python file object |
| 317 to direct that subprocess's filehandle to. | 320 to direct that subprocess's filehandle to. |
| 321 luci_context: full value of LUCI_CONTEXT dict. | |
|
iannucci
2017/08/04 18:55:50
Maybe "entire intended value of the LUCI_CONTEXT d
| |
| 318 env: the full environment to run the command in -- this is passed | 322 env: the full environment to run the command in -- this is passed |
| 319 unaltered to subprocess.Popen. | 323 unaltered to subprocess.Popen. |
| 320 cwd: the working directory of the command. | 324 cwd: the working directory of the command. |
| 321 """ | 325 """ |
| 322 fhandles = {} | 326 fhandles = {} |
| 323 | 327 |
| 324 # If we are given StreamEngine.Streams, map them to PIPE for subprocess. | 328 # If we are given StreamEngine.Streams, map them to PIPE for subprocess. |
| 325 # We will manually forward them to their corresponding stream. | 329 # We will manually forward them to their corresponding stream. |
| 326 for key in ('stdout', 'stderr'): | 330 for key in ('stdout', 'stderr'): |
| 327 handle = handles.get(key) | 331 handle = handles.get(key) |
| 328 if isinstance(handle, stream.StreamEngine.Stream): | 332 if isinstance(handle, stream.StreamEngine.Stream): |
| 329 fhandles[key] = subprocess42.PIPE | 333 fhandles[key] = subprocess42.PIPE |
| 330 else: | 334 else: |
| 331 fhandles[key] = handle | 335 fhandles[key] = handle |
| 332 | 336 |
| 333 # stdin must be a real handle, if it exists | 337 # stdin must be a real handle, if it exists |
| 334 fhandles['stdin'] = handles.get('stdin') | 338 fhandles['stdin'] = handles.get('stdin') |
| 335 | 339 |
| 336 with _modify_lookup_path(env.get('PATH')): | 340 with luci_ctx.replace(luci_context) as path: |
|
Vadim Sh.
2017/07/29 00:40:55
no changes here except lines 340-343
Rietveld is t
| |
| 337 proc = subprocess42.Popen( | 341 if path: |
|
iannucci
2017/08/04 18:55:50
nit: I would rename path to luci_ctx_path or somet
| |
| 338 cmd, | 342 env = env.copy() |
| 339 env=env, | 343 env['LUCI_CONTEXT'] = path |
| 340 cwd=cwd, | |
| 341 detached=True, | |
| 342 universal_newlines=True, | |
| 343 **fhandles) | |
| 344 | 344 |
| 345 # Safe to close file handles now that subprocess has inherited them. | 345 with _modify_lookup_path(env.get('PATH')): |
| 346 for handle in fhandles.itervalues(): | 346 proc = subprocess42.Popen( |
| 347 if isinstance(handle, file): | 347 cmd, |
| 348 handle.close() | 348 env=env, |
| 349 cwd=cwd, | |
| 350 detached=True, | |
| 351 universal_newlines=True, | |
| 352 **fhandles) | |
| 349 | 353 |
| 350 outstreams = {} | 354 # Safe to close file handles now that subprocess has inherited them. |
| 351 linebufs = {} | 355 for handle in fhandles.itervalues(): |
| 356 if isinstance(handle, file): | |
| 357 handle.close() | |
| 352 | 358 |
| 353 for key in ('stdout', 'stderr'): | 359 outstreams = {} |
| 354 handle = handles.get(key) | 360 linebufs = {} |
| 355 if isinstance(handle, stream.StreamEngine.Stream): | |
| 356 outstreams[key] = handle | |
| 357 linebufs[key] = _streamingLinebuf() | |
| 358 | 361 |
| 359 if linebufs: | 362 for key in ('stdout', 'stderr'): |
| 360 # manually check the timeout, because we poll | 363 handle = handles.get(key) |
| 361 start_time = time.time() | 364 if isinstance(handle, stream.StreamEngine.Stream): |
| 362 for pipe, data in proc.yield_any(timeout=1): | 365 outstreams[key] = handle |
| 363 if timeout and time.time() - start_time > timeout: | 366 linebufs[key] = _streamingLinebuf() |
| 364 # Don't know the name of the step, so raise this and it'll get caught | |
| 365 raise subprocess42.TimeoutExpired(cmd, timeout) | |
| 366 | 367 |
| 367 if pipe is None: | 368 if linebufs: |
| 368 continue | 369 # manually check the timeout, because we poll |
| 369 buf = linebufs.get(pipe) | 370 start_time = time.time() |
| 370 if not buf: | 371 for pipe, data in proc.yield_any(timeout=1): |
| 371 continue | 372 if timeout and time.time() - start_time > timeout: |
| 372 buf.ingest(data) | 373 # Don't know the name of the step, so raise this and it'll get caugh t |
| 373 for line in buf.get_buffered(): | 374 raise subprocess42.TimeoutExpired(cmd, timeout) |
| 374 outstreams[pipe].write_line(line) | 375 |
| 375 else: | 376 if pipe is None: |
| 376 proc.wait(timeout) | 377 continue |
| 378 buf = linebufs.get(pipe) | |
| 379 if not buf: | |
| 380 continue | |
| 381 buf.ingest(data) | |
| 382 for line in buf.get_buffered(): | |
| 383 outstreams[pipe].write_line(line) | |
| 384 else: | |
| 385 proc.wait(timeout) | |
| 377 | 386 |
| 378 return proc.returncode | 387 return proc.returncode |
| 379 | 388 |
| 380 def _trigger_builds(self, step, trigger_specs): | 389 def _trigger_builds(self, step, trigger_specs): |
| 381 assert trigger_specs is not None | 390 assert trigger_specs is not None |
| 382 for trig in trigger_specs: | 391 for trig in trigger_specs: |
| 383 builder_name = trig.builder_name | 392 builder_name = trig.builder_name |
| 384 if not builder_name: | 393 if not builder_name: |
| 385 raise ValueError('Trigger spec: builder_name is not set') | 394 raise ValueError('Trigger spec: builder_name is not set') |
| 386 | 395 |
| (...skipping 441 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 828 supplied command, and only uses the |env| kwarg for modifying the environment | 837 supplied command, and only uses the |env| kwarg for modifying the environment |
| 829 of the child process. | 838 of the child process. |
| 830 """ | 839 """ |
| 831 saved_path = os.environ['PATH'] | 840 saved_path = os.environ['PATH'] |
| 832 try: | 841 try: |
| 833 if path is not None: | 842 if path is not None: |
| 834 os.environ['PATH'] = path | 843 os.environ['PATH'] = path |
| 835 yield | 844 yield |
| 836 finally: | 845 finally: |
| 837 os.environ['PATH'] = saved_path | 846 os.environ['PATH'] = saved_path |
| OLD | NEW |