Code

version info in scripts
[roundup.git] / roundup / scripts / roundup_server.py
index 72d7d380b0fac9c9220af2cdd59c922806d186e6..c14cdf6abf597b70ae1e60422ce2b9333164cac8 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-""" HTTP Server that serves roundup.
 
 
-$Id: roundup_server.py,v 1.12 2002-09-23 06:48:35 richard Exp $
+"""Command-line script that runs a server over roundup.cgi.client.
+
+$Id: roundup_server.py,v 1.43 2004-04-05 23:43:04 richard Exp $
 """
 """
+__docformat__ = 'restructuredtext'
 
 # python version check
 from roundup import version_check
 
 # python version check
 from roundup import version_check
+from roundup import __version__ as roundup_version
 
 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
 
 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
-import BaseHTTPServer
+import BaseHTTPServer, socket, errno
 
 # Roundup modules of use here
 from roundup.cgi import cgitb, client
 import roundup.instance
 from roundup.i18n import _
 
 
 # Roundup modules of use here
 from roundup.cgi import cgitb, client
 import roundup.instance
 from roundup.i18n import _
 
+try:
+    import signal
+except:
+    signal = None
+
 #
 ##  Configuration
 #
 #
 ##  Configuration
 #
@@ -40,25 +48,38 @@ from roundup.i18n import _
 # Make sure the NAME part doesn't include any url-unsafe characters like 
 # spaces, as these confuse the cookie handling in browsers like IE.
 TRACKER_HOMES = {
 # Make sure the NAME part doesn't include any url-unsafe characters like 
 # spaces, as these confuse the cookie handling in browsers like IE.
 TRACKER_HOMES = {
-    'bar': '/tmp/bar',
+#    'example': '/path/to/example',
 }
 
 ROUNDUP_USER = None
 }
 
 ROUNDUP_USER = None
+ROUNDUP_GROUP = None
+ROUNDUP_LOG_IP = 1
+HOSTNAME = ''
+PORT = 8080
+PIDFILE = None
+LOGFILE = None
 
 
 
 
-# Where to log debugging information to. Use an instance of DevNull if you
-# don't want to log anywhere.
-# TODO: actually use this stuff
-#class DevNull:
-#    def write(self, info):
-#        pass
-#LOG = open('/var/log/roundup.cgi.log', 'a')
-#LOG = DevNull()
-
 #
 ##  end configuration
 #
 
 #
 ##  end configuration
 #
 
+# "default" favicon.ico
+# generate by using "icotool" and tools/base64
+import zlib, base64
+favico = zlib.decompress(base64.decodestring('''
+eJztjr1PmlEUh59XgVoshdYPWorFIhaRFq0t9pNq37b60lYSTRzcTFw6GAfj5gDYaF0dTB0MxMSE
+gQQd3FzKJiEC0UCIUUN1M41pV2JCXySg/0ITn5tfzvmdc+85FwT56HSc81UJjXJsk1UsNcsSqCk1
+BS64lK+vr7OyssLJyQl2ux2j0cjU1BQajYZIJEIwGMRms+H3+zEYDExOTjI2Nsbm5iZWqxWv18vW
+1hZDQ0Ok02kmJiY4Ojpienqa3d1dxsfHUSqVeDwe5ufnyeVyrK6u4nK5ODs7Y3FxEYfDwdzcHCaT
+icPDQ5LJJIIgMDIyQj6fZ39/n+3tbdbW1pAkiYWFBWZmZtjb2yMejzM8PEwgEMDn85HNZonFYqjV
+asLhMMvLy2QyGfR6PaOjowwODmKxWDg+PkalUhEKhSgUCiwtLWE2m9nZ2UGhULCxscHp6SmpVIpo
+NMrs7CwHBwdotVoSiQRXXPG/IzY7RHtt922xjFRb01H1XhKfPBNbi/7my7rrLXJ88eppvxwEfV3f
+NY3Y6exofVdsV3+2wnPFDdPjB83n7xuVpcFvygPbGwxF31LZIKrQDfR2Xvh7lmrX654L/7bvlnng
+bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g
+23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI=
+'''.strip()))
+
 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     TRACKER_HOMES = TRACKER_HOMES
     ROUNDUP_USER = ROUNDUP_USER
 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     TRACKER_HOMES = TRACKER_HOMES
     ROUNDUP_USER = ROUNDUP_USER
