Code

- added a favicon
[roundup.git] / roundup / scripts / roundup_server.py
index 1575f418f94522a2978be811a5675c073b6dc778..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.26 2003-08-12 01:14:11 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, socket
+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,35 +47,34 @@ 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('''
-eJyVUk2IQVEUfn4yaRYjibdQZiVba/ZE2djIUmHWFjaKGVmIlY2iFMVG2ViQhXqFSP6iFFJvw4uF
-LGdWd743mpeMn+a88917Oue7955z3qEoET6FQkHx8iahKDV2A8B7XgERRf/EKMSUzyf8ypbbnnQy
-mWBdr9eVSkVw3tJGoxGNRpvNZigUyufzWPv9Pvwcx0UiERj7/V4g73Y7j8fTarWMRmO73U4kEkKI
-YZhardbr9eLxuOD0+/2ZTMZisYjFYpqmU6kU799uN5tNMBg8HA7ZbPY8GaTh8/mEipRKpclk0ul0
-NpvNarUmk0mWZS/yr9frcrmc+iMOh+NWydPp1Ov1SiSSc344HL7fKKfTiSN2u12tVqOcxWJxn6/V
-ag0GAwxkrlKp5vP5fT7ulMlk6XRar9dLpVIUXi6Xb5Hxa1wul0ajKZVKsVjM7XYXCoVOp3OVPJvN
-AoFAtVo1m825XO7hSODOYrH4kHbxxGAwwODBGI/H6DBs5LNara7yl8slGjIcDsHpdrunU6PRCAP2
-r3fPdUcIYeyEfLSAJ0LeAUZHCAt8Al/8/kLIEWDB5YDj0wm8fAP6fVfo
+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):
@@ -88,21 +94,27 @@ 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:
+            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("<pre>")
-                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 = run_cgi
@@ -130,12 +142,11 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         rest = self.path
 
         if rest == '/favicon.ico':
-            raise client.NotFound
-#            self.send_response(200)
-#            self.send_header('Content-Type', 'image/x-ico')
-#            self.end_headers()
-#            self.wfile.write(favicon)
-#            return
+            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:
@@ -204,7 +215,7 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         c = tracker.Client(tracker, self, env)
         c.main()
 
-    LOG_IPADDRESS = 1
+    LOG_IPADDRESS = ROUNDUP_LOG_IP
     def address_string(self):
         if self.LOG_IPADDRESS:
             return self.client_address[0]
@@ -212,6 +223,20 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
             host, port = self.client_address
             return socket.getfqdn(host)
 
+    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:
@@ -231,8 +256,12 @@ else:
         '''
         _svc_name_ = "Roundup Bug Tracker"
         _svc_display_name_ = "Roundup Bug Tracker"
-        address = ('', 8888)
+        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)
@@ -303,22 +332,33 @@ else:
                     break
             return rv
 
-
 def usage(message=''):
-    if message:
-        message = _('Error: %(error)s\n\n')%{'error': message}
-    print _('''%(message)sUsage:
+    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
- -l: sets a filename to log to (instead of stdout)
+ -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. Note: on Windows the PID argument is needed,
-     but ignored. The -l option *must* be specified if this option is.
+     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
@@ -367,16 +407,7 @@ 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.
     '''
     # time out after a minute if we can
@@ -384,29 +415,68 @@ def run():
     if hasattr(socket, 'setdefaulttimeout'):
         socket.setdefaulttimeout(60)
 
-    hostname = ''
-    port = 8080
-    pidfile = None
-    logfile = None
+    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:hN')
+            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
@@ -431,42 +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:
-        if RoundupService:
-            # don't do any other stuff
-            RoundupService.address = address
-            return win32serviceutil.HandleCommandLine(RoundupService)
-        elif not hasattr(os, 'fork'):
+        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:
         # appending, unbuffered
         sys.stdout = sys.stderr = open(logfile, 'a', 0)
 
-    httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
-    print _('Roundup server started on %(address)s')%locals()
+    if success_message:
+        print success_message
+    else:
+        print _('Roundup server started on %(address)s')%locals()
+
     try:
         httpd.serve_forever()
     except KeyboardInterrupt: