Code

9427c8fc72383c9c752497e772d24d7c3f6d56be
[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                     line = None
128                     while not line:
129                         try:
130                             line = self.__fileobj.readline(*args)
131                         except SSL.WantReadError:
132                             sleep (.1)
133                             line = None
134                     return line
136                 def read(self, *args):
137                     """ SSL.Connection can return WantRead """
138                     while True:
139                         try:
140                             return self.__fileobj.read(*args)
141                         except SSL.WantReadError:
142                             sleep (.1)
144                 def __getattr__(self, attrib):
145                     return getattr(self.__fileobj, attrib)
147             class ConnFixer(object):
148                 """ wraps an SSL socket so that it implements makefile
149                     which the HTTP handlers require """
150                 def __init__(self, conn):
151                     self.__conn = conn
152                 def makefile(self, mode, bufsize):
153                     fo = socket._fileobject(self.__conn, mode, bufsize)
154                     return RetryingFile(fo)
156                 def __getattr__(self, attrib):
157                     return getattr(self.__conn, attrib)
159             conn = ConnFixer(conn)
160         return (conn, info)
162 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
163     TRACKER_HOMES = {}
164     TRACKERS = None
165     LOG_IPADDRESS = 1
166     DEBUG_MODE = False
167     CONFIG = None
169     def get_tracker(self, name):
170         """Return a tracker instance for given tracker name"""
171         # Note: try/except KeyError works faster that has_key() check
172         #   if the key is usually found in the dictionary
173         #
174         # Return cached tracker instance if we have a tracker cache
175         if self.TRACKERS:
176             try:
177                 return self.TRACKERS[name]
178             except KeyError:
179                 pass
180         # No cached tracker.  Look for home path.
181         try:
182             tracker_home = self.TRACKER_HOMES[name]
183         except KeyError:
184             raise client.NotFound
185         # open the instance
186         tracker = roundup.instance.open(tracker_home)
187         # and cache it if we have a tracker cache
188         if self.TRACKERS:
189             self.TRACKERS[name] = tracker
190         return tracker
192     def run_cgi(self):
193         """ Execute the CGI command. Wrap an innner call in an error
194             handler so all errors can be caught.
195         """
196         save_stdin = sys.stdin
197         sys.stdin = self.rfile
198         try:
199             self.inner_run_cgi()
200         except client.NotFound:
201             self.send_error(404, self.path)
202         except client.Unauthorised, message:
203             self.send_error(403, '%s (%s)'%(self.path, message))
204         except:
205             exc, val, tb = sys.exc_info()
206             if hasattr(socket, 'timeout') and isinstance(val, socket.timeout):
207                 self.log_error('timeout')
208             else:
209                 # it'd be nice to be able to detect if these are going to have
210                 # any effect...
211                 self.send_response(400)
212                 self.send_header('Content-Type', 'text/html')
213                 self.end_headers()
214                 if self.DEBUG_MODE:
215                     try:
216                         reload(cgitb)
217                         self.wfile.write(cgitb.breaker())
218                         self.wfile.write(cgitb.html())
219                     except:
220                         s = StringIO.StringIO()
221                         traceback.print_exc(None, s)
222                         self.wfile.write("<pre>")
223                         self.wfile.write(cgi.escape(s.getvalue()))
224                         self.wfile.write("</pre>\n")
225                 else:
226                     # user feedback
227                     self.wfile.write(cgitb.breaker())
228                     ts = time.ctime()
229                     self.wfile.write('''<p>%s: An error occurred. Please check
230                     the server log for more infomation.</p>'''%ts)
231                     # out to the logfile
232                     print 'EXCEPTION AT', ts
233                     traceback.print_exc()
234         sys.stdin = save_stdin
236     do_GET = do_POST = do_HEAD = run_cgi
238     def index(self):
239         ''' Print up an index of the available trackers
240         '''
241         keys = self.TRACKER_HOMES.keys()
242         if len(keys) == 1:
243             self.send_response(302)
244             self.send_header('Location', urllib.quote(keys[0]) + '/index')
245             self.end_headers()
246         else:
247             self.send_response(200)
249         self.send_header('Content-Type', 'text/html')
250         self.end_headers()
251         w = self.wfile.write
253         if self.CONFIG and self.CONFIG['TEMPLATE']:
254             template = open(self.CONFIG['TEMPLATE']).read()
255             pt = PageTemplate()
256             pt.write(template)
257             extra = { 'trackers': self.TRACKERS,
258                 'nothing' : None,
259                 'true' : 1,
260                 'false' : 0,
261             }
262             w(pt.pt_render(extra_context=extra))
263         else:
264             w(_('<html><head><title>Roundup trackers index</title></head>\n'
265                 '<body><h1>Roundup trackers index</h1><ol>\n'))
266             keys.sort()
267             for tracker in keys:
268                 w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
269                     'tracker_url': urllib.quote(tracker),
270                     'tracker_name': cgi.escape(tracker)})
271             w('</ol></body></html>')
273     def inner_run_cgi(self):
274         ''' This is the inner part of the CGI handling
275         '''
276         rest = self.path
278         # file-like object for the favicon.ico file information
279         favicon_fileobj = None
281         if rest == '/favicon.ico':
282             # check to see if a custom favicon was specified, and set
283             # favicon_fileobj to the input file
284             if self.CONFIG is not None:
285                 favicon_filepath = os.path.abspath(self.CONFIG['FAVICON'])
287                 if os.access(favicon_filepath, os.R_OK):
288                     favicon_fileobj = open(favicon_filepath, 'rb')
291             if favicon_fileobj is None:
292                 favicon_fileobj = StringIO.StringIO(favico)
294             self.send_response(200)
295             self.send_header('Content-Type', 'image/x-icon')
296             self.end_headers()
298             # this bufsize is completely arbitrary, I picked 4K because it sounded good.
299             # if someone knows of a better buffer size, feel free to plug it in.
300             bufsize = 4 * 1024
301             Processing = True
302             while Processing:
303                 data = favicon_fileobj.read(bufsize)
304                 if len(data) > 0:
305                     self.wfile.write(data)
306                 else:
307                     Processing = False
309             favicon_fileobj.close()
311             return
313         i = rest.rfind('?')
314         if i >= 0:
315             rest, query = rest[:i], rest[i+1:]
316         else:
317             query = ''
319         # no tracker - spit out the index
320         if rest == '/':
321             self.index()
322             return
324         # figure the tracker
325         l_path = rest.split('/')
326         tracker_name = urllib.unquote(l_path[1]).lower()
328         # handle missing trailing '/'
329         if len(l_path) == 2:
330             self.send_response(301)
331             # redirect - XXX https??
332             protocol = 'http'
333             url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
334             self.send_header('Location', url)
335             self.end_headers()
336             self.wfile.write('Moved Permanently')
337             return
339         # figure out what the rest of the path is
340         if len(l_path) > 2:
341             rest = '/'.join(l_path[2:])
342         else:
343             rest = '/'
345         # Set up the CGI environment
346         env = {}
347         env['TRACKER_NAME'] = tracker_name
348         env['REQUEST_METHOD'] = self.command
349         env['PATH_INFO'] = urllib.unquote(rest)
350         if query:
351             env['QUERY_STRING'] = query
352         if self.headers.typeheader is None:
353             env['CONTENT_TYPE'] = self.headers.type
354         else:
355             env['CONTENT_TYPE'] = self.headers.typeheader
356         length = self.headers.getheader('content-length')
357         if length:
358             env['CONTENT_LENGTH'] = length
359         co = filter(None, self.headers.getheaders('cookie'))
360         if co:
361             env['HTTP_COOKIE'] = ', '.join(co)
362         env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
363         env['SCRIPT_NAME'] = ''
364         env['SERVER_NAME'] = self.server.server_name
365         env['SERVER_PORT'] = str(self.server.server_port)
366         env['HTTP_HOST'] = self.headers['host']
367         if os.environ.has_key('CGI_SHOW_TIMING'):
368             env['CGI_SHOW_TIMING'] = os.environ['CGI_SHOW_TIMING']
369         env['HTTP_ACCEPT_LANGUAGE'] = self.headers.get('accept-language')
371         # do the roundup thing
372         tracker = self.get_tracker(tracker_name)
373         tracker.Client(tracker, self, env).main()
375     def address_string(self):
376         if self.LOG_IPADDRESS:
377             return self.client_address[0]
378         else:
379             host, port = self.client_address
380             return socket.getfqdn(host)
382     def log_message(self, format, *args):
383         ''' Try to *safely* log to stderr.
384         '''
385         try:
386             BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
387                 format, *args)
388         except IOError:
389             # stderr is no longer viable
390             pass
392     def start_response(self, headers, response):
393         self.send_response(response)
394         for key, value in headers:
395             self.send_header(key, value)
396         self.end_headers()
398 def error():
399     exc_type, exc_value = sys.exc_info()[:2]
400     return _('Error: %s: %s' % (exc_type, exc_value))
402 def setgid(group):
403     if group is None:
404         return
405     if not hasattr(os, 'setgid'):
406         return
408     # if root, setgid to the running user
409     if os.getuid():
410         print _('WARNING: ignoring "-g" argument, not root')
411         return
413     try:
414         import grp
415     except ImportError:
416         raise ValueError, _("Can't change groups - no grp module")
417     try:
418         try:
419             gid = int(group)
420         except ValueError:
421             gid = grp.getgrnam(group)[2]
422         else:
423             grp.getgrgid(gid)
424     except KeyError:
425         raise ValueError,_("Group %(group)s doesn't exist")%locals()
426     os.setgid(gid)
428 def setuid(user):
429     if not hasattr(os, 'getuid'):
430         return
432     # People can remove this check if they're really determined
433     if user is None:
434         if os.getuid():
435             return
436         raise ValueError, _("Can't run as root!")
438     if os.getuid():
439         print _('WARNING: ignoring "-u" argument, not root')
440         return
442     try:
443         import pwd
444     except ImportError:
445         raise ValueError, _("Can't change users - no pwd module")
446     try:
447         try:
448             uid = int(user)
449         except ValueError:
450             uid = pwd.getpwnam(user)[2]
451         else:
452             pwd.getpwuid(uid)
453     except KeyError:
454         raise ValueError, _("User %(user)s doesn't exist")%locals()
455     os.setuid(uid)
457 class TrackerHomeOption(configuration.FilePathOption):
459     # Tracker homes do not need any description strings
460     def format(self):
461         return "%(name)s = %(value)s\n" % {
462                 "name": self.setting,
463                 "value": self.value2str(self._value),
464             }
466 class ServerConfig(configuration.Config):
468     SETTINGS = (
469             ("main", (
470             (configuration.Option, "host", "",
471                 "Host name of the Roundup web server instance.\n"
472                 "If empty, listen on all network interfaces."),
473             (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
474                 "Port to listen on."),
475             (configuration.NullableFilePathOption, "favicon", "favicon.ico",
476                 "Path to favicon.ico image file."
477                 "  If unset, built-in favicon.ico is used."),
478             (configuration.NullableOption, "user", "",
479                 "User ID as which the server will answer requests.\n"
480                 "In order to use this option, "
481                 "the server must be run initially as root.\n"
482                 "Availability: Unix."),
483             (configuration.NullableOption, "group", "",
484                 "Group ID as which the server will answer requests.\n"
485                 "In order to use this option, "
486                 "the server must be run initially as root.\n"
487                 "Availability: Unix."),
488             (configuration.BooleanOption, "nodaemon", "no",
489                 "don't fork (this overrides the pidfile mechanism)'"),
490             (configuration.BooleanOption, "log_hostnames", "no",
491                 "Log client machine names instead of IP addresses "
492                 "(much slower)"),
493             (configuration.NullableFilePathOption, "pidfile", "",
494                 "File to which the server records "
495                 "the process id of the daemon.\n"
496                 "If this option is not set, "
497                 "the server will run in foreground\n"),
498             (configuration.NullableFilePathOption, "logfile", "",
499                 "Log file path.  If unset, log to stderr."),
500             (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
501                 "Set processing of each request in separate subprocess.\n"
502                 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
503             (configuration.NullableFilePathOption, "template", "",
504                 "Tracker index template. If unset, built-in will be used."),
505             (configuration.BooleanOption, "ssl", "no",
506                 "Enable SSL support (requires pyopenssl)"),
507             (configuration.NullableFilePathOption, "pem", "",
508                 "PEM file used for SSL. A temporary self-signed certificate\n"
509                 "will be used if left blank."),
510         )),
511         ("trackers", (), "Roundup trackers to serve.\n"
512             "Each option in this section defines single Roundup tracker.\n"
513             "Option name identifies the tracker and will appear in the URL.\n"
514             "Option value is tracker home directory path.\n"
515             "The path may be either absolute or relative\n"
516             "to the directory containig this config file."),
517     )
519     # options recognized by config
520     OPTIONS = {
521         "host": "n:",
522         "port": "p:",
523         "group": "g:",
524         "user": "u:",
525         "logfile": "l:",
526         "pidfile": "d:",
527         "nodaemon": "D",
528         "log_hostnames": "N",
529         "multiprocess": "t:",
530         "template": "i:",
531         "ssl": "s",
532         "pem": "e:",
533     }
535     def __init__(self, config_file=None):
536         configuration.Config.__init__(self, config_file, self.SETTINGS)
537         self.sections.append("trackers")
539     def _adjust_options(self, config):
540         """Add options for tracker homes"""
541         # return early if there are no tracker definitions.
542         # trackers must be specified on the command line.
543         if not config.has_section("trackers"):
544             return
545         # config defaults appear in all sections.
546         # filter them out.
547         defaults = config.defaults().keys()
548         for name in config.options("trackers"):
549             if name not in defaults:
550                 self.add_option(TrackerHomeOption(self, "trackers", name))
552     def getopt(self, args, short_options="", long_options=(),
553         config_load_options=("C", "config"), **options
554     ):
555         options.update(self.OPTIONS)
556         return configuration.Config.getopt(self, args,
557             short_options, long_options, config_load_options, **options)
559     def _get_name(self):
560         return "Roundup server"
562     def trackers(self):
563         """Return tracker definitions as a list of (name, home) pairs"""
564         trackers = []
565         for option in self._get_section_options("trackers"):
566             trackers.append((option, os.path.abspath(
567                 self["TRACKERS_" + option.upper()])))
568         return trackers
570     def set_logging(self):
571         """Initialise logging to the configured file, if any."""
572         # appending, unbuffered
573         sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0)
575     def get_server(self):
576         """Return HTTP server object to run"""
577         # we don't want the cgi module interpreting the command-line args ;)
578         sys.argv = sys.argv[:1]
580         # preload all trackers unless we are in "debug" mode
581         tracker_homes = self.trackers()
582         if self["MULTIPROCESS"] == "debug":
583             trackers = None
584         else:
585             trackers = dict([(name, roundup.instance.open(home, optimize=1))
586                 for (name, home) in tracker_homes])
588         # build customized request handler class
589         class RequestHandler(RoundupRequestHandler):
590             LOG_IPADDRESS = not self["LOG_HOSTNAMES"]
591             TRACKER_HOMES = dict(tracker_homes)
592             TRACKERS = trackers
593             DEBUG_MODE = self["MULTIPROCESS"] == "debug"
594             CONFIG = self
596         if self["SSL"]:
597             base_server = SecureHTTPServer
598         else:
599             base_server = BaseHTTPServer.HTTPServer
601         # obtain request server class
602         if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
603             print _("Multiprocess mode \"%s\" is not available, "
604                 "switching to single-process") % self["MULTIPROCESS"]
605             self["MULTIPROCESS"] = "none"
606             server_class = base_server
607         elif self["MULTIPROCESS"] == "fork":
608             class ForkingServer(SocketServer.ForkingMixIn,
609                 base_server):
610                     pass
611             server_class = ForkingServer
612         elif self["MULTIPROCESS"] == "thread":
613             class ThreadingServer(SocketServer.ThreadingMixIn,
614                 base_server):
615                     pass
616             server_class = ThreadingServer
617         else:
618             server_class = base_server
620         # obtain server before changing user id - allows to
621         # use port < 1024 if started as root
622         try:
623             args = ((self["HOST"], self["PORT"]), RequestHandler)
624             kwargs = {}
625             if self["SSL"]:
626                 kwargs['ssl_pem'] = self["PEM"]
627             httpd = server_class(*args, **kwargs)
628         except socket.error, e:
629             if e[0] == errno.EADDRINUSE:
630                 raise socket.error, \
631                     _("Unable to bind to port %s, port already in use.") \
632                     % self["PORT"]
633             raise
634         # change user and/or group
635         setgid(self["GROUP"])
636         setuid(self["USER"])
637         # return the server
638         return httpd
640 try:
641     import win32serviceutil
642 except:
643     RoundupService = None
644 else:
646     # allow the win32
647     import win32service
649     class SvcShutdown(Exception):
650         pass
652     class RoundupService(win32serviceutil.ServiceFramework):
654         _svc_name_ = "roundup"
655         _svc_display_name_ = "Roundup Bug Tracker"
657         running = 0
658         server = None
660         def SvcDoRun(self):
661             import servicemanager
662             self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
663             config = ServerConfig()
664             (optlist, args) = config.getopt(sys.argv[1:])
665             if not config["LOGFILE"]:
666                 servicemanager.LogMsg(servicemanager.EVENTLOG_ERROR_TYPE,
667                     servicemanager.PYS_SERVICE_STOPPED,
668                     (self._svc_display_name_, "\r\nMissing logfile option"))
669                 self.ReportServiceStatus(win32service.SERVICE_STOPPED)
670                 return
671             config.set_logging()
672             self.server = config.get_server()
673             self.running = 1
674             self.ReportServiceStatus(win32service.SERVICE_RUNNING)
675             servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
676                 servicemanager.PYS_SERVICE_STARTED, (self._svc_display_name_,
677                     " at %s:%s" % (config["HOST"], config["PORT"])))
678             while self.running:
679                 self.server.handle_request()
680             servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
681                 servicemanager.PYS_SERVICE_STOPPED,
682                 (self._svc_display_name_, ""))
683             self.ReportServiceStatus(win32service.SERVICE_STOPPED)
685         def SvcStop(self):
686             self.running = 0
687             # make dummy connection to self to terminate blocking accept()
688             addr = self.server.socket.getsockname()
689             if addr[0] == "0.0.0.0":
690                 addr = ("127.0.0.1", addr[1])
691             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
692             sock.connect(addr)
693             sock.close()
694             self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
696 def usage(message=''):
697     if RoundupService:
698         os_part = \
699 ""''' -c <Command>  Windows Service options.
700                If you want to run the server as a Windows Service, you
701                must use configuration file to specify tracker homes.
702                Logfile option is required to run Roundup Tracker service.
703                Typing "roundup-server -c help" shows Windows Services
704                specifics.'''
705     else:
706         os_part = ""''' -u <UID>      runs the Roundup web server as this UID
707  -g <GID>      runs the Roundup web server as this GID
708  -d <PIDfile>  run the server in the background and write the server's PID
709                to the file indicated by PIDfile. The -l option *must* be
710                specified if -d is used.'''
711     if message:
712         message += '\n'
713     print _('''%(message)sUsage: roundup-server [options] [name=tracker home]*
715 Options:
716  -v            print the Roundup version number and exit
717  -h            print this text and exit
718  -S            create or update configuration file and exit
719  -C <fname>    use configuration file <fname>
720  -n <name>     set the host name of the Roundup web server instance
721  -p <port>     set the port to listen on (default: %(port)s)
722  -l <fname>    log to the file indicated by fname instead of stderr/stdout
723  -N            log client machine names instead of IP addresses (much slower)
724  -i <fname>    set tracker index template
725  -s            enable SSL
726  -e <fname>    PEM file containing SSL key and certificate
727  -t <mode>     multiprocess mode (default: %(mp_def)s).
728                Allowed values: %(mp_types)s.
729 %(os_part)s
731 Long options:
732  --version          print the Roundup version number and exit
733  --help             print this text and exit
734  --save-config      create or update configuration file and exit
735  --config <fname>   use configuration file <fname>
736  All settings of the [main] section of the configuration file
737  also may be specified in form --<name>=<value>
739 Examples:
741  roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\
742     -n localhost -p 8917 -l /var/log/roundup.log \\
743     support=/var/spool/roundup-trackers/support
745  roundup-server -C /opt/roundup/etc/roundup-server.ini
747  roundup-server support=/var/spool/roundup-trackers/support
749  roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\
750     support=/var/spool/roundup-trackers/support
752 Configuration file format:
753    Roundup Server configuration file has common .ini file format.
754    Configuration file created with 'roundup-server -S' contains
755    detailed explanations for each option.  Please see that file
756    for option descriptions.
758 How to use "name=tracker home":
759    These arguments set the tracker home(s) to use. The name is how the
760    tracker is identified in the URL (it's the first part of the URL path).
761    The tracker home is the directory that was identified when you did
762    "roundup-admin init". You may specify any number of these name=home
763    pairs on the command-line. Make sure the name part doesn't include
764    any url-unsafe characters like spaces, as these confuse IE.
765 ''') % {
766     "message": message,
767     "os_part": os_part,
768     "port": DEFAULT_PORT,
769     "mp_def": DEFAULT_MULTIPROCESS,
770     "mp_types": ", ".join(MULTIPROCESS_TYPES),
774 def writepidfile(pidfile):
775     ''' Write a pidfile (only). Do not daemonize. '''
776     pid = os.getpid()
777     if pid:
778         pidfile = open(pidfile, 'w')
779         pidfile.write(str(pid))
780         pidfile.close()
782 def daemonize(pidfile):
783     ''' Turn this process into a daemon.
784         - make sure the sys.std(in|out|err) are completely cut off
785         - make our parent PID 1
787         Write our new PID to the pidfile.
789         From A.M. Kuuchling (possibly originally Greg Ward) with
790         modification from Oren Tirosh, and finally a small mod from me.
791     '''
792     # Fork once
793     if os.fork() != 0:
794         os._exit(0)
796     # Create new session
797     os.setsid()
799     # Second fork to force PPID=1
800     pid = os.fork()
801     if pid:
802         pidfile = open(pidfile, 'w')
803         pidfile.write(str(pid))
804         pidfile.close()
805         os._exit(0)
807     os.chdir("/")
809     # close off std(in|out|err), redirect to devnull so the file
810     # descriptors can't be used again
811     devnull = os.open('/dev/null', 0)
812     os.dup2(devnull, 0)
813     os.dup2(devnull, 1)
814     os.dup2(devnull, 2)
816 undefined = []
817 def run(port=undefined, success_message=None):
818     ''' Script entry point - handle args and figure out what to to.
819     '''
820     # time out after a minute if we can
821     if hasattr(socket, 'setdefaulttimeout'):
822         socket.setdefaulttimeout(60)
824     config = ServerConfig()
825     # additional options
826     short_options = "hvS"
827     if RoundupService:
828         short_options += 'c'
829     try:
830         (optlist, args) = config.getopt(sys.argv[1:],
831             short_options, ("help", "version", "save-config",))
832     except (getopt.GetoptError, configuration.ConfigurationError), e:
833         usage(str(e))
834         return
836     # if running in windows service mode, don't do any other stuff
837     if ("-c", "") in optlist:
838         # acquire command line options recognized by service
839         short_options = "cC:"
840         long_options = ["config"]
841         for (long_name, short_name) in config.OPTIONS.items():
842             short_options += short_name
843             long_name = long_name.lower().replace("_", "-")
844             if short_name[-1] == ":":
845                 long_name += "="
846             long_options.append(long_name)
847         optlist = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
848         svc_args = []
849         for (opt, arg) in optlist:
850             if opt in ("-C", "-l"):
851                 # make sure file name is absolute
852                 svc_args.extend((opt, os.path.abspath(arg)))
853             elif opt in ("--config", "--logfile"):
854                 # ditto, for long options
855                 svc_args.append("=".join(opt, os.path.abspath(arg)))
856             elif opt != "-c":
857                 svc_args.extend(opt)
858         RoundupService._exe_args_ = " ".join(svc_args)
859         # pass the control to serviceutil
860         win32serviceutil.HandleCommandLine(RoundupService,
861             argv=sys.argv[:1] + args)
862         return
864     # add tracker names from command line.
865     # this is done early to let '--save-config' handle the trackers.
866     if args:
867         for arg in args:
868             try:
869                 name, home = arg.split('=')
870             except ValueError:
871                 raise ValueError, _("Instances must be name=home")
872             config.add_option(TrackerHomeOption(config, "trackers", name))
873             config["TRACKERS_" + name.upper()] = home
875     # handle remaining options
876     if optlist:
877         for (opt, arg) in optlist:
878             if opt in ("-h", "--help"):
879                 usage()
880             elif opt in ("-v", "--version"):
881                 print '%s (python %s)' % (roundup_version,
882                     sys.version.split()[0])
883             elif opt in ("-S", "--save-config"):
884                 config.save()
885                 print _("Configuration saved to %s") % config.filepath
886         # any of the above options prevent server from running
887         return
889     # port number in function arguments overrides config and command line
890     if port is not undefined:
891         config.PORT = port
893     if config["LOGFILE"]:
894         config["LOGFILE"] = os.path.abspath(config["LOGFILE"])
895         # switch logging from stderr/stdout to logfile
896         config.set_logging()
897     if config["PIDFILE"]:
898         config["PIDFILE"] = os.path.abspath(config["PIDFILE"])
900     # fork the server from our parent if a pidfile is specified
901     if config["PIDFILE"]:
902         if not hasattr(os, 'fork'):
903             print _("Sorry, you can't run the server as a daemon"
904                 " on this Operating System")
905             sys.exit(0)
906         else:
907             if config['NODAEMON']:
908                 writepidfile(config["PIDFILE"])
909             else:
910                 daemonize(config["PIDFILE"])
912     # create the server
913     httpd = config.get_server()
915     if success_message:
916         print success_message
917     else:
918         print _('Roundup server started on %(HOST)s:%(PORT)s') \
919             % config
921     try:
922         httpd.serve_forever()
923     except KeyboardInterrupt:
924         print 'Keyboard Interrupt: exiting'
926 if __name__ == '__main__':
927     run()
929 # vim: sts=4 sw=4 et si