Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(78)

Side by Side Diff: gclient_utils.py

Issue 2300433003: gclient_utils: provide subproces42 based CheckCallAndFilter. (Closed)
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/gclient_utils_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
OLDNEW
« no previous file with comments | « no previous file | tests/gclient_utils_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698