X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fscripts%2Froundup_server.py;h=2e26218fe0b6930312661465dcdd056e776872e7;hb=a0396e58a791fdd349cfd376751637d99b846473;hp=c14cdf6abf597b70ae1e60422ce2b9333164cac8;hpb=ebceedf66795d79ee76ecc869abdcd8e65ac3a81;p=roundup.git diff --git a/roundup/scripts/roundup_server.py b/roundup/scripts/roundup_server.py index c14cdf6..2e26218 100644 --- a/roundup/scripts/roundup_server.py +++ b/roundup/scripts/roundup_server.py @@ -13,57 +13,32 @@ # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -# +# """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 $ +$Id: roundup_server.py,v 1.94 2007-09-25 04:27:12 jpend Exp $ """ __docformat__ = 'restructuredtext' +import errno, cgi, getopt, os, socket, sys, traceback, urllib, time +import ConfigParser, BaseHTTPServer, SocketServer, StringIO + +try: + from OpenSSL import SSL +except ImportError: + SSL = None + # python version check -from roundup import version_check +from roundup import configuration, version_check from roundup import __version__ as roundup_version -import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp -import BaseHTTPServer, socket, errno - # Roundup modules of use here from roundup.cgi import cgitb, client +from roundup.cgi.PageTemplates.PageTemplate import PageTemplate import roundup.instance from roundup.i18n import _ -try: - import signal -except: - signal = None - -# -## Configuration -# - -# This indicates where the Roundup trackers live. They're given as NAME -> -# TRACKER_HOME, where the NAME part is used in the URL to select the -# appropriate reacker. -# 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 = { -# 'example': '/path/to/example', -} - -ROUNDUP_USER = None -ROUNDUP_GROUP = None -ROUNDUP_LOG_IP = 1 -HOSTNAME = '' -PORT = 8080 -PIDFILE = None -LOGFILE = None - - -# -## end configuration -# - # "default" favicon.ico # generate by using "icotool" and tools/base64 import zlib, base64 @@ -80,9 +55,134 @@ bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g 23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI= '''.strip())) +DEFAULT_PORT = 8080 + +# See what types of multiprocess server are available +# Note: the order is important. Preferred multiprocess type +# is the last element of this list. +# "debug" means "none" + no tracker/template cache +MULTIPROCESS_TYPES = ["debug", "none"] +try: + import thread +except ImportError: + pass +else: + MULTIPROCESS_TYPES.append("thread") +if hasattr(os, 'fork'): + MULTIPROCESS_TYPES.append("fork") +DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1] + +def auto_ssl(): + print _('WARNING: generating temporary SSL certificate') + import OpenSSL, random + pkey = OpenSSL.crypto.PKey() + pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 768) + cert = OpenSSL.crypto.X509() + cert.set_serial_number(random.randint(0, sys.maxint)) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) # one year + cert.get_subject().CN = '*' + cert.get_subject().O = 'Roundup Dummy Certificate' + cert.get_issuer().CN = 'Roundup Dummy Certificate Authority' + cert.get_issuer().O = 'Self-Signed' + cert.set_pubkey(pkey) + cert.sign(pkey, 'md5') + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey(pkey) + ctx.use_certificate(cert) + + return ctx + +class SecureHTTPServer(BaseHTTPServer.HTTPServer): + def __init__(self, server_address, HandlerClass, ssl_pem=None): + assert SSL, "pyopenssl not installed" + BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass) + self.socket = socket.socket(self.address_family, self.socket_type) + if ssl_pem: + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey_file(ssl_pem) + ctx.use_certificate_file(ssl_pem) + else: + ctx = auto_ssl() + self.ssl_context = ctx + self.socket = SSL.Connection(ctx, self.socket) + self.server_bind() + self.server_activate() + + def get_request(self): + (conn, info) = self.socket.accept() + if self.ssl_context: + + class RetryingFile(object): + """ SSL.Connection objects can return Want__Error + on recv/write, meaning "try again". We'll handle + the try looping here """ + def __init__(self, fileobj): + self.__fileobj = fileobj + + def readline(self, *args): + """ SSL.Connection can return WantRead """ + while True: + try: + return self.__fileobj.readline(*args) + except SSL.WantReadError: + time.sleep(.1) + + def read(self, *args): + """ SSL.Connection can return WantRead """ + while True: + try: + return self.__fileobj.read(*args) + except SSL.WantReadError: + time.sleep(.1) + + def __getattr__(self, attrib): + return getattr(self.__fileobj, attrib) + + class ConnFixer(object): + """ wraps an SSL socket so that it implements makefile + which the HTTP handlers require """ + def __init__(self, conn): + self.__conn = conn + def makefile(self, mode, bufsize): + fo = socket._fileobject(self.__conn, mode, bufsize) + return RetryingFile(fo) + + def __getattr__(self, attrib): + return getattr(self.__conn, attrib) + + conn = ConnFixer(conn) + return (conn, info) + class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - TRACKER_HOMES = TRACKER_HOMES - ROUNDUP_USER = ROUNDUP_USER + TRACKER_HOMES = {} + TRACKERS = None + LOG_IPADDRESS = 1 + DEBUG_MODE = False + CONFIG = None + + def get_tracker(self, name): + """Return a tracker instance for given tracker name""" + # Note: try/except KeyError works faster that has_key() check + # if the key is usually found in the dictionary + # + # Return cached tracker instance if we have a tracker cache + if self.TRACKERS: + try: + return self.TRACKERS[name] + except KeyError: + pass + # No cached tracker. Look for home path. + try: + tracker_home = self.TRACKER_HOMES[name] + except KeyError: + raise client.NotFound + # open the instance + tracker = roundup.instance.open(tracker_home) + # and cache it if we have a tracker cache + if self.TRACKERS: + self.TRACKERS[name] = tracker + return tracker def run_cgi(self): """ Execute the CGI command. Wrap an innner call in an error @@ -94,61 +194,115 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.inner_run_cgi() except client.NotFound: self.send_error(404, self.path) - except client.Unauthorised: - self.send_error(403, self.path) + except client.Unauthorised, message: + self.send_error(403, '%s (%s)'%(self.path, message)) except: exc, val, tb = sys.exc_info() - if hasattr(socket, 'timeout') and exc == socket.timeout: - s = StringIO.StringIO() - traceback.print_exc(None, s) - self.log_message(str(s.getvalue())) + if hasattr(socket, 'timeout') and isinstance(val, socket.timeout): + self.log_error('timeout') 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) + if self.DEBUG_MODE: + 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("
")
+                        self.wfile.write(cgi.escape(s.getvalue()))
+                        self.wfile.write("
\n") + else: + # user feedback self.wfile.write(cgitb.breaker()) - self.wfile.write(cgitb.html()) - except: - s = StringIO.StringIO() - traceback.print_exc(None, s) - self.wfile.write("
")
-                    self.wfile.write(cgi.escape(s.getvalue()))
-                    self.wfile.write("
\n") + ts = time.ctime() + self.wfile.write('''

%s: An error occurred. Please check + the server log for more infomation.

'''%ts) + # out to the logfile + print 'EXCEPTION AT', ts + traceback.print_exc() sys.stdin = save_stdin - do_GET = do_POST = run_cgi + do_GET = do_POST = do_HEAD = run_cgi def index(self): ''' Print up an index of the available trackers ''' - self.send_response(200) + keys = self.TRACKER_HOMES.keys() + if len(keys) == 1: + self.send_response(302) + self.send_header('Location', urllib.quote(keys[0]) + '/index') + self.end_headers() + else: + self.send_response(200) + self.send_header('Content-Type', 'text/html') self.end_headers() w = self.wfile.write - w(_('Roundup trackers index\n')) - w(_('

Roundup trackers index

    \n')) - keys = self.TRACKER_HOMES.keys() - keys.sort() - for tracker in keys: - w(_('
  1. %(tracker_name)s\n')%{ - 'tracker_url': urllib.quote(tracker), - 'tracker_name': cgi.escape(tracker)}) - w(_('
')) + + if self.CONFIG and self.CONFIG['TEMPLATE']: + template = open(self.CONFIG['TEMPLATE']).read() + pt = PageTemplate() + pt.write(template) + extra = { 'trackers': self.TRACKERS, + 'nothing' : None, + 'true' : 1, + 'false' : 0, + } + w(pt.pt_render(extra_context=extra)) + else: + w(_('Roundup trackers index\n' + '

Roundup trackers index

    \n')) + keys.sort() + for tracker in keys: + w('
  1. %(tracker_name)s\n'%{ + 'tracker_url': urllib.quote(tracker), + 'tracker_name': cgi.escape(tracker)}) + w('
') def inner_run_cgi(self): ''' This is the inner part of the CGI handling ''' rest = self.path + # file-like object for the favicon.ico file information + favicon_fileobj = None + if rest == '/favicon.ico': + # check to see if a custom favicon was specified, and set + # favicon_fileobj to the input file + if self.CONFIG is not None: + favicon_filepath = os.path.abspath(self.CONFIG['FAVICON']) + + if os.access(favicon_filepath, os.R_OK): + favicon_fileobj = open(favicon_filepath, 'rb') + + + if favicon_fileobj is None: + favicon_fileobj = StringIO.StringIO(favico) + self.send_response(200) self.send_header('Content-Type', 'image/x-icon') self.end_headers() - self.wfile.write(favico) + + # this bufsize is completely arbitrary, I picked 4K because it sounded good. + # if someone knows of a better buffer size, feel free to plug it in. + bufsize = 4 * 1024 + Processing = True + while Processing: + data = favicon_fileobj.read(bufsize) + if len(data) > 0: + self.wfile.write(data) + else: + Processing = False + + favicon_fileobj.close() + return i = rest.rfind('?') @@ -159,11 +313,12 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # no tracker - spit out the index if rest == '/': - return self.index() + self.index() + return # figure the tracker l_path = rest.split('/') - tracker_name = urllib.unquote(l_path[1]) + tracker_name = urllib.unquote(l_path[1]).lower() # handle missing trailing '/' if len(l_path) == 2: @@ -176,12 +331,6 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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 - # figure out what the rest of the path is if len(l_path) > 2: rest = '/'.join(l_path[2:]) @@ -195,7 +344,6 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): env['PATH_INFO'] = urllib.unquote(rest) if query: env['QUERY_STRING'] = query - host = self.address_string() if self.headers.typeheader is None: env['CONTENT_TYPE'] = self.headers.type else: @@ -211,14 +359,14 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): env['SERVER_NAME'] = self.server.server_name env['SERVER_PORT'] = str(self.server.server_port) env['HTTP_HOST'] = self.headers['host'] + if os.environ.has_key('CGI_SHOW_TIMING'): + env['CGI_SHOW_TIMING'] = os.environ['CGI_SHOW_TIMING'] + env['HTTP_ACCEPT_LANGUAGE'] = self.headers.get('accept-language') - decoded_query = query.replace('+', ' ') - - # do the roundup thang - c = tracker.Client(tracker, self, env) - c.main() + # do the roundup thing + tracker = self.get_tracker(tracker_name) + tracker.Client(tracker, self, env).main() - LOG_IPADDRESS = ROUNDUP_LOG_IP def address_string(self): if self.LOG_IPADDRESS: return self.client_address[0] @@ -236,145 +384,414 @@ class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # stderr is no longer viable pass + def start_response(self, headers, response): + self.send_response(response) + for key, value in headers: + self.send_header(key, value) + self.end_headers() + def error(): exc_type, exc_value = sys.exc_info()[:2] return _('Error: %s: %s' % (exc_type, exc_value)) +def setgid(group): + if group is None: + return + if not hasattr(os, 'setgid'): + return + + # if root, setgid to the running user + if os.getuid(): + print _('WARNING: ignoring "-g" argument, not root') + return + + try: + import grp + except ImportError: + raise ValueError, _("Can't change groups - no grp module") + try: + try: + gid = int(group) + except ValueError: + gid = grp.getgrnam(group)[2] + else: + grp.getgrgid(gid) + except KeyError: + raise ValueError,_("Group %(group)s doesn't exist")%locals() + os.setgid(gid) + +def setuid(user): + if not hasattr(os, 'getuid'): + return + + # People can remove this check if they're really determined + if user is None: + if os.getuid(): + return + raise ValueError, _("Can't run as root!") + + if os.getuid(): + print _('WARNING: ignoring "-u" argument, not root') + return + + try: + import pwd + except ImportError: + raise ValueError, _("Can't change users - no pwd module") + try: + try: + uid = int(user) + except ValueError: + uid = pwd.getpwnam(user)[2] + else: + pwd.getpwuid(uid) + except KeyError: + raise ValueError, _("User %(user)s doesn't exist")%locals() + os.setuid(uid) + +class TrackerHomeOption(configuration.FilePathOption): + + # Tracker homes do not need any description strings + def format(self): + return "%(name)s = %(value)s\n" % { + "name": self.setting, + "value": self.value2str(self._value), + } + +class ServerConfig(configuration.Config): + + SETTINGS = ( + ("main", ( + (configuration.Option, "host", "", + "Host name of the Roundup web server instance.\n" + "If empty, listen on all network interfaces."), + (configuration.IntegerNumberOption, "port", DEFAULT_PORT, + "Port to listen on."), + (configuration.NullableFilePathOption, "favicon", "favicon.ico", + "Path to favicon.ico image file." + " If unset, built-in favicon.ico is used."), + (configuration.NullableOption, "user", "", + "User ID as which the server will answer requests.\n" + "In order to use this option, " + "the server must be run initially as root.\n" + "Availability: Unix."), + (configuration.NullableOption, "group", "", + "Group ID as which the server will answer requests.\n" + "In order to use this option, " + "the server must be run initially as root.\n" + "Availability: Unix."), + (configuration.BooleanOption, "nodaemon", "no", + "don't fork (this overrides the pidfile mechanism)'"), + (configuration.BooleanOption, "log_hostnames", "no", + "Log client machine names instead of IP addresses " + "(much slower)"), + (configuration.NullableFilePathOption, "pidfile", "", + "File to which the server records " + "the process id of the daemon.\n" + "If this option is not set, " + "the server will run in foreground\n"), + (configuration.NullableFilePathOption, "logfile", "", + "Log file path. If unset, log to stderr."), + (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS, + "Set processing of each request in separate subprocess.\n" + "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)), + (configuration.NullableFilePathOption, "template", "", + "Tracker index template. If unset, built-in will be used."), + (configuration.BooleanOption, "ssl", "no", + "Enable SSL support (requires pyopenssl)"), + (configuration.NullableFilePathOption, "pem", "", + "PEM file used for SSL. A temporary self-signed certificate\n" + "will be used if left blank."), + )), + ("trackers", (), "Roundup trackers to serve.\n" + "Each option in this section defines single Roundup tracker.\n" + "Option name identifies the tracker and will appear in the URL.\n" + "Option value is tracker home directory path.\n" + "The path may be either absolute or relative\n" + "to the directory containig this config file."), + ) + + # options recognized by config + OPTIONS = { + "host": "n:", + "port": "p:", + "group": "g:", + "user": "u:", + "logfile": "l:", + "pidfile": "d:", + "nodaemon": "D", + "log_hostnames": "N", + "multiprocess": "t:", + "template": "i:", + "ssl": "s", + "pem": "e:", + } + + def __init__(self, config_file=None): + configuration.Config.__init__(self, config_file, self.SETTINGS) + self.sections.append("trackers") + + def _adjust_options(self, config): + """Add options for tracker homes""" + # return early if there are no tracker definitions. + # trackers must be specified on the command line. + if not config.has_section("trackers"): + return + # config defaults appear in all sections. + # filter them out. + defaults = config.defaults().keys() + for name in config.options("trackers"): + if name not in defaults: + self.add_option(TrackerHomeOption(self, "trackers", name)) + + def getopt(self, args, short_options="", long_options=(), + config_load_options=("C", "config"), **options + ): + options.update(self.OPTIONS) + return configuration.Config.getopt(self, args, + short_options, long_options, config_load_options, **options) + + def _get_name(self): + return "Roundup server" + + def trackers(self): + """Return tracker definitions as a list of (name, home) pairs""" + trackers = [] + for option in self._get_section_options("trackers"): + trackers.append((option, os.path.abspath( + self["TRACKERS_" + option.upper()]))) + return trackers + + def set_logging(self): + """Initialise logging to the configured file, if any.""" + # appending, unbuffered + sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0) + + def get_server(self): + """Return HTTP server object to run""" + # we don't want the cgi module interpreting the command-line args ;) + sys.argv = sys.argv[:1] + + # preload all trackers unless we are in "debug" mode + tracker_homes = self.trackers() + if self["MULTIPROCESS"] == "debug": + trackers = None + else: + trackers = dict([(name, roundup.instance.open(home, optimize=1)) + for (name, home) in tracker_homes]) + + # build customized request handler class + class RequestHandler(RoundupRequestHandler): + LOG_IPADDRESS = not self["LOG_HOSTNAMES"] + TRACKER_HOMES = dict(tracker_homes) + TRACKERS = trackers + DEBUG_MODE = self["MULTIPROCESS"] == "debug" + CONFIG = self + + def setup(self): + if self.CONFIG["SSL"]: + # perform initial ssl handshake. This will set + # internal state correctly so that later closing SSL + # socket works (with SSL end-handshake started) + self.request.do_handshake() + RoundupRequestHandler.setup(self) + + def finish(self): + RoundupRequestHandler.finish(self) + if self.CONFIG["SSL"]: + self.request.shutdown() + self.request.close() + + if self["SSL"]: + base_server = SecureHTTPServer + else: + # time out after a minute if we can + # This sets the socket to non-blocking. SSL needs a blocking + # socket, so we do this only for non-SSL connections. + if hasattr(socket, 'setdefaulttimeout'): + socket.setdefaulttimeout(60) + base_server = BaseHTTPServer.HTTPServer + + # obtain request server class + if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES: + print _("Multiprocess mode \"%s\" is not available, " + "switching to single-process") % self["MULTIPROCESS"] + self["MULTIPROCESS"] = "none" + server_class = base_server + elif self["MULTIPROCESS"] == "fork": + class ForkingServer(SocketServer.ForkingMixIn, + base_server): + pass + server_class = ForkingServer + elif self["MULTIPROCESS"] == "thread": + class ThreadingServer(SocketServer.ThreadingMixIn, + base_server): + pass + server_class = ThreadingServer + else: + server_class = base_server + + # obtain server before changing user id - allows to + # use port < 1024 if started as root + try: + args = ((self["HOST"], self["PORT"]), RequestHandler) + kwargs = {} + if self["SSL"]: + kwargs['ssl_pem'] = self["PEM"] + httpd = server_class(*args, **kwargs) + except socket.error, e: + if e[0] == errno.EADDRINUSE: + raise socket.error, \ + _("Unable to bind to port %s, port already in use.") \ + % self["PORT"] + raise + # change user and/or group + setgid(self["GROUP"]) + setuid(self["USER"]) + # return the server + return httpd + try: import win32serviceutil except: RoundupService = None else: + # allow the win32 import win32service - import win32event - from win32event import * - from win32file import * - SvcShutdown = "ServiceShutdown" + class SvcShutdown(Exception): + pass - class RoundupService(win32serviceutil.ServiceFramework, - BaseHTTPServer.HTTPServer): - ''' A Roundup standalone server for Win32 by Ewout Prangsma - ''' - _svc_name_ = "Roundup Bug Tracker" + class RoundupService(win32serviceutil.ServiceFramework): + + _svc_name_ = "roundup" _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) + running = 0 + server = None def SvcDoRun(self): - try: - self.serve_forever() - except SvcShutdown: - pass + import servicemanager + self.ReportServiceStatus(win32service.SERVICE_START_PENDING) + config = ServerConfig() + (optlist, args) = config.getopt(sys.argv[1:]) + if not config["LOGFILE"]: + servicemanager.LogMsg(servicemanager.EVENTLOG_ERROR_TYPE, + servicemanager.PYS_SERVICE_STOPPED, + (self._svc_display_name_, "\r\nMissing logfile option")) + self.ReportServiceStatus(win32service.SERVICE_STOPPED) + return + config.set_logging() + self.server = config.get_server() + self.running = 1 + self.ReportServiceStatus(win32service.SERVICE_RUNNING) + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STARTED, (self._svc_display_name_, + " at %s:%s" % (config["HOST"], config["PORT"]))) + while self.running: + self.server.handle_request() + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STOPPED, + (self._svc_display_name_, "")) + self.ReportServiceStatus(win32service.SERVICE_STOPPED) - 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 + def SvcStop(self): + self.running = 0 + # make dummy connection to self to terminate blocking accept() + addr = self.server.socket.getsockname() + if addr[0] == "0.0.0.0": + addr = ("127.0.0.1", addr[1]) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(addr) + sock.close() + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 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.''' + os_part = \ +""''' -c Windows Service options. + If you want to run the server as a Windows Service, you + must use configuration file to specify tracker homes. + Logfile option is required to run Roundup Tracker service. + 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 - tracker home is the directory that was identified when you did + os_part = ""''' -u runs the Roundup web server as this UID + -g runs the Roundup web server as this GID + -d run the server in the background and write the server's PID + to the file indicated by PIDfile. The -l option *must* be + specified if -d is used.''' + if message: + message += '\n' + print _('''%(message)sUsage: roundup-server [options] [name=tracker home]* + +Options: + -v print the Roundup version number and exit + -h print this text and exit + -S create or update configuration file and exit + -C use configuration file + -n set the host name of the Roundup web server instance + -p set the port to listen on (default: %(port)s) + -l log to the file indicated by fname instead of stderr/stdout + -N log client machine names instead of IP addresses (much slower) + -i set tracker index template + -s enable SSL + -e PEM file containing SSL key and certificate + -t multiprocess mode (default: %(mp_def)s). + Allowed values: %(mp_types)s. +%(os_part)s + +Long options: + --version print the Roundup version number and exit + --help print this text and exit + --save-config create or update configuration file and exit + --config use configuration file + All settings of the [main] section of the configuration file + also may be specified in form --= + +Examples: + + roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\ + -n localhost -p 8917 -l /var/log/roundup.log \\ + support=/var/spool/roundup-trackers/support + + roundup-server -C /opt/roundup/etc/roundup-server.ini + + roundup-server support=/var/spool/roundup-trackers/support + + roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\ + support=/var/spool/roundup-trackers/support + +Configuration file format: + Roundup Server configuration file has common .ini file format. + Configuration file created with 'roundup-server -S' contains + detailed explanations for each option. Please see that file + for option descriptions. + +How to use "name=tracker home": + These arguments set 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 "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. - Make sure the name part doesn't include any url-unsafe characters like - spaces, as these confuse the cookie handling in browsers like IE. -''')%locals() - sys.exit(0) + pairs on the command-line. Make sure the name part doesn't include + any url-unsafe characters like spaces, as these confuse IE. +''') % { + "message": message, + "os_part": os_part, + "port": DEFAULT_PORT, + "mp_def": DEFAULT_MULTIPROCESS, + "mp_types": ", ".join(MULTIPROCESS_TYPES), +} + + +def writepidfile(pidfile): + ''' Write a pidfile (only). Do not daemonize. ''' + pid = os.getpid() + if pid: + pidfile = open(pidfile, 'w') + pidfile.write(str(pid)) + pidfile.close() def daemonize(pidfile): ''' Turn this process into a daemon. @@ -402,150 +819,114 @@ def daemonize(pidfile): os._exit(0) os.chdir("/") - os.umask(0) - # close off sys.std(in|out|err), redirect to devnull so the file + # close off std(in|out|err), redirect to devnull so the file # descriptors can't be used again devnull = os.open('/dev/null', 0) os.dup2(devnull, 0) os.dup2(devnull, 1) os.dup2(devnull, 2) -def run(port=PORT, success_message=None): +undefined = [] +def run(port=undefined, 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 - + config = ServerConfig() + # additional options + short_options = "hvS" + if RoundupService: + short_options += 'c' try: - # handle the command-line args - options = 'n:p:u:d:l:hNv' - if RoundupService: - options += 'c' - - try: - optlist, args = getopt.getopt(sys.argv[1:], options) - except getopt.GetoptError, e: - usage(str(e)) - - user = ROUNDUP_USER - group = None + (optlist, args) = config.getopt(sys.argv[1:], + short_options, ("help", "version", "save-config",)) + except (getopt.GetoptError, configuration.ConfigurationError), e: + usage(str(e)) + return + + # if running in windows service mode, don't do any other stuff + if ("-c", "") in optlist: + # acquire command line options recognized by service + short_options = "cC:" + long_options = ["config"] + for (long_name, short_name) in config.OPTIONS.items(): + short_options += short_name + long_name = long_name.lower().replace("_", "-") + if short_name[-1] == ":": + long_name += "=" + long_options.append(long_name) + optlist = getopt.getopt(sys.argv[1:], short_options, long_options)[0] + svc_args = [] 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 == '-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 - if not os.getuid() and user is not None: - try: - import pwd - except ImportError: - raise ValueError, _("Can't change users - no pwd module") - try: - uid = pwd.getpwnam(user)[2] - except KeyError: - raise ValueError, _("User %(user)s doesn't exist")%locals() - os.setuid(uid) - elif os.getuid() and user is not None: - print _('WARNING: ignoring "-u" argument, not root') - - # People can remove this check if they're really determined - if not os.getuid() and user is None: - raise ValueError, _("Can't run as root!") - - # handle tracker specs - if args: - d = {} - for arg in args: - try: - name, home = arg.split('=') - except ValueError: - raise ValueError, _("Instances must be name=home") - d[name] = os.path.abspath(home) - RoundupRequestHandler.TRACKER_HOMES = d - except SystemExit: - raise - except ValueError: - usage(error()) - except: - print error() - sys.exit(1) - - # we don't want the cgi module interpreting the command-line args ;) - sys.argv = sys.argv[:1] - - if pidfile: + if opt in ("-C", "-l"): + # make sure file name is absolute + svc_args.extend((opt, os.path.abspath(arg))) + elif opt in ("--config", "--logfile"): + # ditto, for long options + svc_args.append("=".join(opt, os.path.abspath(arg))) + elif opt != "-c": + svc_args.extend(opt) + RoundupService._exe_args_ = " ".join(svc_args) + # pass the control to serviceutil + win32serviceutil.HandleCommandLine(RoundupService, + argv=sys.argv[:1] + args) + return + + # add tracker names from command line. + # this is done early to let '--save-config' handle the trackers. + if args: + for arg in args: + try: + name, home = arg.split('=') + except ValueError: + raise ValueError, _("Instances must be name=home") + config.add_option(TrackerHomeOption(config, "trackers", name)) + config["TRACKERS_" + name.upper()] = home + + # handle remaining options + if optlist: + for (opt, arg) in optlist: + if opt in ("-h", "--help"): + usage() + elif opt in ("-v", "--version"): + print '%s (python %s)' % (roundup_version, + sys.version.split()[0]) + elif opt in ("-S", "--save-config"): + config.save() + print _("Configuration saved to %s") % config.filepath + # any of the above options prevent server from running + return + + # port number in function arguments overrides config and command line + if port is not undefined: + config.PORT = port + + if config["LOGFILE"]: + config["LOGFILE"] = os.path.abspath(config["LOGFILE"]) + # switch logging from stderr/stdout to logfile + config.set_logging() + if config["PIDFILE"]: + config["PIDFILE"] = os.path.abspath(config["PIDFILE"]) + + # fork the server from our parent if a pidfile is specified + if config["PIDFILE"]: if not hasattr(os, 'fork'): - print "Sorry, you can't run the server as a daemon on this" \ - 'Operating System' + 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) + if config['NODAEMON']: + writepidfile(config["PIDFILE"]) + else: + daemonize(config["PIDFILE"]) - # redirect stdout/stderr to our logfile - if logfile: - # appending, unbuffered - sys.stdout = sys.stderr = open(logfile, 'a', 0) + # create the server + httpd = config.get_server() if success_message: print success_message else: - print _('Roundup server started on %(address)s')%locals() + print _('Roundup server started on %(HOST)s:%(PORT)s') \ + % config try: httpd.serve_forever() @@ -555,4 +936,4 @@ def run(port=PORT, success_message=None): if __name__ == '__main__': run() -# vim: set filetype=python ts=4 sw=4 et si +# vim: sts=4 sw=4 et si