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

Side by Side Diff: gclient_utils.py

Issue 2293013002: bot_update/gclient: kill git fetch after timeout regardless of output. (Closed)
Patch Set: better output 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
(...skipping 448 matching lines...) Expand 10 before | Expand all | Expand 10 after
459 print >> sys.stderr, ' ', zombie.pid 459 print >> sys.stderr, ' ', zombie.pid
460 460
461 461
462 class _KillTimer(object): 462 class _KillTimer(object):
463 """Timer that kills child process after certain interval since last poke or 463 """Timer that kills child process after certain interval since last poke or
464 creation. 464 creation.
465 """ 465 """
466 # TODO(tandrii): we really want to make use of subprocess42 here, and not 466 # TODO(tandrii): we really want to make use of subprocess42 here, and not
467 # re-invent the wheel, but it's too much work :( 467 # re-invent the wheel, but it's too much work :(
468 468
469 def __init__(self, timeout, child): 469 def __init__(self, timeout, child, child_info):
470 self._timeout = timeout 470 self._timeout = timeout
471 self._child = child 471 self._child = child
472 self._child_info = child_info
472 473
473 self._cv = threading.Condition() 474 self._cv = threading.Condition()
474 # All items below are protected by condition above. 475 # All items below are protected by condition above.
475 self._kill_at = None 476 self._kill_at = None
477 self._killing_attempted = False
476 self._working = True 478 self._working = True
477 self._thread = None 479 self._thread = None
478 480
479 # Start the timer immediately. 481 # Start the timer immediately.
480 if self._timeout: 482 if self._timeout:
481 self._kill_at = time.time() + self._timeout 483 self._kill_at = time.time() + self._timeout
482 self._thread = threading.Thread(name='_KillTimer', target=self._work) 484 self._thread = threading.Thread(name='_KillTimer', target=self._work)
483 self._thread.daemon = True 485 self._thread.daemon = True
484 self._thread.start() 486 self._thread.start()
485 487
488 @property
489 def killing_attempted(self):
490 return self._killing_attempted
491
486 def poke(self): 492 def poke(self):
487 if not self._timeout: 493 if not self._timeout:
488 return 494 return
489 with self._cv: 495 with self._cv:
490 self._kill_at = time.time() + self._timeout 496 self._kill_at = time.time() + self._timeout
491 497
492 def cancel(self): 498 def cancel(self):
493 with self._cv: 499 with self._cv:
494 self._working = False 500 self._working = False
495 self._cv.notifyAll() 501 self._cv.notifyAll()
496 502
497 def _work(self): 503 def _work(self):
498 if not self._timeout: 504 if not self._timeout:
499 return 505 return
500 while True: 506 while True:
501 with self._cv: 507 with self._cv:
502 if not self._working: 508 if not self._working:
503 return 509 return
504 left = self._kill_at - time.time() 510 left = self._kill_at - time.time()
505 if left > 0: 511 if left > 0:
506 self._cv.wait(timeout=left) 512 self._cv.wait(timeout=left)
507 continue 513 continue
508 try: 514 try:
509 logging.warn('killing child %s because of no output for %fs', 515 logging.warn('killing child %s %s because of no output for %fs',
510 self._child, self._timeout) 516 self._child.pid, self._child_info, self._timeout)
517 self._killing_attempted = True
511 self._child.kill() 518 self._child.kill()
512 except OSError: 519 except OSError:
513 logging.exception('failed to kill child %s', self._child) 520 logging.exception('failed to kill child %s', self._child)
514 return 521 return
515 522
516 523
517 def CheckCallAndFilter(args, stdout=None, filter_fn=None, 524 def CheckCallAndFilter(args, stdout=None, filter_fn=None,
518 print_stdout=None, call_filter_on_first_line=False, 525 print_stdout=None, call_filter_on_first_line=False,
519 retry=False, kill_timeout=None, **kwargs): 526 retry=False, kill_timeout=None, **kwargs):
520 """Runs a command and calls back a filter function if needed. 527 """Runs a command and calls back a filter function if needed.
521 528
522 Accepts all subprocess2.Popen() parameters plus: 529 Accepts all subprocess2.Popen() parameters plus:
523 print_stdout: If True, the command's stdout is forwarded to stdout. 530 print_stdout: If True, the command's stdout is forwarded to stdout.
524 filter_fn: A function taking a single string argument called with each line 531 filter_fn: A function taking a single string argument called with each line
525 of the subprocess2's output. Each line has the trailing newline 532 of the subprocess2's output. Each line has the trailing newline
526 character trimmed. 533 character trimmed.
527 stdout: Can be any bufferable output. 534 stdout: Can be any bufferable output.
528 retry: If the process exits non-zero, sleep for a brief interval and try 535 retry: If the process exits non-zero, sleep for a brief interval and try
529 again, up to RETRY_MAX times. 536 again, up to RETRY_MAX times.
530 kill_timeout: (float) if given, number of seconds after which process would 537 kill_timeout: (float) if given, number of seconds after which process would
531 be killed if there is no output. Must not be used with shell=True as 538 be killed. Must not be used with shell=True as only shell process
532 only shell process would be killed, but not processes spawned by 539 would be killed, but not processes spawned by shell.
533 shell.
534 540
535 stderr is always redirected to stdout. 541 stderr is always redirected to stdout.
536 """ 542 """
537 assert print_stdout or filter_fn 543 assert print_stdout or filter_fn
538 assert not kwargs.get('shell', False) or not kill_timeout, ( 544 assert not kwargs.get('shell', False) or not kill_timeout, (
539 'kill_timeout should not be used with shell=True') 545 'kill_timeout should not be used with shell=True')
540 stdout = stdout or sys.stdout 546 stdout = stdout or sys.stdout
541 output = cStringIO.StringIO() 547 output = cStringIO.StringIO()
542 filter_fn = filter_fn or (lambda x: None) 548 filter_fn = filter_fn or (lambda x: None)
543 549
544 sleep_interval = RETRY_INITIAL_SLEEP 550 sleep_interval = RETRY_INITIAL_SLEEP
545 run_cwd = kwargs.get('cwd', os.getcwd()) 551 run_cwd = kwargs.get('cwd', os.getcwd())
552 debug_kid_info = "'%s' in %s" % (' '.join('"%s"' % x for x in args), run_cwd)
Michael Achenbach 2016/08/30 15:02:52 Wouldn't str(args) be sufficient? Never mind. Saw
553
546 for _ in xrange(RETRY_MAX + 1): 554 for _ in xrange(RETRY_MAX + 1):
547 kid = subprocess2.Popen( 555 kid = subprocess2.Popen(
548 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT, 556 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
549 **kwargs) 557 **kwargs)
550 558
551 GClientChildren.add(kid) 559 GClientChildren.add(kid)
552 560
553 # Do a flush of stdout before we begin reading from the subprocess2's stdout 561 # Do a flush of stdout before we begin reading from the subprocess2's stdout
554 stdout.flush() 562 stdout.flush()
555 563
556 # Also, we need to forward stdout to prevent weird re-ordering of output. 564 # Also, we need to forward stdout to prevent weird re-ordering of output.
557 # This has to be done on a per byte basis to make sure it is not buffered: 565 # This has to be done on a per byte basis to make sure it is not buffered:
558 # normally buffering is done for each line, but if svn requests input, no 566 # normally buffering is done for each line, but if svn requests input, no
559 # end-of-line character is output after the prompt and it would not show up. 567 # end-of-line character is output after the prompt and it would not show up.
560 try: 568 try:
561 timeout_killer = _KillTimer(kill_timeout, kid) 569 timeout_killer = _KillTimer(kill_timeout, kid, debug_kid_info)
Michael Achenbach 2016/08/30 15:02:52 naming nit: child vs. kid - well, it's from the ol
562 in_byte = kid.stdout.read(1) 570 in_byte = kid.stdout.read(1)
563 if in_byte: 571 if in_byte:
564 if call_filter_on_first_line: 572 if call_filter_on_first_line:
565 filter_fn(None) 573 filter_fn(None)
566 in_line = '' 574 in_line = ''
567 while in_byte: 575 while in_byte:
568 timeout_killer.poke()
569 output.write(in_byte) 576 output.write(in_byte)
570 if print_stdout: 577 if print_stdout:
571 stdout.write(in_byte) 578 stdout.write(in_byte)
572 if in_byte not in ['\r', '\n']: 579 if in_byte not in ['\r', '\n']:
573 in_line += in_byte 580 in_line += in_byte
574 else: 581 else:
575 filter_fn(in_line) 582 filter_fn(in_line)
576 in_line = '' 583 in_line = ''
577 in_byte = kid.stdout.read(1) 584 in_byte = kid.stdout.read(1)
578 # Flush the rest of buffered output. This is only an issue with 585 # Flush the rest of buffered output. This is only an issue with
579 # stdout/stderr not ending with a \n. 586 # stdout/stderr not ending with a \n.
580 if len(in_line): 587 if len(in_line):
581 filter_fn(in_line) 588 filter_fn(in_line)
582 rv = kid.wait() 589 rv = kid.wait()
583 timeout_killer.cancel() 590 timeout_killer.cancel()
584 591
585 # Don't put this in a 'finally,' since the child may still run if we get 592 # Don't put this in a 'finally,' since the child may still run if we get
586 # an exception. 593 # an exception.
587 GClientChildren.remove(kid) 594 GClientChildren.remove(kid)
588 595
589 except KeyboardInterrupt: 596 except KeyboardInterrupt:
590 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args) 597 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
591 raise 598 raise
592 599
593 if rv == 0: 600 if rv == 0:
594 return output.getvalue() 601 return output.getvalue()
595 if not retry: 602 if not retry:
596 break 603 break
597 print ("WARNING: subprocess '%s' in %s failed; will retry after a short " 604 print ("WARNING: subprocess %s failed; will retry after a short nap..." %
598 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd)) 605 debug_kid_info)
606 if timeout_killer.killing_attempted:
607 print('The subprocess above was likely killed because it looked hung. '
608 'Output thus far:\n> %s' %
609 ('\n> '.join(output.getvalue().splitlines())))
599 time.sleep(sleep_interval) 610 time.sleep(sleep_interval)
600 sleep_interval *= 2 611 sleep_interval *= 2
601 raise subprocess2.CalledProcessError( 612 raise subprocess2.CalledProcessError(
602 rv, args, kwargs.get('cwd', None), None, None) 613 rv, args, kwargs.get('cwd', None), None, None)
603 614
604 615
605 class GitFilter(object): 616 class GitFilter(object):
606 """A filter_fn implementation for quieting down git output messages. 617 """A filter_fn implementation for quieting down git output messages.
607 618
608 Allows a custom function to skip certain lines (predicate), and will throttle 619 Allows a custom function to skip certain lines (predicate), and will throttle
(...skipping 674 matching lines...) Expand 10 before | Expand all | Expand 10 after
1283 # Just incase we have some ~/blah paths. 1294 # Just incase we have some ~/blah paths.
1284 target = os.path.abspath(os.path.expanduser(target)) 1295 target = os.path.abspath(os.path.expanduser(target))
1285 if os.path.isfile(target) and os.access(target, os.X_OK): 1296 if os.path.isfile(target) and os.access(target, os.X_OK):
1286 return target 1297 return target
1287 if sys.platform.startswith('win'): 1298 if sys.platform.startswith('win'):
1288 for suffix in ('.bat', '.cmd', '.exe'): 1299 for suffix in ('.bat', '.cmd', '.exe'):
1289 alt_target = target + suffix 1300 alt_target = target + suffix
1290 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK): 1301 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1291 return alt_target 1302 return alt_target
1292 return None 1303 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