OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Generic utils.""" | 5 """Generic utils.""" |
6 | 6 |
7 import codecs | 7 import codecs |
8 import cStringIO | 8 import cStringIO |
9 import datetime | 9 import datetime |
10 import logging | 10 import logging |
11 import os | 11 import os |
12 import pipes | 12 import pipes |
13 import platform | 13 import platform |
14 import Queue | 14 import Queue |
15 import re | 15 import re |
16 import stat | 16 import stat |
17 import subprocess | 17 import subprocess |
18 import sys | 18 import sys |
19 import tempfile | 19 import tempfile |
20 import threading | 20 import threading |
21 import time | 21 import time |
22 import urlparse | 22 import urlparse |
23 | 23 |
| 24 # TODO(tandrii): migrate users to subprocess42. |
24 import subprocess2 | 25 import subprocess2 |
| 26 import subprocess42 |
25 | 27 |
26 | 28 |
27 RETRY_MAX = 3 | 29 RETRY_MAX = 3 |
28 RETRY_INITIAL_SLEEP = 0.5 | 30 RETRY_INITIAL_SLEEP = 0.5 |
29 START = datetime.datetime.now() | 31 START = datetime.datetime.now() |
30 | 32 |
31 | 33 |
32 _WARNINGS = [] | 34 _WARNINGS = [] |
33 | 35 |
34 | 36 |
(...skipping 227 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
262 | 264 |
263 def CommandToStr(args): | 265 def CommandToStr(args): |
264 """Converts an arg list into a shell escaped string.""" | 266 """Converts an arg list into a shell escaped string.""" |
265 return ' '.join(pipes.quote(arg) for arg in args) | 267 return ' '.join(pipes.quote(arg) for arg in args) |
266 | 268 |
267 | 269 |
268 def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs): | 270 def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs): |
269 """Adds 'header' support to CheckCallAndFilter. | 271 """Adds 'header' support to CheckCallAndFilter. |
270 | 272 |
271 If |always| is True, a message indicating what is being done | 273 If |always| is True, a message indicating what is being done |
272 is printed to stdout all the time even if not output is generated. Otherwise | 274 is printed to stdout all the time even if no output is generated. Otherwise |
273 the message header is printed only if the call generated any ouput. | 275 the message header is printed only if the call generated any ouput. |
| 276 |
| 277 If use_v42=True is given, uses subprocess42 through CheckCallAndFilter42. |
274 """ | 278 """ |
275 stdout = kwargs.setdefault('stdout', sys.stdout) | 279 stdout = kwargs.setdefault('stdout', sys.stdout) |
276 if header is None: | 280 if header is None: |
277 header = "\n________ running '%s' in '%s'\n" % ( | 281 header = "\n________ running '%s' in '%s'\n" % ( |
278 ' '.join(args), kwargs.get('cwd', '.')) | 282 ' '.join(args), kwargs.get('cwd', '.')) |
279 | 283 |
280 if always: | 284 if always: |
281 stdout.write(header) | 285 stdout.write(header) |
282 else: | 286 else: |
283 filter_fn = kwargs.get('filter_fn') | 287 filter_fn = kwargs.get('filter_fn') |
284 def filter_msg(line): | 288 def filter_msg(line): |
285 if line is None: | 289 if line is None: |
286 stdout.write(header) | 290 stdout.write(header) |
287 elif filter_fn: | 291 elif filter_fn: |
288 filter_fn(line) | 292 filter_fn(line) |
289 kwargs['filter_fn'] = filter_msg | 293 kwargs['filter_fn'] = filter_msg |
290 kwargs['call_filter_on_first_line'] = True | 294 kwargs['call_filter_on_first_line'] = True |
291 # Obviously. | 295 # Obviously. |
292 kwargs.setdefault('print_stdout', True) | 296 kwargs.setdefault('print_stdout', True) |
| 297 if kwargs.pop('use_v42', False): |
| 298 return CheckCallAndFilter42(args, **kwargs) |
293 return CheckCallAndFilter(args, **kwargs) | 299 return CheckCallAndFilter(args, **kwargs) |
294 | 300 |
295 | 301 |
296 class Wrapper(object): | 302 class Wrapper(object): |
297 """Wraps an object, acting as a transparent proxy for all properties by | 303 """Wraps an object, acting as a transparent proxy for all properties by |
298 default. | 304 default. |
299 """ | 305 """ |
300 def __init__(self, wrapped): | 306 def __init__(self, wrapped): |
301 self._wrapped = wrapped | 307 self._wrapped = wrapped |
302 | 308 |
(...skipping 211 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
514 try: | 520 try: |
515 logging.warn('killing child %s %s because of no output for %fs', | 521 logging.warn('killing child %s %s because of no output for %fs', |
516 self._child.pid, self._child_info, self._timeout) | 522 self._child.pid, self._child_info, self._timeout) |
517 self._killing_attempted = True | 523 self._killing_attempted = True |
518 self._child.kill() | 524 self._child.kill() |
519 except OSError: | 525 except OSError: |
520 logging.exception('failed to kill child %s', self._child) | 526 logging.exception('failed to kill child %s', self._child) |
521 return | 527 return |
522 | 528 |
523 | 529 |
| 530 #TODO(tandrii): all users of this should be converted to CheckCallAndFilter42. |
524 def CheckCallAndFilter(args, stdout=None, filter_fn=None, | 531 def CheckCallAndFilter(args, stdout=None, filter_fn=None, |
525 print_stdout=None, call_filter_on_first_line=False, | 532 print_stdout=None, call_filter_on_first_line=False, |
526 retry=False, kill_timeout=None, **kwargs): | 533 retry=False, kill_timeout=None, **kwargs): |
527 """Runs a command and calls back a filter function if needed. | 534 """Runs a command and calls back a filter function if needed. |
528 | 535 |
529 Accepts all subprocess2.Popen() parameters plus: | 536 Accepts all subprocess2.Popen() parameters plus: |
530 print_stdout: If True, the command's stdout is forwarded to stdout. | 537 print_stdout: If True, the command's stdout is forwarded to stdout. |
531 filter_fn: A function taking a single string argument called with each line | 538 filter_fn: A function taking a single string argument called with each line |
532 of the subprocess2's output. Each line has the trailing newline | 539 of the subprocess2's output. Each line has the trailing newline |
533 character trimmed. | 540 character trimmed. |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
606 if timeout_killer.killing_attempted: | 613 if timeout_killer.killing_attempted: |
607 print('The subprocess above was likely killed because it looked hung. ' | 614 print('The subprocess above was likely killed because it looked hung. ' |
608 'Output thus far:\n> %s' % | 615 'Output thus far:\n> %s' % |
609 ('\n> '.join(output.getvalue().splitlines()))) | 616 ('\n> '.join(output.getvalue().splitlines()))) |
610 time.sleep(sleep_interval) | 617 time.sleep(sleep_interval) |
611 sleep_interval *= 2 | 618 sleep_interval *= 2 |
612 raise subprocess2.CalledProcessError( | 619 raise subprocess2.CalledProcessError( |
613 rv, args, kwargs.get('cwd', None), None, None) | 620 rv, args, kwargs.get('cwd', None), None, None) |
614 | 621 |
615 | 622 |
| 623 def CheckCallAndFilter42(args, stdout=None, filter_fn=None, |
| 624 print_stdout=None, call_filter_on_first_line=False, |
| 625 retry=False, kill_timeout=None, **kwargs): |
| 626 """Runs a command and calls back a filter function if needed. |
| 627 |
| 628 Accepts all subprocess2.Popen() parameters plus: |
| 629 print_stdout: If True, the command's stdout is forwarded to stdout. |
| 630 filter_fn: A function taking a single string argument called with each line |
| 631 of the subprocess2's output. Each line has the trailing newline |
| 632 character trimmed. |
| 633 stdout: Can be any bufferable output. |
| 634 retry: If the process exits non-zero, sleep for a brief interval and try |
| 635 again, up to RETRY_MAX times. |
| 636 kill_timeout: (float) if given, number of seconds after which spawned |
| 637 process would be killed. This applies to each retry separately. |
| 638 |
| 639 stderr is always redirected to stdout. |
| 640 """ |
| 641 assert print_stdout or filter_fn |
| 642 if kill_timeout: |
| 643 assert isinstance(kill_timeout, (int, float)), kill_timeout |
| 644 |
| 645 stdout = stdout or sys.stdout |
| 646 output = cStringIO.StringIO() |
| 647 filter_fn = filter_fn or (lambda x: None) |
| 648 |
| 649 sleep_interval = RETRY_INITIAL_SLEEP |
| 650 run_cwd = kwargs.get('cwd', os.getcwd()) |
| 651 debug_child_info = "'%s' in %s" % (' '.join('"%s"' % x for x in args), run_cwd
) |
| 652 |
| 653 for _ in xrange(RETRY_MAX + 1): |
| 654 child = subprocess42.Popen( |
| 655 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT, |
| 656 **kwargs) |
| 657 |
| 658 GClientChildren.add(child) |
| 659 |
| 660 # Do a flush of stdout before we begin reading from the subprocess42's |
| 661 # stdout. |
| 662 stdout.flush() |
| 663 |
| 664 # Also, we need to forward stdout to prevent weird re-ordering of output. |
| 665 try: |
| 666 first_line = True |
| 667 in_line = '' |
| 668 # Don't block for more than 1 second. |
| 669 for pipe, data in child.yield_any(maxsize=256, timeout=1): |
| 670 if pipe is None: |
| 671 # No new data. |
| 672 if kill_timeout and child.duration() > kill_timeout: |
| 673 print('ERROR: killing process %s running for %.0fs (timeout: %.0fs)' |
| 674 % (debug_child_info, child.duration(), kill_timeout)) |
| 675 child.kill() |
| 676 continue |
| 677 |
| 678 assert pipe == 'stdout' |
| 679 if first_line: |
| 680 first_line = False |
| 681 if call_filter_on_first_line: |
| 682 filter_fn(None) |
| 683 |
| 684 output.write(data) |
| 685 if print_stdout: |
| 686 stdout.write(data) |
| 687 for byte in data: |
| 688 if byte not in ['\r', '\n']: |
| 689 in_line += byte |
| 690 else: |
| 691 filter_fn(in_line) |
| 692 in_line = '' |
| 693 # Flush the rest of buffered output. This is only an issue with |
| 694 # stdout/stderr not ending with a \n. |
| 695 if len(in_line): |
| 696 filter_fn(in_line) |
| 697 rv = child.wait() |
| 698 |
| 699 # Don't put this in a 'finally,' since the child may still run if we get |
| 700 # an exception. |
| 701 GClientChildren.remove(child) |
| 702 |
| 703 except KeyboardInterrupt: |
| 704 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args) |
| 705 raise |
| 706 |
| 707 if rv == 0: |
| 708 return output.getvalue() |
| 709 if not retry: |
| 710 break |
| 711 print ("WARNING: subprocess %s failed; will retry after a short nap..." % |
| 712 debug_child_info) |
| 713 time.sleep(sleep_interval) |
| 714 sleep_interval *= 2 |
| 715 # TODO(tandrii): change this to subprocess.CalledProcessError, |
| 716 # beacuse subprocess42 doesn't have a class unlike subprocess2. |
| 717 raise subprocess2.CalledProcessError( |
| 718 rv, args, kwargs.get('cwd', None), None, None) |
| 719 |
| 720 |
616 class GitFilter(object): | 721 class GitFilter(object): |
617 """A filter_fn implementation for quieting down git output messages. | 722 """A filter_fn implementation for quieting down git output messages. |
618 | 723 |
619 Allows a custom function to skip certain lines (predicate), and will throttle | 724 Allows a custom function to skip certain lines (predicate), and will throttle |
620 the output of percentage completed lines to only output every X seconds. | 725 the output of percentage completed lines to only output every X seconds. |
621 """ | 726 """ |
622 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*') | 727 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*') |
623 | 728 |
624 def __init__(self, time_throttle=0, predicate=None, out_fh=None): | 729 def __init__(self, time_throttle=0, predicate=None, out_fh=None): |
625 """ | 730 """ |
(...skipping 668 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1294 # Just incase we have some ~/blah paths. | 1399 # Just incase we have some ~/blah paths. |
1295 target = os.path.abspath(os.path.expanduser(target)) | 1400 target = os.path.abspath(os.path.expanduser(target)) |
1296 if os.path.isfile(target) and os.access(target, os.X_OK): | 1401 if os.path.isfile(target) and os.access(target, os.X_OK): |
1297 return target | 1402 return target |
1298 if sys.platform.startswith('win'): | 1403 if sys.platform.startswith('win'): |
1299 for suffix in ('.bat', '.cmd', '.exe'): | 1404 for suffix in ('.bat', '.cmd', '.exe'): |
1300 alt_target = target + suffix | 1405 alt_target = target + suffix |
1301 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK): | 1406 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK): |
1302 return alt_target | 1407 return alt_target |
1303 return None | 1408 return None |
OLD | NEW |