@@ -76,59 +97,88 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         except client.Unauthorised:
             self.send_error(403, self.path)
         except:
         except client.Unauthorised:
             self.send_error(403, self.path)
         except:
-            # it'd be nice to be able to detect if these are going to have
-            # any effect...
-            self.send_response(400)
-            self.send_header('Content-Type', 'text/html')
-            self.end_headers()
-            try:
-                reload(cgitb)
-                self.wfile.write(cgitb.breaker())
-                self.wfile.write(cgitb.html())
-            except:
-                self.wfile.write("<pre>")
+            exc, val, tb = sys.exc_info()
+            if hasattr(socket, 'timeout') and exc == socket.timeout:
                 s = StringIO.StringIO()
                 traceback.print_exc(None, s)
                 s = StringIO.StringIO()
                 traceback.print_exc(None, s)
-                self.wfile.write(cgi.escape(s.getvalue()))
-                self.wfile.write("</pre>\n")
+                self.log_message(str(s.getvalue()))
+            else:
+                # it'd be nice to be able to detect if these are going to have
+                # any effect...
+                self.send_response(400)
+                self.send_header('Content-Type', 'text/html')
+                self.end_headers()
+                try:
+                    reload(cgitb)
+                    self.wfile.write(cgitb.breaker())
+                    self.wfile.write(cgitb.html())
+                except:
+                    s = StringIO.StringIO()
+                    traceback.print_exc(None, s)
+                    self.wfile.write("<pre>")
+                    self.wfile.write(cgi.escape(s.getvalue()))
+                    self.wfile.write("</pre>\n")
         sys.stdin = save_stdin
 
         sys.stdin = save_stdin
 
-    do_GET = do_POST = do_HEAD = send_head = run_cgi
+    do_GET = do_POST = run_cgi
 
     def index(self):
 
     def index(self):
-        ''' Print up an index of the available instances
+        ''' Print up an index of the available trackers
         '''
         self.send_response(200)
         self.send_header('Content-Type', 'text/html')
         self.end_headers()
         w = self.wfile.write
         '''
         self.send_response(200)
         self.send_header('Content-Type', 'text/html')
         self.end_headers()
         w = self.wfile.write
-        w(_('<html><head><title>Roundup instances index</title></head>\n'))
-        w(_('<body><h1>Roundup instances index</h1><ol>\n'))
-        for instance in self.TRACKER_HOMES.keys():
-            w(_('<li><a href="%(instance_url)s/index">%(instance_name)s</a>\n')%{
-                'instance_url': urllib.quote(instance),
-                'instance_name': cgi.escape(instance)})
+        w(_('<html><head><title>Roundup trackers index</title></head>\n'))
+        w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
+        keys = self.TRACKER_HOMES.keys()
+        keys.sort()
+        for tracker in keys:
+            w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
+                'tracker_url': urllib.quote(tracker),
+                'tracker_name': cgi.escape(tracker)})
         w(_('</ol></body></html>'))
 
     def inner_run_cgi(self):
         ''' This is the inner part of the CGI handling
         '''
         w(_('</ol></body></html>'))
 
     def inner_run_cgi(self):
         ''' This is the inner part of the CGI handling
         '''
-
         rest = self.path
         rest = self.path
+
+        if rest == '/favicon.ico':
+            self.send_response(200)
+            self.send_header('Content-Type', 'image/x-icon')
+            self.end_headers()
+            self.wfile.write(favico)
+            return
+
         i = rest.rfind('?')
         if i >= 0:
             rest, query = rest[:i], rest[i+1:]
         else:
             query = ''
 
         i = rest.rfind('?')
         if i >= 0:
             rest, query = rest[:i], rest[i+1:]
         else:
             query = ''
 
