Code

More SSL fixes. SSL wants the underlying socket non-blocking. So we
[roundup.git] / roundup / scripts / roundup_server.py
1 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
2 # This module is free software, and you may redistribute it and/or modify
3 # under the same terms as Python, so long as this copyright message and
4 # disclaimer are retained in their original form.
5 #
6 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
7 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
8 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
9 # POSSIBILITY OF SUCH DAMAGE.
10 #
11 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
12 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
14 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 #
18 """Command-line script that runs a server over roundup.cgi.client.
20 $Id: roundup_server.py,v 1.94 2007-09-25 04:27:12 jpend Exp $
21 """
22 __docformat__ = 'restructuredtext'
24 import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
25 import ConfigParser, BaseHTTPServer, SocketServer, StringIO
27 try:
28     from OpenSSL import SSL
29 except ImportError:
30     SSL = None
32 from time import sleep
34 # python version check
35 from roundup import configuration, version_check
36 from roundup import __version__ as roundup_version
38 # Roundup modules of use here
39 from roundup.cgi import cgitb, client
40 from roundup.cgi.PageTemplates.PageTemplate import PageTemplate
41 import roundup.instance
42 from roundup.i18n import _
44 # "default" favicon.ico
45 # generate by using "icotool" and tools/base64
46 import zlib, base64
47 favico = zlib.decompress(base64.decodestring('''
48 eJztjr1PmlEUh59XgVoshdYPWorFIhaRFq0t9pNq37b60lYSTRzcTFw6GAfj5gDYaF0dTB0MxMSE
49 gQQd3FzKJiEC0UCIUUN1M41pV2JCXySg/0ITn5tfzvmdc+85FwT56HSc81UJjXJsk1UsNcsSqCk1
50 BS64lK+vr7OyssLJyQl2ux2j0cjU1BQajYZIJEIwGMRms+H3+zEYDExOTjI2Nsbm5iZWqxWv18vW
51 1hZDQ0Ok02kmJiY4Ojpienqa3d1dxsfHUSqVeDwe5ufnyeVyrK6u4nK5ODs7Y3FxEYfDwdzcHCaT
52 icPDQ5LJJIIgMDIyQj6fZ39/n+3tbdbW1pAkiYWFBWZmZtjb2yMejzM8PEwgEMDn85HNZonFYqjV
53 asLhMMvLy2QyGfR6PaOjowwODmKxWDg+PkalUhEKhSgUCiwtLWE2m9nZ2UGhULCxscHp6SmpVIpo
54 NMrs7CwHBwdotVoSiQRXXPG/IzY7RHtt922xjFRb01H1XhKfPBNbi/7my7rrLXJ88eppvxwEfV3f
55 NY3Y6exofVdsV3+2wnPFDdPjB83n7xuVpcFvygPbGwxF31LZIKrQDfR2Xvh7lmrX654L/7bvlnng
56 bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g
57 23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI=
58 '''.strip()))
60 DEFAULT_PORT = 8080
62 # See what types of multiprocess server are available
63 # Note: the order is important.  Preferred multiprocess type
64 #   is the last element of this list.
65 # "debug" means "none" + no tracker/template cache
66 MULTIPROCESS_TYPES = ["debug", "none"]
67 try:
68     import thread
69 except ImportError:
70     pass
71 else:
72     MULTIPROCESS_TYPES.append("thread")
73 if hasattr(os, 'fork'):
74     MULTIPROCESS_TYPES.append("fork")
75 DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
77 def auto_ssl():
78     print _('WARNING: generating temporary SSL certificate')
79     import OpenSSL, time, random, sys
80     pkey = OpenSSL.crypto.PKey()
81     pkey.generate_key(OpenSSL.crypto.TYPE_RSA, 768)
82     cert = OpenSSL.crypto.X509()
83     cert.set_serial_number(random.randint(0, sys.maxint))
84     cert.gmtime_adj_notBefore(0)
85     cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) # one year
86     cert.get_subject().CN = '*'
87     cert.get_subject().O = 'Roundup Dummy Certificate'
88     cert.get_issuer().CN = 'Roundup Dummy Certificate Authority'
89     cert.get_issuer().O = 'Self-Signed'
90     cert.set_pubkey(pkey)
91     cert.sign(pkey, 'md5')
92     ctx = SSL.Context(SSL.SSLv23_METHOD)
93     ctx.use_privatekey(pkey)
94     ctx.use_certificate(cert)
96     return ctx
98 class SecureHTTPServer(BaseHTTPServer.HTTPServer):
99     def __init__(self, server_address, HandlerClass, ssl_pem=None):
100         assert SSL, "pyopenssl not installed"
101         BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
102         self.socket = socket.socket(self.address_family, self.socket_type)
103         if ssl_pem:
104             ctx = SSL.Context(SSL.SSLv23_METHOD)
105             ctx.use_privatekey_file(ssl_pem)
106             ctx.use_certificate_file(ssl_pem)
107         else:
108             ctx = auto_ssl()
109         self.ssl_context = ctx
110         self.socket = SSL.Connection(ctx, self.socket)
111         self.server_bind()
112         self.server_activate()
114     def get_request(self):
115         (conn, info) = self.socket.accept()
116         if self.ssl_context:
118             class RetryingFile(object):
119                 """ SSL.Connection objects can return Want__Error
120                     on recv/write, meaning "try again". We'll handle
121                     the try looping here """
122                 def __init__(self, fileobj):
123                     self.__fileobj = fileobj
125                 def readline(self, *args):
126                     """ SSL.Connection can return WantRead """
127                     while True:
128                         try:
129                             return self.__fileobj.readline(*args)
130                         except SSL.WantReadError:
131                             sleep (.1)
133                 def read(self, *args):
134                     """ SSL.Connection can return WantRead """
135                     while True:
136                         try:
137                             return self.__fileobj.read(*args)
138                         except SSL.WantReadError:
139                             sleep (.1)
141                 def __getattr__(self, attrib):
142                     return getattr(self.__fileobj, attrib)
144             class ConnFixer(object):
145                 """ wraps an SSL socket so that it implements makefile
146                     which the HTTP handlers require """
147                 def __init__(self, conn):
148                     self.__conn = conn
149                 def makefile(self, mode, bufsize):
150                     fo = socket._fileobject(self.__conn, mode, bufsize)
151                     return RetryingFile(fo)
153                 def __getattr__(self, attrib):
154                     return getattr(self.__conn, attrib)
156             conn = ConnFixer(conn)
157         return (conn, info)
159 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
160     TRACKER_HOMES = {}
161     TRACKERS = None
162     LOG_IPADDRESS = 1
163     DEBUG_MODE = False
164     CONFIG = None
166     def get_tracker(self, name):
167         """Return a tracker instance for given tracker name"""
168         # Note: try/except KeyError works faster that has_key() check
169         #   if the key is usually found in the dictionary
170         #
171         # Return cached tracker instance if we have a tracker cache
172         if self.TRACKERS:
173             try:
174                 return self.TRACKERS[name]
175             except KeyError:
176                 pass
177         # No cached tracker.  Look for home path.
178         try:
179             tracker_home = self.TRACKER_HOMES[name]
180         except KeyError:
181             raise client.NotFound
182         # open the instance
183         tracker = roundup.instance.open(tracker_home)
184         # and cache it if we have a tracker cache
185         if self.TRACKERS:
186             self.TRACKERS[name] = tracker
187         return tracker
189     def run_cgi(self):
190         """ Execute the CGI command. Wrap an innner call in an error
191             handler so all errors can be caught.
192         """
193         save_stdin = sys.stdin
194         sys.stdin = self.rfile
195         try:
196             self.inner_run_cgi()
197         except client.NotFound:
198             self.send_error(404, self.path)
199         except client.Unauthorised, message:
200             self.send_error(403, '%s (%s)'%(self.path, message))
201         except:
202             exc, val, tb = sys.exc_info()
203             if hasattr(socket, 'timeout') and isinstance(val, socket.timeout):
204                 self.log_error('timeout')
205             else:
206                 # it'd be nice to be able to detect if these are going to have
207                 # any effect...
208                 self.send_response(400)
209                 self.send_header('Content-Type', 'text/html')
210                 self.end_headers()
211                 if self.DEBUG_MODE:
212                     try:
213                         reload(cgitb)
214                         self.wfile.write(cgitb.breaker())
215                         self.wfile.write(cgitb.html())
216                     except:
217                         s = StringIO.StringIO()
218                         traceback.print_exc(None, s)
219                         self.wfile.write("<pre>")
220                         self.wfile.write(cgi.escape(s.getvalue()))
221                         self.wfile.write("</pre>\n")
222                 else:
223                     # user feedback
224                     self.wfile.write(cgitb.breaker())
225                     ts = time.ctime()
226                     self.wfile.write('''<p>%s: An error occurred. Please check
227                     the server log for more infomation.</p>'''%ts)
228                     # out to the logfile
229                     print 'EXCEPTION AT', ts
230                     traceback.print_exc()
231         sys.stdin = save_stdin
233     do_GET = do_POST = do_HEAD = run_cgi
235     def index(self):
236         ''' Print up an index of the available trackers
237         '''
238         keys = self.TRACKER_HOMES.keys()
239         if len(keys) == 1:
240             self.send_response(302)
241             self.send_header('Location', urllib.quote(keys[0]) + '/index')
242             self.end_headers()
243         else:
244             self.send_response(200)
246         self.send_header('Content-Type', 'text/html')
247         self.end_headers()
248         w = self.wfile.write
250         if self.CONFIG and self.CONFIG['TEMPLATE']:
251             template = open(self.CONFIG['TEMPLATE']).read()
252             pt = PageTemplate()
253             pt.write(template)
254             extra = { 'trackers': self.TRACKERS,
255                 'nothing' : None,
256                 'true' : 1,
257                 'false' : 0,
258             }
259             w(pt.pt_render(extra_context=extra))
260         else:
261             w(_('<html><head><title>Roundup trackers index</title></head>\n'
262                 '<body><h1>Roundup trackers index</h1><ol>\n'))
263             keys.sort()
264             for tracker in keys:
265                 w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
266                     'tracker_url': urllib.quote(tracker),
267                     'tracker_name': cgi.escape(tracker)})
268             w('</ol></body></html>')
270     def inner_run_cgi(self):
271         ''' This is the inner part of the CGI handling
272         '''
273         rest = self.path
275         # file-like object for the favicon.ico file information
276         favicon_fileobj = None
278         if rest == '/favicon.ico':
279             # check to see if a custom favicon was specified, and set
280             # favicon_fileobj to the input file
281             if self.CONFIG is not None:
282                 favicon_filepath = os.path.abspath(self.CONFIG['FAVICON'])
284                 if os.access(favicon_filepath, os.R_OK):
285                     favicon_fileobj = open(favicon_filepath, 'rb')
288             if favicon_fileobj is None:
289                 favicon_fileobj = StringIO.StringIO(favico)
291             self.send_response(200)
292             self.send_header('Content-Type', 'image/x-icon')
293             self.end_headers()
295             # this bufsize is completely arbitrary, I picked 4K because it sounded good.
296             # if someone knows of a better buffer size, feel free to plug it in.
297             bufsize = 4 * 1024
298             Processing = True
299             while Processing:
300                 data = favicon_fileobj.read(bufsize)
301                 if len(data) > 0:
302                     self.wfile.write(data)
303                 else:
304                     Processing = False
306             favicon_fileobj.close()
308             return
310         i = rest.rfind('?')
311         if i >= 0:
312             rest, query = rest[:i], rest[i+1:]
313         else:
314             query = ''
316         # no tracker - spit out the index
317         if rest == '/':
318             self.index()
319             return
321         # figure the tracker
322         l_path = rest.split('/')
323         tracker_name = urllib.unquote(l_path[1]).lower()
325         # handle missing trailing '/'
326         if len(l_path) == 2:
327             self.send_response(301)
328             # redirect - XXX https??
329             protocol = 'http'
330             url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
331             self.send_header('Location', url)
332             self.end_headers()
333             self.wfile.write('Moved Permanently')
334             return
336         # figure out what the rest of the path is
337         if len(l_path) > 2:
338             rest = '/'.join(l_path[2:])
339         else:
340             rest = '/'
342         # Set up the CGI environment
343         env = {}
344         env['TRACKER_NAME'] = tracker_name
345         env['REQUEST_METHOD'] = self.command
346         env['PATH_INFO'] = urllib.unquote(rest)
347         if query:
348             env['QUERY_STRING'] = query
349         if self.headers.typeheader is None:
350             env['CONTENT_TYPE'] = self.headers.type
351         else:
352             env['CONTENT_TYPE'] = self.headers.typeheader
353         length = self.headers.getheader('content-length')
354         if length:
355             env['CONTENT_LENGTH'] = length
356         co = filter(None, self.headers.getheaders('cookie'))
357         if co:
358             env['HTTP_COOKIE'] = ', '.join(co)
359         env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
360         env['SCRIPT_NAME'] = ''
361         env['SERVER_NAME'] = self.server.server_name
362         env['SERVER_PORT'] = str(self.server.server_port)
363         env['HTTP_HOST'] = self.headers['host']
364         if os.environ.has_key('CGI_SHOW_TIMING'):
365             env['CGI_SHOW_TIMING'] = os.environ['CGI_SHOW_TIMING']
366         env['HTTP_ACCEPT_LANGUAGE'] = self.headers.get('accept-language')
368         # do the roundup thing
369         tracker = self.get_tracker(tracker_name)
370         tracker.Client(tracker, self, env).main()
372     def address_string(self):
373         if self.LOG_IPADDRESS:
374             return self.client_address[0]
375         else:
376             host, port = self.client_address
377             return socket.getfqdn(host)
379     def log_message(self, format, *args):
380         ''' Try to *safely* log to stderr.
381         '''
382         try:
383             BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
384                 format, *args)
385         except IOError:
386             # stderr is no longer viable
387             pass
389     def start_response(self, headers, response):
390         self.send_response(response)
391         for key, value in headers:
392             self.send_header(key, value)
393         self.end_headers()
395 def error():
396     exc_type, exc_value = sys.exc_info()[:2]
397     return _('Error: %s: %s' % (exc_type, exc_value))
399 def setgid(group):
400     if group is None:
401         return
402     if not hasattr(os, 'setgid'):
403         return
405     # if root, setgid to the running user
406     if os.getuid():
407         print _('WARNING: ignoring "-g" argument, not root')
408         return
410     try:
411         import grp
412     except ImportError:
413         raise ValueError, _("Can't change groups - no grp module")
414     try:
415         try:
416             gid = int(group)
417         except ValueError:
418             gid = grp.getgrnam(group)[2]
419         else:
420             grp.getgrgid(gid)
421     except KeyError:
422         raise ValueError,_("Group %(group)s doesn't exist")%locals()
423     os.setgid(gid)
425 def setuid(user):
426     if not hasattr(os, 'getuid'):
427         return
429     # People can remove this check if they're really determined
430     if user is None:
431         if os.getuid():
432             return
433         raise ValueError, _("Can't run as root!")
435     if os.getuid():
436         print _('WARNING: ignoring "-u" argument, not root')
437         return
439     try:
440         import pwd
441     except ImportError:
442         raise ValueError, _("Can't change users - no pwd module")
443     try:
444         try:
445             uid = int(user)
446         except ValueError:
447             uid = pwd.getpwnam(user)[2]
448         else:
449             pwd.getpwuid(uid)
450     except KeyError:
451         raise ValueError, _("User %(user)s doesn't exist")%locals()
452     os.setuid(uid)
454 class TrackerHomeOption(configuration.FilePathOption):
456     # Tracker homes do not need any description strings
457     def format(self):
458         return "%(name)s = %(value)s\n" % {
459                 "name": self.setting,
460                 "value": self.value2str(self._value),
461             }
463 class ServerConfig(configuration.Config):
465     SETTINGS = (
466             ("main", (
467             (configuration.Option, "host", "",
468                 "Host name of the Roundup web server instance.\n"
469                 "If empty, listen on all network interfaces."),
470             (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
471                 "Port to listen on."),
472             (configuration.NullableFilePathOption, "favicon", "favicon.ico",
473                 "Path to favicon.ico image file."
474                 "  If unset, built-in favicon.ico is used."),
475             (configuration.NullableOption, "user", "",
476                 "User ID as which the server will answer requests.\n"
477                 "In order to use this option, "
478                 "the server must be run initially as root.\n"
479                 "Availability: Unix."),
480             (configuration.NullableOption, "group", "",
481                 "Group ID as which the server will answer requests.\n"
482                 "In order to use this option, "
483                 "the server must be run initially as root.\n"
484                 "Availability: Unix."),
485             (configuration.BooleanOption, "nodaemon", "no",
486                 "don't fork (this overrides the pidfile mechanism)'"),
487             (configuration.BooleanOption, "log_hostnames", "no",
488                 "Log client machine names instead of IP addresses "
489                 "(much slower)"),
490             (configuration.NullableFilePathOption, "pidfile", "",
491                 "File to which the server records "
492                 "the process id of the daemon.\n"
493                 "If this option is not set, "
494                 "the server will run in foreground\n"),
495             (configuration.NullableFilePathOption, "logfile", "",
496                 "Log file path.  If unset, log to stderr."),
497             (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
498                 "Set processing of each request in separate subprocess.\n"
499                 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
500             (configuration.NullableFilePathOption, "template", "",
501                 "Tracker index template. If unset, built-in will be used."),
502             (configuration.BooleanOption, "ssl", "no",
503                 "Enable SSL support (requires pyopenssl)"),
504             (configuration.NullableFilePathOption, "pem", "",
505                 "PEM file used for SSL. A temporary self-signed certificate\n"
506                 "will be used if left blank."),
507         )),
508         ("trackers", (), "Roundup trackers to serve.\n"
509             "Each option in this section defines single Roundup tracker.\n"
510             "Option name identifies the tracker and will appear in the URL.\n"
511             "Option value is tracker home directory path.\n"
512             "The path may be either absolute or relative\n"
513             "to the directory containig this config file."),
514     )
516     # options recognized by config
517     OPTIONS = {
518         "host": "n:",
519         "port": "p:",
520         "group": "g:",
521         "user": "u:",
522         "logfile": "l:",
523         "pidfile": "d:",
524         "nodaemon": "D",
525         "log_hostnames": "N",
526         "multiprocess": "t:",
527         "template": "i:",
528         "ssl": "s",
529         "pem": "e:",
530     }
532     def __init__(self, config_file=None):
533         configuration.Config.__init__(self, config_file, self.SETTINGS)
534         self.sections.append("trackers")
536     def _adjust_options(self, config):
537         """Add options for tracker homes"""
538         # return early if there are no tracker definitions.
539         # trackers must be specified on the command line.
540         if not config.has_section("trackers"):
541             return
542         # config defaults appear in all sections.
543         # filter them out.
544         defaults = config.defaults().keys()
545         for name in config.options("trackers"):
546             if name not in defaults:
547                 self.add_option(TrackerHomeOption(self, "trackers", name))
549     def getopt(self, args, short_options="", long_options=(),
550         config_load_options=("C", "config"), **options
551     ):
552         options.update(self.OPTIONS)
553         return configuration.Config.getopt(self, args,
554             short_options, long_options, config_load_options, **options)
556     def _get_name(self):
557         return "Roundup server"
559     def trackers(self):
560         """Return tracker definitions as a list of (name, home) pairs"""
561         trackers = []
562         for option in self._get_section_options("trackers"):
563             trackers.append((option, os.path.abspath(
564                 self["TRACKERS_" + option.upper()])))
565         return trackers
567     def set_logging(self):
568         """Initialise logging to the configured file, if any."""
569         # appending, unbuffered
570         sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0)
572     def get_server(self):
573         """Return HTTP server object to run"""
574         # we don't want the cgi module interpreting the command-line args ;)
575         sys.argv = sys.argv[:1]
577         # preload all trackers unless we are in "debug" mode
578         tracker_homes = self.trackers()
579         if self["MULTIPROCESS"] == "debug":
580             trackers = None
581         else:
582             trackers = dict([(name, roundup.instance.open(home, optimize=1))
583                 for (name, home) in tracker_homes])
585         # build customized request handler class
586         class RequestHandler(RoundupRequestHandler):
587             LOG_IPADDRESS = not self["LOG_HOSTNAMES"]
588             TRACKER_HOMES = dict(tracker_homes)
589             TRACKERS = trackers
590             DEBUG_MODE = self["MULTIPROCESS"] == "debug"
591             CONFIG = self
593         if self["SSL"]:
594             base_server = SecureHTTPServer
595         else:
596             # time out after a minute if we can
597             # This sets the socket to non-blocking. SSL needs a blocking
598             # socket, so we do this only for non-SSL connections.
599             if hasattr(socket, 'setdefaulttimeout'):
600                 socket.setdefaulttimeout(60)
601             base_server = BaseHTTPServer.HTTPServer
603         # obtain request server class
604         if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
605             print _("Multiprocess mode \"%s\" is not available, "
606                 "switching to single-process") % self["MULTIPROCESS"]
607             self["MULTIPROCESS"] = "none"
608             server_class = base_server
609         elif self["MULTIPROCESS"] == "fork":
610             class ForkingServer(SocketServer.ForkingMixIn,
611                 base_server):
612                     pass
613             server_class = ForkingServer
614         elif self["MULTIPROCESS"] == "thread":
615             class ThreadingServer(SocketServer.ThreadingMixIn,
616                 base_server):
617                     pass
618             server_class = ThreadingServer
619         else:
620             server_class = base_server
622         # obtain server before changing user id - allows to
623         # use port < 1024 if started as root
624         try:
625             args = ((self["HOST"], self["PORT"]), RequestHandler)
626             kwargs = {}
627             if self["SSL"]:
628                 kwargs['ssl_pem'] = self["PEM"]
629             httpd = server_class(*args, **kwargs)
630         except socket.error, e:
631             if e[0] == errno.EADDRINUSE:
632                 raise socket.error, \
633                     _("Unable to bind to port %s, port already in use.") \
634                     % self["PORT"]
635             raise
636         # change user and/or group
637         setgid(self["GROUP"])
638         setuid(self["USER"])
639         # return the server
640         return httpd
642 try:
643     import win32serviceutil
644 except:
645     RoundupService = None
646 else:
648     # allow the win32
649     import win32service
651     class SvcShutdown(Exception):
652         pass
654     class RoundupService(win32serviceutil.ServiceFramework):
656         _svc_name_ = "roundup"
657         _svc_display_name_ = "Roundup Bug Tracker"
659         running = 0
660         server = None
662         def SvcDoRun(self):
663             import servicemanager
664             self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
665             config = ServerConfig()
666             (optlist, args) = config.getopt(sys.argv[1:])
667             if not config["LOGFILE"]:
668                 servicemanager.LogMsg(servicemanager.EVENTLOG_ERROR_TYPE,
669                     servicemanager.PYS_SERVICE_STOPPED,
670                     (self._svc_display_name_, "\r\nMissing logfile option"))
671                 self.ReportServiceStatus(win32service.SERVICE_STOPPED)
672                 return
673             config.set_logging()
674             self.server = config.get_server()
675             self.running = 1
676             self.ReportServiceStatus(win32service.SERVICE_RUNNING)
677             servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
678                 servicemanager.PYS_SERVICE_STARTED, (self._svc_display_name_,
679                     " at %s:%s" % (config["HOST"], config["PORT"])))
680             while self.running:
681                 self.server.handle_request()
682             servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
683                 servicemanager.PYS_SERVICE_STOPPED,
684                 (self._svc_display_name_, ""))
685             self.ReportServiceStatus(win32service.SERVICE_STOPPED)
687         def SvcStop(self):
688             self.running = 0
689             # make dummy connection to self to terminate blocking accept()
690             addr = self.server.socket.getsockname()
691             if addr[0] == "0.0.0.0":
692                 addr = ("127.0.0.1", addr[1])
693             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
694             sock.connect(addr)
695             sock.close()
696             self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
698 def usage(message=''):
699     if RoundupService:
700         os_part = \
701 ""''' -c <Command>  Windows Service options.
702                If you want to run the server as a Windows Service, you
703                must use configuration file to specify tracker homes.
704                Logfile option is required to run Roundup Tracker service.
705                Typing "roundup-server -c help" shows Windows Services
706                specifics.'''
707     else:
708         os_part = ""''' -u <UID>      runs the Roundup web server as this UID
709  -g <GID>      runs the Roundup web server as this GID
710  -d <PIDfile>  run the server in the background and write the server's PID
711                to the file indicated by PIDfile. The -l option *must* be
712                specified if -d is used.'''
713     if message:
714         message += '\n'
715     print _('''%(message)sUsage: roundup-server [options] [name=tracker home]*
717 Options:
718  -v            print the Roundup version number and exit
719  -h            print this text and exit
720  -S            create or update configuration file and exit
721  -C <fname>    use configuration file <fname>
722  -n <name>     set the host name of the Roundup web server instance
723  -p <port>     set the port to listen on (default: %(port)s)
724  -l <fname>    log to the file indicated by fname instead of stderr/stdout
725  -N            log client machine names instead of IP addresses (much slower)
726  -i <fname>    set tracker index template
727  -s            enable SSL
728  -e <fname>    PEM file containing SSL key and certificate
729  -t <mode>     multiprocess mode (default: %(mp_def)s).
730                Allowed values: %(mp_types)s.
731 %(os_part)s
733 Long options:
734  --version          print the Roundup version number and exit
735  --help             print this text and exit
736  --save-config      create or update configuration file and exit
737  --config <fname>   use configuration file <fname>
738  All settings of the [main] section of the configuration file
739  also may be specified in form --<name>=<value>
741 Examples:
743  roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\
744     -n localhost -p 8917 -l /var/log/roundup.log \\
745     support=/var/spool/roundup-trackers/support
747  roundup-server -C /opt/roundup/etc/roundup-server.ini
749  roundup-server support=/var/spool/roundup-trackers/support
751  roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\
752     support=/var/spool/roundup-trackers/support
754 Configuration file format:
755    Roundup Server configuration file has common .ini file format.
756    Configuration file created with 'roundup-server -S' contains
757    detailed explanations for each option.  Please see that file
758    for option descriptions.
760 How to use "name=tracker home":
761    These arguments set the tracker home(s) to use. The name is how the
762    tracker is identified in the URL (it's the first part of the URL path).
763    The tracker home is the directory that was identified when you did
764    "roundup-admin init". You may specify any number of these name=home
765    pairs on the command-line. Make sure the name part doesn't include
766    any url-unsafe characters like spaces, as these confuse IE.
767 ''') % {
768     "message": message,
769     "os_part": os_part,
770     "port": DEFAULT_PORT,
771     "mp_def": DEFAULT_MULTIPROCESS,
772     "mp_types": ", ".join(MULTIPROCESS_TYPES),
776 def writepidfile(pidfile):
777     ''' Write a pidfile (only). Do not daemonize. '''
778     pid = os.getpid()
779     if pid:
780         pidfile = open(pidfile, 'w')
781         pidfile.write(str(pid))
782         pidfile.close()
784 def daemonize(pidfile):
785     ''' Turn this process into a daemon.
786         - make sure the sys.std(in|out|err) are completely cut off
787         - make our parent PID 1
789         Write our new PID to the pidfile.
791         From A.M. Kuuchling (possibly originally Greg Ward) with
792         modification from Oren Tirosh, and finally a small mod from me.
793     '''
794     # Fork once
795     if os.fork() != 0:
796         os._exit(0)
798     # Create new session
799     os.setsid()
801     # Second fork to force PPID=1
802     pid = os.fork()
803     if pid:
804         pidfile = open(pidfile, 'w')
805         pidfile.write(str(pid))
806         pidfile.close()
807         os._exit(0)
809     os.chdir("/")
811     # close off std(in|out|err), redirect to devnull so the file
812     # descriptors can't be used again
813     devnull = os.open('/dev/null', 0)
814     os.dup2(devnull, 0)
815     os.dup2(devnull, 1)
816     os.dup2(devnull, 2)
818 undefined = []
819 def run(port=undefined, success_message=None):
820     ''' Script entry point - handle args and figure out what to to.
821     '''
822     config = ServerConfig()
823     # additional options
824     short_options = "hvS"
825     if RoundupService:
826         short_options += 'c'
827     try:
828         (optlist, args) = config.getopt(sys.argv[1:],
829             short_options, ("help", "version", "save-config",))
830     except (getopt.GetoptError, configuration.ConfigurationError), e:
831         usage(str(e))
832         return
834     # if running in windows service mode, don't do any other stuff
835     if ("-c", "") in optlist:
836         # acquire command line options recognized by service
837         short_options = "cC:"
838         long_options = ["config"]
839         for (long_name, short_name) in config.OPTIONS.items():
840             short_options += short_name
841             long_name = long_name.lower().replace("_", "-")
842             if short_name[-1] == ":":
843                 long_name += "="
844             long_options.append(long_name)
845         optlist = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
846         svc_args = []
847         for (opt, arg) in optlist:
848             if opt in ("-C", "-l"):
849                 # make sure file name is absolute
850                 svc_args.extend((opt, os.path.abspath(arg)))
851             elif opt in ("--config", "--logfile"):
852                 # ditto, for long options
853                 svc_args.append("=".join(opt, os.path.abspath(arg)))
854             elif opt != "-c":
855                 svc_args.extend(opt)
856         RoundupService._exe_args_ = " ".join(svc_args)
857         # pass the control to serviceutil
858         win32serviceutil.HandleCommandLine(RoundupService,
859             argv=sys.argv[:1] + args)
860         return
862     # add tracker names from command line.
863     # this is done early to let '--save-config' handle the trackers.
864     if args:
865         for arg in args:
866             try:
867                 name, home = arg.split('=')
868             except ValueError:
869                 raise ValueError, _("Instances must be name=home")
870             config.add_option(TrackerHomeOption(config, "trackers", name))
871             config["TRACKERS_" + name.upper()] = home
873     # handle remaining options
874     if optlist:
875         for (opt, arg) in optlist:
876             if opt in ("-h", "--help"):
877                 usage()
878             elif opt in ("-v", "--version"):
879                 print '%s (python %s)' % (roundup_version,
880                     sys.version.split()[0])
881             elif opt in ("-S", "--save-config"):
882                 config.save()
883                 print _("Configuration saved to %s") % config.filepath
884         # any of the above options prevent server from running
885         return
887     # port number in function arguments overrides config and command line
888     if port is not undefined:
889         config.PORT = port
891     if config["LOGFILE"]:
892         config["LOGFILE"] = os.path.abspath(config["LOGFILE"])
893         # switch logging from stderr/stdout to logfile
894         config.set_logging()
895     if config["PIDFILE"]:
896         config["PIDFILE"] = os.path.abspath(config["PIDFILE"])
898     # fork the server from our parent if a pidfile is specified
899     if config["PIDFILE"]:
900         if not hasattr(os, 'fork'):
901             print _("Sorry, you can't run the server as a daemon"
902                 " on this Operating System")
903             sys.exit(0)
904         else:
905             if config['NODAEMON']:
906                 writepidfile(config["PIDFILE"])
907             else:
908                 daemonize(config["PIDFILE"])
910     # create the server
911     httpd = config.get_server()
913     if success_message:
914         print success_message
915     else:
916         print _('Roundup server started on %(HOST)s:%(PORT)s') \
917             % config
919     try:
920         httpd.serve_forever()
921     except KeyboardInterrupt:
922         print 'Keyboard Interrupt: exiting'
924 if __name__ == '__main__':
925     run()
927 # vim: sts=4 sw=4 et si