diff options
author | binji@chromium.org <binji@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-12 23:41:16 +0000 |
---|---|---|
committer | binji@chromium.org <binji@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-12 23:41:16 +0000 |
commit | 8f1a4ff137ca0384af11e169bc28fb31eb91f56a (patch) | |
tree | 1a6e498d5a1c31c76242d7c01154658f724dee53 /native_client_sdk | |
parent | 9bb3a9b5ca87ce641605a4c14b6f7e8e911476f1 (diff) | |
download | chromium_src-8f1a4ff137ca0384af11e169bc28fb31eb91f56a.zip chromium_src-8f1a4ff137ca0384af11e169bc28fb31eb91f56a.tar.gz chromium_src-8f1a4ff137ca0384af11e169bc28fb31eb91f56a.tar.bz2 |
[NaCl SDK] Fix httpd.py to allow quitting via "?quit=1" query parameters.
Much of the changes here are required to support cleaning up all processes under different conditions:
* When Chrome GETs a URL with the query parameter "quit=1"
* When a Chrome exits.
* When the main script is killed (via KeyboardInterrupt, say).
This change also removes all globals from httpd.py, instead having the shared state needed by an HTTPRequestHandler attached to the HTTPServer.
BUG=none
TBR=noelallen@chromium.org
NOTRY=true
Review URL: https://codereview.chromium.org/11363171
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@167266 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'native_client_sdk')
-rwxr-xr-x[-rw-r--r--] | native_client_sdk/src/tools/httpd.py | 120 | ||||
-rwxr-xr-x | native_client_sdk/src/tools/run.py | 21 | ||||
-rwxr-xr-x | native_client_sdk/src/tools/tests/test_httpd.py | 108 |
3 files changed, 211 insertions, 38 deletions
diff --git a/native_client_sdk/src/tools/httpd.py b/native_client_sdk/src/tools/httpd.py index 1fbdbe5..0e2ef67 100644..100755 --- a/native_client_sdk/src/tools/httpd.py +++ b/native_client_sdk/src/tools/httpd.py @@ -1,7 +1,9 @@ +#!/usr/bin/env python # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import BaseHTTPServer import imp import logging import multiprocessing @@ -9,16 +11,14 @@ import optparse import os import SimpleHTTPServer # pylint: disable=W0611 import sys +import time +import urlparse SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) NACL_SDK_ROOT = os.path.dirname(SCRIPT_DIR) -serve_dir = None -delegate_map = {} - - # We only run from the examples directory so that not too much is exposed # via this HTTP server. Everything in the directory is served, so there should # never be anything potentially sensitive in the serving directory, especially @@ -37,14 +37,27 @@ def SanityCheckDirectory(dirname): sys.exit(1) +class PluggableHTTPServer(BaseHTTPServer.HTTPServer): + def __init__(self, *args, **kwargs): + BaseHTTPServer.HTTPServer.__init__(self, *args) + self.serve_dir = kwargs.get('serve_dir', '.') + self.delegate_map = {} + self.running = True + self.result = 0 + + def Shutdown(self, result=0): + self.running = False + self.result = result + + class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def _FindDelegateAtPath(self, dirname): # First check the cache... logging.debug('Looking for cached delegate in %s...' % dirname) handler_script = os.path.join(dirname, 'handler.py') - if dirname in delegate_map: - result = delegate_map[dirname] + if dirname in self.server.delegate_map: + result = self.server.delegate_map[dirname] if result is None: logging.debug('Found None.') else: @@ -79,7 +92,7 @@ class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): delegate = self._FindDelegateForURLRecurse(parent_dir, abs_root) logging.debug('Adding delegate to cache for %s.' % cur_dir) - delegate_map[cur_dir] = delegate + self.server.delegate_map[cur_dir] = delegate return delegate def _FindDelegateForURL(self, url_path): @@ -89,7 +102,7 @@ class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): else: dirname = os.path.dirname(path) - abs_serve_dir = os.path.abspath(serve_dir) + abs_serve_dir = os.path.abspath(self.server.serve_dir) delegate = self._FindDelegateForURLRecurse(dirname, abs_serve_dir) if not delegate: logging.info('No handler found for path %s. Using default.' % url_path) @@ -105,6 +118,19 @@ class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) def do_GET(self): + # TODO(binji): pyauto tests use the ?quit=1 method to kill the server. + # Remove this when we kill the pyauto tests. + _, _, _, query, _ = urlparse.urlsplit(self.path) + if query: + params = urlparse.parse_qs(query) + if '1' in params.get('quit', None): + self.send_response(200, 'OK') + self.send_header('Content-type', 'text/html') + self.send_header('Content-length', '0') + self.end_headers() + self.server.Shutdown() + return + delegate = self._FindDelegateForURL(self.path) if delegate: return delegate.do_GET(self) @@ -127,12 +153,12 @@ class LocalHTTPServer(object): """Class to start a local HTTP server as a child process.""" def __init__(self, dirname, port): - global serve_dir - serve_dir = dirname parent_conn, child_conn = multiprocessing.Pipe() self.process = multiprocessing.Process( target=_HTTPServerProcess, - args=(child_conn, serve_dir, port)) + args=(child_conn, dirname, port, { + 'serve_dir': dirname, + })) self.process.start() if parent_conn.poll(10): # wait 10 seconds self.port = parent_conn.recv() @@ -141,6 +167,47 @@ class LocalHTTPServer(object): self.conn = parent_conn + def ServeForever(self): + """Serve until the child HTTP process tells us to stop. + + Returns: + The result from the child (as an errorcode), or 0 if the server was + killed not by the child (by KeyboardInterrupt for example). + """ + child_result = 0 + try: + # Block on this pipe, waiting for a response from the child process. + child_result = self.conn.recv() + except KeyboardInterrupt: + pass + finally: + self.Shutdown() + return child_result + + def ServeUntilSubprocessDies(self, process): + """Serve until the child HTTP process tells us to stop or |subprocess| dies. + + Returns: + The result from the child (as an errorcode), or 0 if |subprocess| died, + or the server was killed some other way (by KeyboardInterrupt for + example). + """ + child_result = 0 + try: + while True: + if process.poll() is not None: + child_result = 0 + break + if self.conn.poll(): + child_result = self.conn.recv() + break + time.sleep(0) + except KeyboardInterrupt: + pass + finally: + self.Shutdown() + return child_result + def Shutdown(self): """Send a message to the child HTTP server process and wait for it to finish.""" @@ -157,7 +224,7 @@ class LocalHTTPServer(object): return 'http://localhost:%d/%s' % (self.port, rel_url) -def _HTTPServerProcess(conn, dirname, port): +def _HTTPServerProcess(conn, dirname, port, server_kwargs): """Run a local httpserver with the given port or an ephemeral port. This function assumes it is run as a child process using multiprocessing. @@ -165,29 +232,30 @@ def _HTTPServerProcess(conn, dirname, port): Args: conn: A connection to the parent process. The child process sends the local port, and waits for a message from the parent to - stop serving. + stop serving. It also sends a "result" back to the parent -- this can + be used to allow a client-side test to notify the server of results. dirname: The directory to serve. All files are accessible through http://localhost:<port>/path/to/filename. port: The port to serve on. If 0, an ephemeral port will be chosen. + server_kwargs: A dict that will be passed as kwargs to the server. """ - import BaseHTTPServer - try: os.chdir(dirname) - httpd = BaseHTTPServer.HTTPServer(('', port), PluggableHTTPRequestHandler) + httpd = PluggableHTTPServer(('', port), PluggableHTTPRequestHandler, + **server_kwargs) conn.send(httpd.server_address[1]) # the chosen port number httpd.timeout = 0.5 # seconds - running = True - while running: + while httpd.running: # Flush output for MSVS Add-In. sys.stdout.flush() sys.stderr.flush() httpd.handle_request() if conn.poll(): - running = conn.recv() + httpd.running = conn.recv() except KeyboardInterrupt: pass finally: + conn.send(httpd.result) conn.close() @@ -206,18 +274,12 @@ def main(args): if options.do_safe_check: SanityCheckDirectory(options.serve_dir) - server = LocalHTTPServer(options.serve_dir, options.port) + server = LocalHTTPServer(options.serve_dir, int(options.port)) - # Serve forever. + # Serve until the client tells us to stop. When it does, it will give us an + # errorcode. print 'Serving %s on %s...' % (options.serve_dir, server.GetURL('')) - try: - while True: - pass - except KeyboardInterrupt: - pass - finally: - print 'Stopping server.' - server.Shutdown() + return server.ServeForever() if __name__ == '__main__': sys.exit(main(sys.argv[1:])) diff --git a/native_client_sdk/src/tools/run.py b/native_client_sdk/src/tools/run.py index 7c4ddf2..3fbd0e0 100755 --- a/native_client_sdk/src/tools/run.py +++ b/native_client_sdk/src/tools/run.py @@ -37,18 +37,21 @@ def main(args): # 0 means use an ephemeral port. server = httpd.LocalHTTPServer(options.serve_dir, 0) + print 'Serving %s on %s...' % (options.serve_dir, server.GetURL('')) + env = copy.copy(os.environ) + for e in options.environ: + key, value = map(str.strip, e.split('=')) + env[key] = value + + cmd = args + [server.GetURL(options.path)] + print 'Running: %s...' % (' '.join(cmd),) + process = subprocess.Popen(cmd, env=env) try: - env = copy.copy(os.environ) - for e in options.environ: - key, value = map(str.strip, e.split('=')) - env[key] = value - - cmd = args + [server.GetURL(options.path)] - print 'Running: %s...' % (' '.join(cmd),) - subprocess.call(cmd, env=env) + return server.ServeUntilSubprocessDies(process) finally: - server.Shutdown() + if process.returncode is None: + process.kill() if __name__ == '__main__': diff --git a/native_client_sdk/src/tools/tests/test_httpd.py b/native_client_sdk/src/tools/tests/test_httpd.py new file mode 100755 index 0000000..83390d1 --- /dev/null +++ b/native_client_sdk/src/tools/tests/test_httpd.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import os +import re +import sys +import subprocess +import tempfile +import unittest +import urllib2 + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +PARENT_DIR = os.path.dirname(SCRIPT_DIR) + +sys.path.append(PARENT_DIR) + +import httpd + + +class HTTPDTest(unittest.TestCase): + def setUp(self): + self.server = httpd.LocalHTTPServer('.', 0) + + def tearDown(self): + self.server.Shutdown() + + def testQuit(self): + urllib2.urlopen(self.server.GetURL('?quit=1')) + self.server.process.join(10) # Wait 10 seconds for the process to finish. + self.assertFalse(self.server.process.is_alive()) + + +class RunTest(unittest.TestCase): + def setUp(self): + self.process = None + self.tempscript = None + + def tearDown(self): + if self.process and self.process.returncode is None: + self.process.kill() + if self.tempscript: + os.remove(self.tempscript) + + def _Run(self, args=None): + args = args or [] + run_py = os.path.join(PARENT_DIR, 'run.py') + cmd = [sys.executable, run_py] + cmd.extend(args) + self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = self.process.communicate() + try: + self.assertEqual(0, self.process.returncode) + except AssertionError: + print 'subprocess failed: %s:\nstdout: %s\nstderr:%s\n' % ( + ' '.join(cmd), stdout, stderr) + return stdout + + def _WriteTempScript(self, script): + fd, filename = tempfile.mkstemp(suffix='.py') + lines = script.splitlines() + assert len(lines[0]) == 0 # First line is always empty. + # Count the number of spaces after the first newline. + m = re.match(r'\s*', lines[1]) + assert m + indent = len(m.group(0)) + script = '\n'.join(line[indent:] for line in lines[1:]) + + os.write(fd, script) + os.close(fd) + self.tempscript = filename + + def testQuit(self): + self._WriteTempScript(r""" + import sys + import time + import urllib2 + + print 'running tempscript' + sys.stdout.flush() + f = urllib2.urlopen(sys.argv[-1] + '?quit=1') + f.read() + f.close() + time.sleep(10) + + # Should be killed before this prints. + print 'Not killed yet.' + sys.stdout.flush() + sys.exit(0) + """) + stdout = self._Run(['--', sys.executable, self.tempscript]) + self.assertTrue('running tempscript' in stdout) + self.assertTrue("Not killed yet" not in stdout) + + def testSubprocessDies(self): + self._WriteTempScript(r""" + import sys + print 'running tempscript' + sys.stdout.flush() + sys.exit(1) + """) + stdout = self._Run(['--', sys.executable, self.tempscript]) + self.assertTrue('running tempscript' in stdout) + +if __name__ == '__main__': + unittest.main() |