-        # figure the instance
+        # no tracker - spit out the index
         if rest == '/':
             return self.index()
         if rest == '/':
             return self.index()
+
+        # figure the tracker
         l_path = rest.split('/')
         l_path = rest.split('/')
-        instance_name = urllib.unquote(l_path[1])
-        if self.TRACKER_HOMES.has_key(instance_name):
-            instance_home = self.TRACKER_HOMES[instance_name]
-            instance = roundup.instance.open(instance_home)
+        tracker_name = urllib.unquote(l_path[1])
+
+        # handle missing trailing '/'
+        if len(l_path) == 2:
+            self.send_response(301)
+            # redirect - XXX https??
+            protocol = 'http'
+            url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
+            self.send_header('Location', url)
+            self.end_headers()
+            self.wfile.write('Moved Permanently')
+            return
+
+        if self.TRACKER_HOMES.has_key(tracker_name):
+            tracker_home = self.TRACKER_HOMES[tracker_name]
+            tracker = roundup.instance.open(tracker_home)
         else:
             raise client.NotFound
 
         else:
             raise client.NotFound
 
@@ -140,7 +190,7 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 
         # Set up the CGI environment
         env = {}
 
         # Set up the CGI environment
         env = {}
-        env['TRACKER_NAME'] = instance_name
+        env['TRACKER_NAME'] = tracker_name
         env['REQUEST_METHOD'] = self.command
         env['PATH_INFO'] = urllib.unquote(rest)
         if query:
         env['REQUEST_METHOD'] = self.command
         env['PATH_INFO'] = urllib.unquote(rest)
         if query:
@@ -156,6 +206,7 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         co = filter(None, self.headers.getheaders('cookie'))
         if co:
             env['HTTP_COOKIE'] = ', '.join(co)
         co = filter(None, self.headers.getheaders('cookie'))
         if co:
             env['HTTP_COOKIE'] = ', '.join(co)
+        env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
         env['SCRIPT_NAME'] = ''
         env['SERVER_NAME'] = self.server.server_name
         env['SERVER_PORT'] = str(self.server.server_port)
         env['SCRIPT_NAME'] = ''
         env['SERVER_NAME'] = self.server.server_name
         env['SERVER_PORT'] = str(self.server.server_port)
@@ -164,24 +215,159 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         decoded_query = query.replace('+', ' ')
 
         # do the roundup thang
         decoded_query = query.replace('+', ' ')
 
         # do the roundup thang
-        c = instance.Client(instance, self, env)
+        c = tracker.Client(tracker, self, env)
         c.main()
 
         c.main()
 
