Code

- added a favicon
[roundup.git] / roundup / scripts / roundup_server.py
index fe118423484b6f096d0dd1841ff10d8c7d1599fa..2b20fd7b7d2d24a12c75ba416c7d099cb7935070 100644 (file)
 # 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.16 2002-11-28 06:55:57 richard Exp $
+"""Command-line script that runs a server over roundup.cgi.client.
+
+$Id: roundup_server.py,v 1.41 2004-04-05 00:51:45 richard Exp $
 """
+__docformat__ = 'restructuredtext'
 
 # python version check
 from roundup import version_check
 
 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 _
 
+try:
+    import signal
+except:
+    signal = None
+
 #
 ##  Configuration
 #
@@ -40,25 +47,36 @@ 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 = {
-    'bar': '/tmp/bar',
+#    'example': '/path/to/example',
 }
 
 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
 #
 
+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
@@ -76,24 +94,30 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         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)
-                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
 
-    do_GET = do_POST = do_HEAD = send_head = run_cgi
+    do_GET = do_POST = run_cgi
 
     def index(self):
         ''' Print up an index of the available trackers
@@ -115,19 +139,40 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     def inner_run_cgi(self):
         ''' This is the inner part of the CGI handling
         '''
-
         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 = ''
 
-        # figure the tracker
+        # no tracker - spit out the index
         if rest == '/':
             return self.index()
+
+        # figure the tracker
         l_path = rest.split('/')
         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)
@@ -170,18 +215,152 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         c = tracker.Client(tracker, self, env)
         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=tracker 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=tracker home
+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:
+ -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
    tracker home is the directory that was identified when you did
@@ -228,37 +407,76 @@ def daemonize(pidfile):
     os.dup2(devnull, 1)
     os.dup2(devnull, 2)
 
-def abspath(path):
-    ''' Make the given path an absolute path.
-
-        Code from Zope-Coders posting of 2002-10-06 by GvR.
-    '''
-    if not os.path.isabs(path):
-        path = os.path.join(os.getcwd(), path)
-    return os.path.normpath(path)
-
-def run():
+def run(port=PORT, success_message=None):
     ''' Script entry point - handle args and figure out what to to.
     '''
-    hostname = ''
-    port = 8080
-    pidfile = None
-    logfile = None
+    # 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
+        options = 'n:p:u:d:l:hN'
+        if RoundupService:
+            options += 'c'
+
         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
+        group = None
         for (opt, arg) in optlist:
             if opt == '-n': hostname = arg
             elif opt == '-p': port = int(arg)
             elif opt == '-u': user = arg
-            elif opt == '-d': pidfile = abspath(arg)
-            elif opt == '-l': logfile = abspath(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 == '-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
@@ -283,32 +501,45 @@ def run():
         if args:
             d = {}
             for arg in args:
-               try:
+                try:
                     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
+    except ValueError:
+        usage(error())
     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]
-    address = (hostname, port)
 
-    # fork?
     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:
-        sys.stdout = sys.stderr = open(logfile, 'a')
+        # appending, unbuffered
+        sys.stdout = sys.stderr = open(logfile, 'a', 0)
+
+    if success_message:
+        print success_message
+    else:
+        print _('Roundup server started on %(address)s')%locals()
 
-    httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
-    print _('Roundup server started on %(address)s')%locals()
     try:
         httpd.serve_forever()
     except KeyboardInterrupt: