Code

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