-def usage(message=''):
-    if message:
-        message = _('Error: %(error)s\n\n')%{'error': message}
-    print _('''%(message)sUsage:
-roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=instance home]*
+    LOG_IPADDRESS = ROUNDUP_LOG_IP
+    def address_string(self):
+        if self.LOG_IPADDRESS:
+            return self.client_address[0]
+        else:
+            host, port = self.client_address
+            return socket.getfqdn(host)
 
 
- -n: sets the host name
- -p: sets the port to listen on
- -l: sets a filename to log to (instead of stdout)
- -d: daemonize, and write the server's PID to the nominated file
+    def log_message(self, format, *args):
+        ''' Try to *safely* log to stderr.
+        '''
+        try:
+            BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
+                format, *args)
+        except IOError:
+            # stderr is no longer viable
+            pass
+
+def error():
+    exc_type, exc_value = sys.exc_info()[:2]
+    return _('Error: %s: %s' % (exc_type, exc_value))
+
+try:
+    import win32serviceutil
+except:
+    RoundupService = None
+else:
+    # allow the win32
+    import win32service
+    import win32event
+    from win32event import *
+    from win32file import *
+
+    SvcShutdown = "ServiceShutdown"
+
+    class RoundupService(win32serviceutil.ServiceFramework,
+            BaseHTTPServer.HTTPServer):
+        ''' A Roundup standalone server for Win32 by Ewout Prangsma
+        '''
+        _svc_name_ = "Roundup Bug Tracker"
+        _svc_display_name_ = "Roundup Bug Tracker"
+        address = (HOSTNAME, PORT)
+        def __init__(self, args):
+            # redirect stdout/stderr to our logfile
+            if LOGFILE:
+                # appending, unbuffered
+                sys.stdout = sys.stderr = open(LOGFILE, 'a', 0)
+            win32serviceutil.ServiceFramework.__init__(self, args)
+            BaseHTTPServer.HTTPServer.__init__(self, self.address, 
+                RoundupRequestHandler)
+
+            # Create the necessary NT Event synchronization objects...
+            # hevSvcStop is signaled when the SCM sends us a notification
+            # to shutdown the service.
+            self.hevSvcStop = win32event.CreateEvent(None, 0, 0, None)
+
+            # hevConn is signaled when we have a new incomming connection.
+            self.hevConn    = win32event.CreateEvent(None, 0, 0, None)
+
+            # Hang onto this module for other people to use for logging
+            # purposes.
+            import servicemanager
+            self.servicemanager = servicemanager
+
+        def SvcStop(self):
+            # Before we do anything, tell the SCM we are starting the
+            # stop process.
+            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+            win32event.SetEvent(self.hevSvcStop)
+
+        def SvcDoRun(self):
+            try:
+                self.serve_forever()
+            except SvcShutdown:
+                pass
+
+        def get_request(self):
+            # Call WSAEventSelect to enable self.socket to be waited on.
+            WSAEventSelect(self.socket, self.hevConn, FD_ACCEPT)
+            while 1:
+                try:
+                    rv = self.socket.accept()
+                except socket.error, why:
+                    if why[0] != WSAEWOULDBLOCK:
+                        raise
+                    # Use WaitForMultipleObjects instead of select() because
+                    # on NT select() is only good for sockets, and not general
+                    # NT synchronization objects.
+                    rc = WaitForMultipleObjects((self.hevSvcStop, self.hevConn),
+                        0, INFINITE)
+                    if rc == WAIT_OBJECT_0:
+                        # self.hevSvcStop was signaled, this means:
+                        # Stop the service!
+                        # So we throw the shutdown exception, which gets
+                        # caught by self.SvcDoRun
+                        raise SvcShutdown
+                    # Otherwise, rc == WAIT_OBJECT_0 + 1 which means
+                    # self.hevConn was signaled, which means when we call 
+                    # self.socket.accept(), we'll have our incoming connection
+                    # socket!
+                    # Loop back to the top, and let that accept do its thing...
+                else:
+                    # yay! we have a connection
+                    # However... the new socket is non-blocking, we need to
+                    # set it back into blocking mode. (The socket that accept()
+                    # returns has the same properties as the listening sockets,
+                    # this includes any properties set by WSAAsyncSelect, or 
+                    # WSAEventSelect, and whether its a blocking socket or not.)
+                    #
+                    # So if you yank the following line, the setblocking() call 
+                    # will be useless. The socket will still be in non-blocking
+                    # mode.
+                    WSAEventSelect(rv[0], self.hevConn, 0)
+                    rv[0].setblocking(1)
+                    break
+            return rv
 
 
- name=instance home
-   Sets the instance home(s) to use. The name is how the instance is
+def usage(message=''):
+    if RoundupService:
+        win = ''' -c: Windows Service options.  If you want to run the server as a Windows
+     Service, you must configure the rest of the options by changing the
+     constants of this program.  You will at least configure one tracker
+     in the TRACKER_HOMES variable.  This option is mutually exclusive
+     from the rest.  Typing "roundup-server -c help" shows Windows
+     Services specifics.'''
+    else:
+        win = ''
+    port=PORT
+    print _('''%(message)s
+Usage:
+roundup-server [options] [name=tracker home]*
+
+options:
+ -v: print version and exit
+ -n: sets the host name
+ -p: sets the port to listen on (default: %(port)s)
+ -u: sets the uid to this user after listening on the port
+ -g: sets the gid to this group after listening on the port
+ -l: sets a filename to log to (instead of stderr / stdout)
+ -d: run the server in the background and on UN*X write the server's PID
+     to the nominated file. The -l option *must* be specified if this
+     option is.
+ -N: log client machine names in access log instead of IP addresses (much
+     slower)
+%(win)s
+
+name=tracker home:
+   Sets the tracker home(s) to use. The name is how the tracker is
    identified in the URL (it's the first part of the URL path). The
    identified in the URL (it's the first part of the URL path). The
-   instance home is the directory that was identified when you did
+   tracker home is the directory that was identified when you did
    "roundup-admin init". You may specify any number of these name=home
    pairs on the command-line. For convenience, you may edit the
    TRACKER_HOMES variable in the roundup-server file instead.
    "roundup-admin init". You may specify any number of these name=home
    pairs on the command-line. For convenience, you may edit the
    TRACKER_HOMES variable in the roundup-server file instead.
@@ -213,9 +399,9 @@ def daemonize(pidfile):
         pidfile = open(pidfile, 'w')
         pidfile.write(str(pid))
         pidfile.close()
         pidfile = open(pidfile, 'w')
         pidfile.write(str(pid))
         pidfile.close()
-        os._exit(0)         
+        os._exit(0)
 
 
-    os.chdir("/")         
+    os.chdir("/")
     os.umask(0)
 
     # close off sys.std(in|out|err), redirect to devnull so the file
     os.umask(0)
 
     # close off sys.std(in|out|err), redirect to devnull so the file
@@ -225,26 +411,79 @@ def daemonize(pidfile):
     os.dup2(devnull, 1)
     os.dup2(devnull, 2)
 
     os.dup2(devnull, 1)
     os.dup2(devnull, 2)
 
-def run():
-    hostname = ''
-    port = 8080
-    pidfile = None
-    logfile = None
+def run(port=PORT, success_message=None):
+    ''' Script entry point - handle args and figure out what to to.
+    '''
+    # time out after a minute if we can
+    import socket
+    if hasattr(socket, 'setdefaulttimeout'):
+        socket.setdefaulttimeout(60)
+
+    hostname = HOSTNAME
+    pidfile = PIDFILE
+    logfile = LOGFILE
+    user = ROUNDUP_USER
+    group = ROUNDUP_GROUP
+    svc_args = None
+
     try:
         # handle the command-line args
     try:
         # handle the command-line args
+        options = 'n:p:u:d:l:hNv'
+        if RoundupService:
+            options += 'c'
+
         try:
         try:
-            optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:')
+            optlist, args = getopt.getopt(sys.argv[1:], options)
         except getopt.GetoptError, e:
             usage(str(e))
 
         user = ROUNDUP_USER
         except getopt.GetoptError, e:
             usage(str(e))
 
         user = ROUNDUP_USER
+        group = None
         for (opt, arg) in optlist:
             if opt == '-n': hostname = arg
         for (opt, arg) in optlist:
             if opt == '-n': hostname = arg
+            elif opt == '-v':
+                print '%s (python %s)'%(roundup_version, sys.version.split()[0])
+                return
             elif opt == '-p': port = int(arg)
             elif opt == '-u': user = arg
             elif opt == '-p': port = int(arg)
             elif opt == '-u': user = arg
-            elif opt == '-d': pidfile = arg
-            elif opt == '-l': logfile = arg
+            elif opt == '-g': group = arg
+            elif opt == '-d': pidfile = os.path.abspath(arg)
+            elif opt == '-l': logfile = os.path.abspath(arg)
             elif opt == '-h': usage()
             elif opt == '-h': usage()
+            elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
+            elif opt == '-c': svc_args = [opt] + args; args = None
+
+        if svc_args is not None and len(optlist) > 1:
+            raise ValueError, _("windows service option must be the only one")
+
+        if pidfile and not logfile:
+            raise ValueError, _("logfile *must* be specified if pidfile is")
+  
+        # obtain server before changing user id - allows to use port <
+        # 1024 if started as root
+        address = (hostname, port)
+        try:
+            httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
+        except socket.error, e:
+            if e[0] == errno.EADDRINUSE:
+                raise socket.error, \
+                      _("Unable to bind to port %s, port already in use." % port)
+            raise
+
+        if group is not None and hasattr(os, 'getgid'):
+            # if root, setgid to the running user
+            if not os.getgid() and user is not None:
+                try:
+                    import pwd
+                except ImportError:
+                    raise ValueError, _("Can't change groups - no pwd module")
+                try:
+                    gid = pwd.getpwnam(user)[3]
+                except KeyError:
+                    raise ValueError,_("Group %(group)s doesn't exist")%locals()
+                os.setgid(gid)
+            elif os.getgid() and user is not None:
+                print _('WARNING: ignoring "-g" argument, not root')
 
         if hasattr(os, 'getuid'):
             # if root, setuid to the running user
 
         if hasattr(os, 'getuid'):
             # if root, setuid to the running user
@@ -265,37 +504,53 @@ def run():
             if not os.getuid() and user is None:
                 raise ValueError, _("Can't run as root!")
 
             if not os.getuid() and user is None:
                 raise ValueError, _("Can't run as root!")
 
-        # handle instance specs
+        # handle tracker specs
         if args:
             d = {}
             for arg in args:
         if args:
             d = {}
             for arg in args:
-               try:
+                try:
                     name, home = arg.split('=')
                 except ValueError:
                     raise ValueError, _("Instances must be name=home")
                     name, home = arg.split('=')
                 except ValueError:
                     raise ValueError, _("Instances must be name=home")
-                d[name] = home
+                d[name] = os.path.abspath(home)
             RoundupRequestHandler.TRACKER_HOMES = d
     except SystemExit:
         raise
             RoundupRequestHandler.TRACKER_HOMES = d
     except SystemExit:
         raise
+    except ValueError:
+        usage(error())
     except:
     except:
-        exc_type, exc_value = sys.exc_info()[:2]
-        usage('%s: %s'%(exc_type, exc_value))
+        print error()
+        sys.exit(1)
 
     # we don't want the cgi module interpreting the command-line args ;)
     sys.argv = sys.argv[:1]
 
     # we don't want the cgi module interpreting the command-line args ;)
     sys.argv = sys.argv[:1]
-    address = (hostname, port)
 
 
-    # fork?
     if pidfile:
     if pidfile:
-        daemonize(pidfile)
+        if not hasattr(os, 'fork'):
+            print "Sorry, you can't run the server as a daemon on this" \
+                'Operating System'
+            sys.exit(0)
+        else:
+            daemonize(pidfile)
+
+    if svc_args is not None:
+        # don't do any other stuff
+        return win32serviceutil.HandleCommandLine(RoundupService, argv=svc_args)
 
     # redirect stdout/stderr to our logfile
     if logfile:
 
     # redirect stdout/stderr to our logfile
     if logfile:
-        sys.stdout = sys.stderr = open(logfile, 'a')
+        # appending, unbuffered
+        sys.stdout = sys.stderr = open(logfile, 'a', 0)
 
 
-    httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
-    print _('Roundup server started on %(address)s')%locals()
-    httpd.serve_forever()
+    if success_message:
+        print success_message
+    else:
+        print _('Roundup server started on %(address)s')%locals()
+
+    try:
+        httpd.serve_forever()
+    except KeyboardInterrupt:
+        print 'Keyboard Interrupt: exiting'
 
 if __name__ == '__main__':
     run()
 
 if __name__ == '__main__':
     run()