16a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com#!/usr/bin/env python 26a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# Copyright (c) 2012 The Chromium Authors. All rights reserved. 36a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# Use of this source code is governed by a BSD-style license that can be 46a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# found in the LICENSE file. 56a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 66a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com"""A tiny web server. 76a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 86a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comThis is intended to be used for testing, and only run from within the examples 96a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comdirectory. 106a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com""" 116a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 126a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport BaseHTTPServer 136a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport logging 146a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport optparse 156a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport os 166a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport SimpleHTTPServer 176a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport SocketServer 186a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport sys 196a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comimport urlparse 206a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 216a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 226a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comEXAMPLE_PATH=os.path.dirname(os.path.abspath(__file__)) 236a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comNACL_SDK_ROOT = os.getenv('NACL_SDK_ROOT', os.path.dirname(EXAMPLE_PATH)) 246a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 256a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 266a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comif os.path.exists(NACL_SDK_ROOT): 276a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com sys.path.append(os.path.join(NACL_SDK_ROOT, 'tools')) 286a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com import decode_dump 296a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com import getos 306a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comelse: 316a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com NACL_SDK_ROOT=None 326a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 336a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comlast_nexe = None 346a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comlast_nmf = None 356a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 366a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comlogging.getLogger().setLevel(logging.INFO) 376a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 386a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# Using 'localhost' means that we only accept connections 396a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# via the loop back interface. 406a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comSERVER_PORT = 5103 416a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comSERVER_HOST = '' 426a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 436a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# We only run from the examples directory so that not too much is exposed 446a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# via this HTTP server. Everything in the directory is served, so there should 456a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# never be anything potentially sensitive in the serving directory, especially 466a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# if the machine might be a multi-user machine and not all users are trusted. 476a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# We only serve via the loopback interface. 486a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comdef SanityCheckDirectory(): 496a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com httpd_path = os.path.abspath(os.path.dirname(__file__)) 506a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com serve_path = os.path.abspath(os.getcwd()) 516a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 526a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com # Verify we are serving from the directory this script came from, or bellow 536a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if serve_path[:len(httpd_path)] == httpd_path: 546a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return 556a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.error('For security, httpd.py should only be run from within the') 566a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.error('example directory tree.') 576a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.error('We are currently in %s.' % serve_path) 586a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com sys.exit(1) 596a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 606a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 616a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# An HTTP server that will quit when |is_running| is set to False. We also use 626a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# SocketServer.ThreadingMixIn in order to handle requests asynchronously for 636a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# faster responses. 646a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comclass QuittableHTTPServer(SocketServer.ThreadingMixIn, 656a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com BaseHTTPServer.HTTPServer): 666a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com def serve_forever(self, timeout=0.5): 676a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.is_running = True 686a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.timeout = timeout 696a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com while self.is_running: 706a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.handle_request() 716a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 726a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com def shutdown(self): 736a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.is_running = False 746a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return 1 756a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 766a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 776a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# "Safely" split a string at |sep| into a [key, value] pair. If |sep| does not 786a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# exist in |str|, then the entire |str| is the key and the value is set to an 796a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# empty string. 806a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comdef KeyValuePair(str, sep='='): 816a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if sep in str: 826a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return str.split(sep) 836a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com else: 846a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return [str, ''] 856a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 866a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 876a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# A small handler that looks for '?quit=1' query in the path and shuts itself 886a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com# down if it finds that parameter. 896a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comclass QuittableHTTPHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 906a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com def send_head(self): 916a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com """Common code for GET and HEAD commands. 926a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 936a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com This sends the response code and MIME headers. 946a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 956a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com Return value is either a file object (which has to be copied 966a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com to the outputfile by the caller unless the command was HEAD, 976a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com and must be closed by the caller under all circumstances), or 986a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com None, in which case the caller has nothing further to do. 996a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1006a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com """ 1016a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com path = self.translate_path(self.path) 1026a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com f = None 1036a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if os.path.isdir(path): 1046a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if not self.path.endswith('/'): 1056a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com # redirect browser - doing basically what apache does 1066a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_response(301) 1076a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header("Location", self.path + "/") 1086a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.end_headers() 1096a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return None 1106a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com for index in "index.html", "index.htm": 1116a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com index = os.path.join(path, index) 1126a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if os.path.exists(index): 1136a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com path = index 1146a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com break 1156a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com else: 1166a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return self.list_directory(path) 1176a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com ctype = self.guess_type(path) 1186a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com try: 1196a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com # Always read in binary mode. Opening files in text mode may cause 1206a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com # newline translations, making the actual size of the content 1216a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com # transmitted *less* than the content-length! 1226a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com f = open(path, 'rb') 1236a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com except IOError: 1246a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_error(404, "File not found") 1256a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return None 1266a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_response(200) 1276a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header("Content-type", ctype) 1286a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com fs = os.fstat(f.fileno()) 1296a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header("Content-Length", str(fs[6])) 1306a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 1316a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header('Cache-Control','no-cache, must-revalidate') 1326a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header('Expires','-1') 1336a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.end_headers() 1346a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return f 1356a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1366a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com def do_GET(self): 1376a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com global last_nexe, last_nmf 1386a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com (_, _, path, query, _) = urlparse.urlsplit(self.path) 1396a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com url_params = dict([KeyValuePair(key_value) 1406a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com for key_value in query.split('&')]) 1416a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if 'quit' in url_params and '1' in url_params['quit']: 1426a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_response(200, 'OK') 1436a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header('Content-type', 'text/html') 1446a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.send_header('Content-length', '0') 1456a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.end_headers() 1466a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.server.shutdown() 1476a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return 1486a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1496a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if path.endswith('.nexe'): 1506a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com last_nexe = path 1516a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if path.endswith('.nmf'): 1526a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com last_nmf = path 1536a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1546a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 1556a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1566a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com def do_POST(self): 1576a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com (_, _,path, query, _) = urlparse.urlsplit(self.path) 1586a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if 'Content-Length' in self.headers: 1596a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if not NACL_SDK_ROOT: 1606a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com self.wfile('Could not find NACL_SDK_ROOT to decode trace.') 1616a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return 1626a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com data = self.rfile.read(int(self.headers['Content-Length'])) 1636a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com nexe = '.' + last_nexe 1646a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com nmf = '.' + last_nmf 1656a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com addr = os.path.join(NACL_SDK_ROOT, 'toolchain', 1666a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com getos.GetPlatform() + '_x86_newlib', 1676a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 'bin', 'x86_64-nacl-addr2line') 1686a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com decoder = decode_dump.CoreDecoder(nexe, nmf, addr, None, None) 1696a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com info = decoder.Decode(data) 1706a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com trace = decoder.StackTrace(info) 1716a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com decoder.PrintTrace(trace, sys.stdout) 1726a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com decoder.PrintTrace(trace, self.wfile) 1736a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1746a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1756a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comdef Run(server_address, 1766a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com server_class=QuittableHTTPServer, 1776a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com handler_class=QuittableHTTPHandler): 1786a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com httpd = server_class(server_address, handler_class) 1796a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.info("Starting local server on port %d", server_address[1]) 1806a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.info("To shut down send http://localhost:%d?quit=1", 1816a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com server_address[1]) 1826a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com try: 1836a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com httpd.serve_forever() 1846a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com except KeyboardInterrupt: 1856a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.info("Received keyboard interrupt.") 1866a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com httpd.server_close() 1876a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1886a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com logging.info("Shutting down local server on port %d", server_address[1]) 1896a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1906a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 1916a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comdef main(): 1926a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com usage_str = "usage: %prog [options] [optional_portnum]" 1936a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com parser = optparse.OptionParser(usage=usage_str) 1946a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com parser.add_option( 1956a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com '--no_dir_check', dest='do_safe_check', 1966a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com action='store_false', default=True, 1976a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com help='Do not ensure that httpd.py is being run from a safe directory.') 1986a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com (options, args) = parser.parse_args(sys.argv) 1996a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if options.do_safe_check: 2006a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com SanityCheckDirectory() 2016a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com if len(args) > 2: 2026a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com print 'Too many arguments specified.' 2036a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com parser.print_help() 2046a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com elif len(args) == 2: 2056a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com Run((SERVER_HOST, int(args[1]))) 2066a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com else: 2076a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com Run((SERVER_HOST, SERVER_PORT)) 2086a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com return 0 2096a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 2106a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com 2116a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.comif __name__ == '__main__': 2126a98b8c0b5ffe1a23902cdf7e692f702b703eaebborenet@google.com sys.exit(main()) 213