Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Reads a .isolated, creates a tree of hardlinks and runs the test. | 6 """Reads a .isolated, creates a tree of hardlinks and runs the test. |
| 7 | 7 |
| 8 Keeps a local cache. | 8 Keeps a local cache. |
| 9 """ | 9 """ |
| 10 | 10 |
| 11 import cookielib | |
| 12 import ctypes | 11 import ctypes |
| 13 import functools | 12 import functools |
| 14 import hashlib | 13 import hashlib |
| 15 import httplib | 14 import httplib |
| 16 import itertools | |
| 17 import json | 15 import json |
| 18 import logging | 16 import logging |
| 19 import math | |
| 20 import optparse | 17 import optparse |
| 21 import os | 18 import os |
| 22 import Queue | 19 import Queue |
| 23 import random | 20 import random |
| 24 import re | 21 import re |
| 25 import shutil | 22 import shutil |
| 26 import socket | |
| 27 import ssl | |
| 28 import stat | 23 import stat |
| 29 import subprocess | 24 import subprocess |
| 30 import sys | 25 import sys |
| 31 import tempfile | 26 import tempfile |
| 32 import threading | |
| 33 import time | 27 import time |
| 34 import urllib | |
| 35 import urllib2 | |
| 36 import urlparse | |
| 37 import zlib | 28 import zlib |
| 38 | 29 |
| 39 from third_party.rietveld import upload | |
| 40 from third_party.depot_tools import fix_encoding | 30 from third_party.depot_tools import fix_encoding |
| 41 | 31 |
| 42 from utils import lru | 32 from utils import lru |
| 33 from utils import net | |
| 43 from utils import threading_utils | 34 from utils import threading_utils |
| 44 from utils import tools | 35 from utils import tools |
| 45 from utils import zip_package | 36 from utils import zip_package |
| 46 | 37 |
| 47 | 38 |
| 48 # Hack out upload logging.info() | |
| 49 upload.logging = logging.getLogger('upload') | |
| 50 # Mac pylint choke on this line. | |
| 51 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 | |
| 52 | |
| 53 | |
| 54 # Absolute path to this file (can be None if running from zip on Mac). | 39 # Absolute path to this file (can be None if running from zip on Mac). |
| 55 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None | 40 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None |
| 56 | 41 |
| 57 # Directory that contains this file (might be inside zip package). | 42 # Directory that contains this file (might be inside zip package). |
| 58 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None | 43 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None |
| 59 | 44 |
| 60 # Directory that contains currently running script file. | 45 # Directory that contains currently running script file. |
| 61 MAIN_DIR = os.path.dirname(os.path.abspath(zip_package.get_main_script_path())) | 46 MAIN_DIR = os.path.dirname(os.path.abspath(zip_package.get_main_script_path())) |
| 62 | 47 |
| 63 # Types of action accepted by link_file(). | 48 # Types of action accepted by link_file(). |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 81 # The delay (in seconds) to wait between logging statements when retrieving | 66 # The delay (in seconds) to wait between logging statements when retrieving |
| 82 # the required files. This is intended to let the user (or buildbot) know that | 67 # the required files. This is intended to let the user (or buildbot) know that |
| 83 # the program is still running. | 68 # the program is still running. |
| 84 DELAY_BETWEEN_UPDATES_IN_SECS = 30 | 69 DELAY_BETWEEN_UPDATES_IN_SECS = 30 |
| 85 | 70 |
| 86 # Maximum expected delay (in seconds) between successive file fetches | 71 # Maximum expected delay (in seconds) between successive file fetches |
| 87 # in run_tha_test. If it takes longer than that, a deadlock might be happening | 72 # in run_tha_test. If it takes longer than that, a deadlock might be happening |
| 88 # and all stack frames for all threads are dumped to log. | 73 # and all stack frames for all threads are dumped to log. |
| 89 DEADLOCK_TIMEOUT = 5 * 60 | 74 DEADLOCK_TIMEOUT = 5 * 60 |
| 90 | 75 |
| 91 # The name of the key to store the count of url attempts. | |
| 92 COUNT_KEY = 'UrlOpenAttempt' | |
| 93 | |
| 94 # Default maximum number of attempts to trying opening a url before aborting. | |
| 95 URL_OPEN_MAX_ATTEMPTS = 30 | |
| 96 # Default timeout when retrying. | |
| 97 URL_OPEN_TIMEOUT = 6*60. | |
| 98 | |
| 99 # Read timeout in seconds for downloads from isolate storage. If there's no | 76 # Read timeout in seconds for downloads from isolate storage. If there's no |
| 100 # response from the server within this timeout whole download will be aborted. | 77 # response from the server within this timeout whole download will be aborted. |
| 101 DOWNLOAD_READ_TIMEOUT = 60 | 78 DOWNLOAD_READ_TIMEOUT = 60 |
| 102 | 79 |
| 103 # Global (for now) map: server URL (http://example.com) -> HttpService instance. | |
| 104 # Used by get_http_service to cache HttpService instances. | |
| 105 _http_services = {} | |
| 106 _http_services_lock = threading.Lock() | |
| 107 | 80 |
| 108 # Used by get_flavor(). | 81 # Used by get_flavor(). |
| 109 FLAVOR_MAPPING = { | 82 FLAVOR_MAPPING = { |
| 110 'cygwin': 'win', | 83 'cygwin': 'win', |
| 111 'win32': 'win', | 84 'win32': 'win', |
| 112 'darwin': 'mac', | 85 'darwin': 'mac', |
| 113 'sunos5': 'solaris', | 86 'sunos5': 'solaris', |
| 114 'freebsd7': 'freebsd', | 87 'freebsd7': 'freebsd', |
| 115 'freebsd8': 'freebsd', | 88 'freebsd8': 'freebsd', |
| 116 } | 89 } |
| 117 | 90 |
| 118 | 91 |
| 119 class ConfigError(ValueError): | 92 class ConfigError(ValueError): |
| 120 """Generic failure to load a .isolated file.""" | 93 """Generic failure to load a .isolated file.""" |
| 121 pass | 94 pass |
| 122 | 95 |
| 123 | 96 |
| 124 class MappingError(OSError): | 97 class MappingError(OSError): |
| 125 """Failed to recreate the tree.""" | 98 """Failed to recreate the tree.""" |
| 126 pass | 99 pass |
| 127 | 100 |
| 128 | 101 |
| 129 class TimeoutError(IOError): | |
| 130 """Timeout while reading HTTP response.""" | |
| 131 | |
| 132 def __init__(self, inner_exc=None): | |
| 133 super(TimeoutError, self).__init__(str(inner_exc or 'Timeout')) | |
| 134 self.inner_exc = inner_exc | |
| 135 | |
| 136 | |
| 137 def get_as_zip_package(executable=True): | 102 def get_as_zip_package(executable=True): |
| 138 """Returns ZipPackage with this module and all its dependencies. | 103 """Returns ZipPackage with this module and all its dependencies. |
| 139 | 104 |
| 140 If |executable| is True will store run_isolated.py as __main__.py so that | 105 If |executable| is True will store run_isolated.py as __main__.py so that |
| 141 zip package is directly executable be python. | 106 zip package is directly executable be python. |
| 142 """ | 107 """ |
| 143 # Building a zip package when running from another zip package is | 108 # Building a zip package when running from another zip package is |
| 144 # unsupported and probably unneeded. | 109 # unsupported and probably unneeded. |
| 145 assert not zip_package.is_zipped_module(sys.modules[__name__]) | 110 assert not zip_package.is_zipped_module(sys.modules[__name__]) |
| 146 assert THIS_FILE_PATH | 111 assert THIS_FILE_PATH |
| (...skipping 217 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 364 raise ConfigError( | 329 raise ConfigError( |
| 365 'Expected \'os\' to be \'%s\' but got \'%s\'' % | 330 'Expected \'os\' to be \'%s\' but got \'%s\'' % |
| 366 (expected_value, value)) | 331 (expected_value, value)) |
| 367 | 332 |
| 368 else: | 333 else: |
| 369 raise ConfigError('Unknown key %s' % key) | 334 raise ConfigError('Unknown key %s' % key) |
| 370 | 335 |
| 371 return data | 336 return data |
| 372 | 337 |
| 373 | 338 |
| 374 def url_open(url, **kwargs): | |
| 375 """Attempts to open the given url multiple times. | |
| 376 | |
| 377 |data| can be either: | |
| 378 -None for a GET request | |
| 379 -str for pre-encoded data | |
| 380 -list for data to be encoded | |
| 381 -dict for data to be encoded (COUNT_KEY will be added in this case) | |
| 382 | |
| 383 Returns HttpResponse object, where the response may be read from, or None | |
| 384 if it was unable to connect. | |
| 385 """ | |
| 386 urlhost, urlpath = split_server_request_url(url) | |
| 387 service = get_http_service(urlhost) | |
| 388 return service.request(urlpath, **kwargs) | |
| 389 | |
| 390 | |
| 391 def url_read(url, **kwargs): | |
| 392 """Attempts to open the given url multiple times and read all data from it. | |
| 393 | |
| 394 Accepts same arguments as url_open function. | |
| 395 | |
| 396 Returns all data read or None if it was unable to connect or read the data. | |
| 397 """ | |
| 398 response = url_open(url, **kwargs) | |
| 399 if not response: | |
| 400 return None | |
| 401 try: | |
| 402 return response.read() | |
| 403 except TimeoutError: | |
| 404 return None | |
| 405 | |
| 406 | |
| 407 def split_server_request_url(url): | |
| 408 """Splits the url into scheme+netloc and path+params+query+fragment.""" | |
| 409 url_parts = list(urlparse.urlparse(url)) | |
| 410 urlhost = '%s://%s' % (url_parts[0], url_parts[1]) | |
| 411 urlpath = urlparse.urlunparse(['', ''] + url_parts[2:]) | |
| 412 return urlhost, urlpath | |
| 413 | |
| 414 | |
| 415 def get_http_service(urlhost): | |
| 416 """Returns existing or creates new instance of HttpService that can send | |
| 417 requests to given base urlhost. | |
| 418 """ | |
| 419 # Ensure consistency. | |
| 420 urlhost = str(urlhost).lower().rstrip('/') | |
| 421 with _http_services_lock: | |
| 422 service = _http_services.get(urlhost) | |
| 423 if not service: | |
| 424 service = AppEngineService(urlhost) | |
| 425 _http_services[urlhost] = service | |
| 426 return service | |
| 427 | |
| 428 | |
| 429 class HttpService(object): | |
| 430 """Base class for a class that provides an API to HTTP based service: | |
| 431 - Provides 'request' method. | |
| 432 - Supports automatic request retries. | |
| 433 - Supports persistent cookies. | |
| 434 - Thread safe. | |
| 435 """ | |
| 436 | |
| 437 # File to use to store all auth cookies. | |
| 438 COOKIE_FILE = os.path.join(os.path.expanduser('~'), '.isolated_cookies') | |
| 439 | |
| 440 # CookieJar reused by all services + lock that protects its instantiation. | |
| 441 _cookie_jar = None | |
| 442 _cookie_jar_lock = threading.Lock() | |
| 443 | |
| 444 def __init__(self, urlhost): | |
| 445 self.urlhost = urlhost | |
| 446 self.cookie_jar = self.load_cookie_jar() | |
| 447 self.opener = self.create_url_opener() | |
| 448 | |
| 449 def authenticate(self): # pylint: disable=R0201 | |
| 450 """Called when HTTP server asks client to authenticate. | |
| 451 Can be implemented in subclasses. | |
| 452 """ | |
| 453 return False | |
| 454 | |
| 455 @staticmethod | |
| 456 def load_cookie_jar(): | |
| 457 """Returns global CoookieJar object that stores cookies in the file.""" | |
| 458 with HttpService._cookie_jar_lock: | |
| 459 if HttpService._cookie_jar is not None: | |
| 460 return HttpService._cookie_jar | |
| 461 jar = ThreadSafeCookieJar(HttpService.COOKIE_FILE) | |
| 462 jar.load() | |
| 463 HttpService._cookie_jar = jar | |
| 464 return jar | |
| 465 | |
| 466 @staticmethod | |
| 467 def save_cookie_jar(): | |
| 468 """Called when cookie jar needs to be flushed to disk.""" | |
| 469 with HttpService._cookie_jar_lock: | |
| 470 if HttpService._cookie_jar is not None: | |
| 471 HttpService._cookie_jar.save() | |
| 472 | |
| 473 def create_url_opener(self): # pylint: disable=R0201 | |
| 474 """Returns OpenerDirector that will be used when sending requests. | |
| 475 Can be reimplemented in subclasses.""" | |
| 476 return urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookie_jar)) | |
| 477 | |
| 478 def request(self, urlpath, data=None, content_type=None, **kwargs): | |
| 479 """Attempts to open the given url multiple times. | |
| 480 | |
| 481 |urlpath| is relative to the server root, i.e. '/some/request?param=1'. | |
| 482 | |
| 483 |data| can be either: | |
| 484 -None for a GET request | |
| 485 -str for pre-encoded data | |
| 486 -list for data to be encoded | |
| 487 -dict for data to be encoded (COUNT_KEY will be added in this case) | |
| 488 | |
| 489 Returns a file-like object, where the response may be read from, or None | |
| 490 if it was unable to connect. | |
| 491 """ | |
| 492 assert urlpath and urlpath[0] == '/' | |
| 493 | |
| 494 if isinstance(data, dict) and COUNT_KEY in data: | |
| 495 logging.error('%s already existed in the data passed into UlrOpen. It ' | |
| 496 'would be overwritten. Aborting UrlOpen', COUNT_KEY) | |
| 497 return None | |
| 498 | |
| 499 method = 'GET' if data is None else 'POST' | |
| 500 assert not ((method != 'POST') and content_type), ( | |
| 501 'Can\'t use content_type on GET') | |
| 502 | |
| 503 def make_request(extra): | |
| 504 """Returns a urllib2.Request instance for this specific retry.""" | |
| 505 if isinstance(data, str) or data is None: | |
| 506 payload = data | |
| 507 else: | |
| 508 if isinstance(data, dict): | |
| 509 payload = data.items() | |
| 510 else: | |
| 511 payload = data[:] | |
| 512 payload.extend(extra.iteritems()) | |
| 513 payload = urllib.urlencode(payload) | |
| 514 new_url = urlparse.urljoin(self.urlhost, urlpath[1:]) | |
| 515 if isinstance(data, str) or data is None: | |
| 516 # In these cases, add the extra parameter to the query part of the url. | |
| 517 url_parts = list(urlparse.urlparse(new_url)) | |
| 518 # Append the query parameter. | |
| 519 if url_parts[4] and extra: | |
| 520 url_parts[4] += '&' | |
| 521 url_parts[4] += urllib.urlencode(extra) | |
| 522 new_url = urlparse.urlunparse(url_parts) | |
| 523 request = urllib2.Request(new_url, data=payload) | |
| 524 if payload is not None: | |
| 525 if content_type: | |
| 526 request.add_header('Content-Type', content_type) | |
| 527 request.add_header('Content-Length', len(payload)) | |
| 528 return request | |
| 529 | |
| 530 return self._retry_loop(make_request, **kwargs) | |
| 531 | |
| 532 def _retry_loop( | |
| 533 self, | |
| 534 make_request, | |
| 535 max_attempts=URL_OPEN_MAX_ATTEMPTS, | |
| 536 retry_404=False, | |
| 537 retry_50x=True, | |
| 538 timeout=URL_OPEN_TIMEOUT, | |
| 539 read_timeout=None): | |
| 540 """Runs internal request-retry loop. | |
| 541 | |
| 542 - Optionally retries HTTP 404 and 50x. | |
| 543 - Retries up to |max_attempts| times. If None or 0, there's no limit in the | |
| 544 number of retries. | |
| 545 - Retries up to |timeout| duration in seconds. If None or 0, there's no | |
| 546 limit in the time taken to do retries. | |
| 547 - If both |max_attempts| and |timeout| are None or 0, this functions retries | |
| 548 indefinitely. | |
| 549 | |
| 550 If |read_timeout| is not None will configure underlying socket to | |
| 551 raise TimeoutError exception whenever there's no response from the server | |
| 552 for more than |read_timeout| seconds. It can happen during any read | |
| 553 operation so once you pass non-None |read_timeout| be prepared to handle | |
| 554 these exceptions in subsequent reads from the stream. | |
| 555 """ | |
| 556 authenticated = False | |
| 557 last_error = None | |
| 558 attempt = 0 | |
| 559 start = self._now() | |
| 560 for attempt in itertools.count(): | |
| 561 if max_attempts and attempt >= max_attempts: | |
| 562 # Too many attempts. | |
| 563 break | |
| 564 if timeout and (self._now() - start) >= timeout: | |
| 565 # Retried for too long. | |
| 566 break | |
| 567 extra = {COUNT_KEY: attempt} if attempt else {} | |
| 568 request = make_request(extra) | |
| 569 try: | |
| 570 url_response = self._url_open(request, timeout=read_timeout) | |
| 571 logging.debug('url_open(%s) succeeded', request.get_full_url()) | |
| 572 return HttpResponse(url_response, request.get_full_url()) | |
| 573 except urllib2.HTTPError as e: | |
| 574 # Unauthorized. Ask to authenticate and then try again. | |
| 575 if e.code in (401, 403): | |
| 576 # Try to authenticate only once. If it doesn't help, then server does | |
| 577 # not support app engine authentication. | |
| 578 logging.error( | |
| 579 'Authentication is required for %s on attempt %d.\n%s', | |
| 580 request.get_full_url(), attempt, | |
| 581 self._format_exception(e, verbose=True)) | |
| 582 if not authenticated and self.authenticate(): | |
| 583 authenticated = True | |
| 584 # Do not sleep. | |
| 585 continue | |
| 586 # If authentication failed, return. | |
| 587 logging.error( | |
| 588 'Unable to authenticate to %s.\n%s', | |
| 589 request.get_full_url(), self._format_exception(e, verbose=True)) | |
| 590 return None | |
| 591 | |
| 592 if ((e.code < 500 and not (retry_404 and e.code == 404)) or | |
| 593 (e.code >= 500 and not retry_50x)): | |
| 594 # This HTTPError means we reached the server and there was a problem | |
| 595 # with the request, so don't retry. | |
| 596 logging.error( | |
| 597 'Able to connect to %s but an exception was thrown.\n%s', | |
| 598 request.get_full_url(), self._format_exception(e, verbose=True)) | |
| 599 return None | |
| 600 | |
| 601 # The HTTPError was due to a server error, so retry the attempt. | |
| 602 logging.warning('Able to connect to %s on attempt %d.\n%s', | |
| 603 request.get_full_url(), attempt, | |
| 604 self._format_exception(e)) | |
| 605 last_error = e | |
| 606 | |
| 607 except (urllib2.URLError, httplib.HTTPException, | |
| 608 socket.timeout, ssl.SSLError) as e: | |
| 609 logging.warning('Unable to open url %s on attempt %d.\n%s', | |
| 610 request.get_full_url(), attempt, | |
| 611 self._format_exception(e)) | |
| 612 last_error = e | |
| 613 | |
| 614 # Only sleep if we are going to try again. | |
| 615 if max_attempts and attempt != max_attempts: | |
| 616 remaining = None | |
| 617 if timeout: | |
| 618 remaining = timeout - (self._now() - start) | |
| 619 if remaining <= 0: | |
| 620 break | |
| 621 self.sleep_before_retry(attempt, remaining) | |
| 622 | |
| 623 logging.error('Unable to open given url, %s, after %d attempts.\n%s', | |
| 624 request.get_full_url(), max_attempts, | |
| 625 self._format_exception(last_error, verbose=True)) | |
| 626 return None | |
| 627 | |
| 628 def _url_open(self, request, timeout=None): | |
| 629 """Low level method to execute urllib2.Request's. | |
| 630 | |
| 631 To be mocked in tests. | |
| 632 """ | |
| 633 if timeout is not None: | |
| 634 return self.opener.open(request, timeout=timeout) | |
| 635 else: | |
| 636 # Leave original default value for |timeout|. It's nontrivial. | |
| 637 return self.opener.open(request) | |
| 638 | |
| 639 @staticmethod | |
| 640 def _now(): | |
| 641 """To be mocked in tests.""" | |
| 642 return time.time() | |
| 643 | |
| 644 @staticmethod | |
| 645 def calculate_sleep_before_retry(attempt, max_duration): | |
| 646 # Maximum sleeping time. We're hammering a cloud-distributed service, it'll | |
| 647 # survive. | |
| 648 MAX_SLEEP = 10. | |
| 649 # random.random() returns [0.0, 1.0). Starts with relatively short waiting | |
| 650 # time by starting with 1.5/2+1.5^-1 median offset. | |
| 651 duration = (random.random() * 1.5) + math.pow(1.5, (attempt - 1)) | |
| 652 assert duration > 0.1 | |
| 653 duration = min(MAX_SLEEP, duration) | |
| 654 if max_duration: | |
| 655 duration = min(max_duration, duration) | |
| 656 return duration | |
| 657 | |
| 658 @classmethod | |
| 659 def sleep_before_retry(cls, attempt, max_duration): | |
| 660 """Sleeps for some amount of time when retrying the request. | |
| 661 | |
| 662 To be mocked in tests. | |
| 663 """ | |
| 664 time.sleep(cls.calculate_sleep_before_retry(attempt, max_duration)) | |
| 665 | |
| 666 @staticmethod | |
| 667 def _format_exception(exc, verbose=False): | |
| 668 """Given an instance of some exception raised by urlopen returns human | |
| 669 readable piece of text with detailed information about the error. | |
| 670 """ | |
| 671 out = ['Exception: %s' % (exc,)] | |
| 672 if verbose: | |
| 673 if isinstance(exc, urllib2.HTTPError): | |
| 674 out.append('-' * 10) | |
| 675 if exc.hdrs: | |
| 676 for header, value in exc.hdrs.items(): | |
| 677 if not header.startswith('x-'): | |
| 678 out.append('%s: %s' % (header.capitalize(), value)) | |
| 679 out.append('') | |
| 680 out.append(exc.read() or '<empty body>') | |
| 681 out.append('-' * 10) | |
| 682 return '\n'.join(out) | |
| 683 | |
| 684 | |
| 685 class HttpResponse(object): | |
| 686 """Response from HttpService.""" | |
| 687 | |
| 688 def __init__(self, url_response, url): | |
| 689 self._url_response = url_response | |
| 690 self._url = url | |
| 691 self._read = 0 | |
| 692 | |
| 693 @property | |
| 694 def content_length(self): | |
| 695 """Total length to the response or None if not known in advance.""" | |
| 696 length = self._url_response.headers.get('Content-Length') | |
| 697 return int(length) if length is not None else None | |
| 698 | |
| 699 def read(self, size=None): | |
| 700 """Reads up to |size| bytes from the stream and returns them. | |
| 701 | |
| 702 If |size| is None reads all available bytes. | |
| 703 | |
| 704 Raises TimeoutError on read timeout. | |
| 705 """ | |
| 706 try: | |
| 707 data = self._url_response.read(size) | |
| 708 self._read += len(data) | |
| 709 return data | |
| 710 except (socket.timeout, ssl.SSLError) as e: | |
| 711 logging.error('Timeout while reading from %s, read %d of %s: %s', | |
| 712 self._url, self._read, self.content_length, e) | |
| 713 raise TimeoutError(e) | |
| 714 | |
| 715 | |
| 716 class AppEngineService(HttpService): | |
| 717 """This class implements authentication support for | |
| 718 an app engine based services. | |
| 719 """ | |
| 720 | |
| 721 # This lock ensures that user won't be confused with multiple concurrent | |
| 722 # login prompts. | |
| 723 _auth_lock = threading.Lock() | |
| 724 | |
| 725 def __init__(self, urlhost, email=None, password=None): | |
| 726 super(AppEngineService, self).__init__(urlhost) | |
| 727 self.email = email | |
| 728 self.password = password | |
| 729 self._keyring = None | |
| 730 | |
| 731 def authenticate(self): | |
| 732 """Authenticates in the app engine application. | |
| 733 Returns True on success. | |
| 734 """ | |
| 735 if not upload: | |
| 736 logging.error('\'upload\' module is missing, ' | |
| 737 'app engine authentication is disabled.') | |
| 738 return False | |
| 739 cookie_jar = self.cookie_jar | |
| 740 save_cookie_jar = self.save_cookie_jar | |
| 741 # RPC server that uses AuthenticationSupport's cookie jar. | |
| 742 class AuthServer(upload.AbstractRpcServer): | |
| 743 def _GetOpener(self): | |
| 744 # Authentication code needs to know about 302 response. | |
| 745 # So make OpenerDirector without HTTPRedirectHandler. | |
| 746 opener = urllib2.OpenerDirector() | |
| 747 opener.add_handler(urllib2.ProxyHandler()) | |
| 748 opener.add_handler(urllib2.UnknownHandler()) | |
| 749 opener.add_handler(urllib2.HTTPHandler()) | |
| 750 opener.add_handler(urllib2.HTTPDefaultErrorHandler()) | |
| 751 opener.add_handler(urllib2.HTTPSHandler()) | |
| 752 opener.add_handler(urllib2.HTTPErrorProcessor()) | |
| 753 opener.add_handler(urllib2.HTTPCookieProcessor(cookie_jar)) | |
| 754 return opener | |
| 755 def PerformAuthentication(self): | |
| 756 self._Authenticate() | |
| 757 save_cookie_jar() | |
| 758 return self.authenticated | |
| 759 with AppEngineService._auth_lock: | |
| 760 rpc_server = AuthServer(self.urlhost, self.get_credentials) | |
| 761 return rpc_server.PerformAuthentication() | |
| 762 | |
| 763 def get_credentials(self): | |
| 764 """Called during authentication process to get the credentials. | |
| 765 May be called mutliple times if authentication fails. | |
| 766 Returns tuple (email, password). | |
| 767 """ | |
| 768 # 'authenticate' calls this only if 'upload' is present. | |
| 769 # Ensure other callers (if any) fail non-cryptically if 'upload' is missing. | |
| 770 assert upload, '\'upload\' module is required for this to work' | |
| 771 if self.email and self.password: | |
| 772 return (self.email, self.password) | |
| 773 if not self._keyring: | |
| 774 self._keyring = upload.KeyringCreds(self.urlhost, | |
| 775 self.urlhost, | |
| 776 self.email) | |
| 777 return self._keyring.GetUserCredentials() | |
| 778 | |
| 779 | |
| 780 class ThreadSafeCookieJar(cookielib.MozillaCookieJar): | |
| 781 """MozillaCookieJar with thread safe load and save.""" | |
| 782 | |
| 783 def load(self, filename=None, ignore_discard=False, ignore_expires=False): | |
| 784 """Loads cookies from the file if it exists.""" | |
| 785 filename = os.path.expanduser(filename or self.filename) | |
| 786 with self._cookies_lock: | |
| 787 if os.path.exists(filename): | |
| 788 try: | |
| 789 cookielib.MozillaCookieJar.load(self, filename, | |
| 790 ignore_discard, | |
| 791 ignore_expires) | |
| 792 logging.debug('Loaded cookies from %s', filename) | |
| 793 except (cookielib.LoadError, IOError): | |
| 794 pass | |
| 795 else: | |
| 796 try: | |
| 797 fd = os.open(filename, os.O_CREAT, 0600) | |
| 798 os.close(fd) | |
| 799 except OSError: | |
| 800 logging.error('Failed to create %s', filename) | |
| 801 try: | |
| 802 os.chmod(filename, 0600) | |
| 803 except OSError: | |
| 804 logging.error('Failed to fix mode for %s', filename) | |
| 805 | |
| 806 def save(self, filename=None, ignore_discard=False, ignore_expires=False): | |
| 807 """Saves cookies to the file, completely overwriting it.""" | |
| 808 logging.debug('Saving cookies to %s', filename or self.filename) | |
| 809 with self._cookies_lock: | |
| 810 try: | |
| 811 cookielib.MozillaCookieJar.save(self, filename, | |
| 812 ignore_discard, | |
| 813 ignore_expires) | |
| 814 except OSError: | |
| 815 logging.error('Failed to save %s', filename) | |
| 816 | |
| 817 | |
| 818 def valid_file(filepath, size): | 339 def valid_file(filepath, size): |
| 819 """Determines if the given files appears valid (currently it just checks | 340 """Determines if the given files appears valid (currently it just checks |
| 820 the file's size).""" | 341 the file's size).""" |
| 821 if size == UNKNOWN_FILE_SIZE: | 342 if size == UNKNOWN_FILE_SIZE: |
| 822 return True | 343 return True |
| 823 actual_size = os.stat(filepath).st_size | 344 actual_size = os.stat(filepath).st_size |
| 824 if size != actual_size: | 345 if size != actual_size: |
| 825 logging.warning( | 346 logging.warning( |
| 826 'Found invalid item %s; %d != %d', | 347 'Found invalid item %s; %d != %d', |
| 827 os.path.basename(filepath), actual_size, size) | 348 os.path.basename(filepath), actual_size, size) |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 917 def _download_file(base_url, item, dest): | 438 def _download_file(base_url, item, dest): |
| 918 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this | 439 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this |
| 919 # easy. | 440 # easy. |
| 920 try: | 441 try: |
| 921 zipped_source = base_url + item | 442 zipped_source = base_url + item |
| 922 logging.debug('download_file(%s)', zipped_source) | 443 logging.debug('download_file(%s)', zipped_source) |
| 923 | 444 |
| 924 # Because the app engine DB is only eventually consistent, retry | 445 # Because the app engine DB is only eventually consistent, retry |
| 925 # 404 errors because the file might just not be visible yet (even | 446 # 404 errors because the file might just not be visible yet (even |
| 926 # though it has been uploaded). | 447 # though it has been uploaded). |
| 927 connection = url_open(zipped_source, retry_404=True, | 448 connection = net.url_open(zipped_source, retry_404=True, |
|
M-A Ruel
2013/08/28 15:01:48
I'd align:
connection = net.url_open(
zipped_s
| |
| 928 read_timeout=DOWNLOAD_READ_TIMEOUT) | 449 read_timeout=DOWNLOAD_READ_TIMEOUT) |
| 929 if not connection: | 450 if not connection: |
| 930 raise IOError('Unable to open connection to %s' % zipped_source) | 451 raise IOError('Unable to open connection to %s' % zipped_source) |
| 931 | 452 |
| 932 content_length = connection.content_length | 453 content_length = connection.content_length |
| 933 decompressor = zlib.decompressobj() | 454 decompressor = zlib.decompressobj() |
| 934 size = 0 | 455 size = 0 |
| 935 with open(dest, 'wb') as f: | 456 with open(dest, 'wb') as f: |
| 936 while True: | 457 while True: |
| 937 chunk = connection.read(ZIPPED_FILE_CHUNK) | 458 chunk = connection.read(ZIPPED_FILE_CHUNK) |
| 938 if not chunk: | 459 if not chunk: |
| (...skipping 732 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1671 except Exception, e: | 1192 except Exception, e: |
| 1672 # Make sure any exception is logged. | 1193 # Make sure any exception is logged. |
| 1673 logging.exception(e) | 1194 logging.exception(e) |
| 1674 return 1 | 1195 return 1 |
| 1675 | 1196 |
| 1676 | 1197 |
| 1677 if __name__ == '__main__': | 1198 if __name__ == '__main__': |
| 1678 # Ensure that we are always running with the correct encoding. | 1199 # Ensure that we are always running with the correct encoding. |
| 1679 fix_encoding.fix_encoding() | 1200 fix_encoding.fix_encoding() |
| 1680 sys.exit(main()) | 1201 sys.exit(main()) |
| OLD | NEW |