Code

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