Premiere version : mise en route du suivi.
[auf_roundup.git] / roundup / scripts / .svn / text-base / roundup_server.py.svn-base
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 #
17
18 """Command-line script that runs a server over roundup.cgi.client.
19
20 $Id: roundup_server.py,v 1.94 2007-09-25 04:27:12 jpend Exp $
21 """
22 __docformat__ = 'restructuredtext'
23
24 import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
25 import ConfigParser, BaseHTTPServer, SocketServer, StringIO
26
27 try:
28     from OpenSSL import SSL
29 except ImportError:
30     SSL = None
31
32 # python version check
33 from roundup import configuration, version_check
34 from roundup import __version__ as roundup_version
35
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 _
41
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()))
57
58 DEFAULT_PORT = 8080
59
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]
74
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)
93
94     return ctx
95
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()
111
112     def get_request(self):
113         (conn, info) = self.socket.accept()
114         if self.ssl_context:
115
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
122
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)
130
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)
138
139                 def __getattr__(self, attrib):
140                     return getattr(self.__fileobj, attrib)
141
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)
150
151                 def __getattr__(self, attrib):
152                     return getattr(self.__conn, attrib)
153
154             conn = ConnFixer(conn)
155         return (conn, info)
156
157 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
158     TRACKER_HOMES = {}
159     TRACKERS = None
160     LOG_IPADDRESS = 1
161     DEBUG_MODE = False
162     CONFIG = None
163
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
186
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()
227
228     do_GET = do_POST = do_HEAD = run_cgi
229
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)
240
241         self.send_header('Content-Type', 'text/html')
242         self.end_headers()
243         w = self.wfile.write
244
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>')
264
265     def inner_run_cgi(self):
266         ''' This is the inner part of the CGI handling
267         '''
268         rest = self.path
269
270         # file-like object for the favicon.ico file information
271         favicon_fileobj = None
272
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'])
278
279                 if os.access(favicon_filepath, os.R_OK):
280                     favicon_fileobj = open(favicon_filepath, 'rb')
281
282
283             if favicon_fileobj is None:
284                 favicon_fileobj = StringIO.StringIO(favico)
285
286             self.send_response(200)
287             self.send_header('Content-Type', 'image/x-icon')
288             self.end_headers()
289
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
300
301             favicon_fileobj.close()
302
303             return
304
305         i = rest.rfind('?')
306         if i >= 0:
307             rest, query = rest[:i], rest[i+1:]
308         else:
309             query = ''
310
311         # no tracker - spit out the index
312         if rest == '/':
313             self.index()
314             return
315
316         # figure the tracker
317         l_path = rest.split('/')
318         tracker_name = urllib.unquote(l_path[1]).lower()
319
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
330
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 = '/'
336
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
366         # do the roundup thing
367         tracker = self.get_tracker(tracker_name)
368         tracker.Client(tracker, self, env).main()
369
370     def address_string(self):
371         if self.LOG_IPADDRESS:
372             return self.client_address[0]
373         else:
374             host, port = self.client_address
375             return socket.getfqdn(host)
376
377     def log_message(self, format, *args):
378         ''' Try to *safely* log to stderr.
379         '''
380         try:
381             BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,
382                 format, *args)
383         except IOError:
384             # stderr is no longer viable
385             pass
386
387     def start_response(self, headers, response):
388         self.send_response(response)
389         for key, value in headers:
390             self.send_header(key, value)
391         self.end_headers()
392
393 def error():
394     exc_type, exc_value = sys.exc_info()[:2]
395     return _('Error: %s: %s' % (exc_type, exc_value))
396
397 def setgid(group):
398     if group is None:
399         return
400     if not hasattr(os, 'setgid'):
401         return
402
403     # if root, setgid to the running user
404     if os.getuid():
405         print _('WARNING: ignoring "-g" argument, not root')
406         return
407
408     try:
409         import grp
410     except ImportError:
411         raise ValueError, _("Can't change groups - no grp module")
412     try:
413         try:
414             gid = int(group)
415         except ValueError:
416             gid = grp.getgrnam(group)[2]
417         else:
418             grp.getgrgid(gid)
419     except KeyError:
420         raise ValueError,_("Group %(group)s doesn't exist")%locals()
421     os.setgid(gid)
422
423 def setuid(user):
424     if not hasattr(os, 'getuid'):
425         return
426
427     # People can remove this check if they're really determined
428     if user is None:
429         if os.getuid():
430             return
431         raise ValueError, _("Can't run as root!")
432
433     if os.getuid():
434         print _('WARNING: ignoring "-u" argument, not root')
435         return
436
437     try:
438         import pwd
439     except ImportError:
440         raise ValueError, _("Can't change users - no pwd module")
441     try:
442         try:
443             uid = int(user)
444         except ValueError:
445             uid = pwd.getpwnam(user)[2]
446         else:
447             pwd.getpwuid(uid)
448     except KeyError:
449         raise ValueError, _("User %(user)s doesn't exist")%locals()
450     os.setuid(uid)
451
452 class TrackerHomeOption(configuration.FilePathOption):
453
454     # Tracker homes do not need any description strings
455     def format(self):
456         return "%(name)s = %(value)s\n" % {
457                 "name": self.setting,
458                 "value": self.value2str(self._value),
459             }
460
461 class ServerConfig(configuration.Config):
462
463     SETTINGS = (
464             ("main", (
465             (configuration.Option, "host", "",
466                 "Host name of the Roundup web server instance.\n"
467                 "If empty, listen on all network interfaces."),
468             (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
469                 "Port to listen on."),
470             (configuration.NullableFilePathOption, "favicon", "favicon.ico",
471                 "Path to favicon.ico image file."
472                 "  If unset, built-in favicon.ico is used."),
473             (configuration.NullableOption, "user", "",
474                 "User ID as which the server will answer requests.\n"
475                 "In order to use this option, "
476                 "the server must be run initially as root.\n"
477                 "Availability: Unix."),
478             (configuration.NullableOption, "group", "",
479                 "Group 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.BooleanOption, "nodaemon", "no",
484                 "don't fork (this overrides the pidfile mechanism)'"),
485             (configuration.BooleanOption, "log_hostnames", "no",
486                 "Log client machine names instead of IP addresses "
487                 "(much slower)"),
488             (configuration.NullableFilePathOption, "pidfile", "",
489                 "File to which the server records "
490                 "the process id of the daemon.\n"
491                 "If this option is not set, "
492                 "the server will run in foreground\n"),
493             (configuration.NullableFilePathOption, "logfile", "",
494                 "Log file path.  If unset, log to stderr."),
495             (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
496                 "Set processing of each request in separate subprocess.\n"
497                 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
498             (configuration.NullableFilePathOption, "template", "",
499                 "Tracker index template. If unset, built-in will be used."),
500             (configuration.BooleanOption, "ssl", "no",
501                 "Enable SSL support (requires pyopenssl)"),
502             (configuration.NullableFilePathOption, "pem", "",
503                 "PEM file used for SSL. A temporary self-signed certificate\n"
504                 "will be used if left blank."),
505         )),
506         ("trackers", (), "Roundup trackers to serve.\n"
507             "Each option in this section defines single Roundup tracker.\n"
508             "Option name identifies the tracker and will appear in the URL.\n"
509             "Option value is tracker home directory path.\n"
510             "The path may be either absolute or relative\n"
511             "to the directory containig this config file."),
512     )
513
514     # options recognized by config
515     OPTIONS = {
516         "host": "n:",
517         "port": "p:",
518         "group": "g:",
519         "user": "u:",
520         "logfile": "l:",
521         "pidfile": "d:",
522         "nodaemon": "D",
523         "log_hostnames": "N",
524         "multiprocess": "t:",
525         "template": "i:",
526         "ssl": "s",
527         "pem": "e:",
528     }
529
530     def __init__(self, config_file=None):
531         configuration.Config.__init__(self, config_file, self.SETTINGS)
532         self.sections.append("trackers")
533
534     def _adjust_options(self, config):
535         """Add options for tracker homes"""
536         # return early if there are no tracker definitions.
537         # trackers must be specified on the command line.
538         if not config.has_section("trackers"):
539             return
540         # config defaults appear in all sections.
541         # filter them out.
542         defaults = config.defaults().keys()
543         for name in config.options("trackers"):
544             if name not in defaults:
545                 self.add_option(TrackerHomeOption(self, "trackers", name))
546
547     def getopt(self, args, short_options="", long_options=(),
548         config_load_options=("C", "config"), **options
549     ):
550         options.update(self.OPTIONS)
551         return configuration.Config.getopt(self, args,
552             short_options, long_options, config_load_options, **options)
553
554     def _get_name(self):
555         return "Roundup server"
556
557     def trackers(self):
558         """Return tracker definitions as a list of (name, home) pairs"""
559         trackers = []
560         for option in self._get_section_options("trackers"):
561             trackers.append((option, os.path.abspath(
562                 self["TRACKERS_" + option.upper()])))
563         return trackers
564
565     def set_logging(self):
566         """Initialise logging to the configured file, if any."""
567         # appending, unbuffered
568         sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0)
569
570     def get_server(self):
571         """Return HTTP server object to run"""
572         # we don't want the cgi module interpreting the command-line args ;)
573         sys.argv = sys.argv[:1]
574
575         # preload all trackers unless we are in "debug" mode
576         tracker_homes = self.trackers()
577         if self["MULTIPROCESS"] == "debug":
578             trackers = None
579         else:
580             trackers = dict([(name, roundup.instance.open(home, optimize=1))
581                 for (name, home) in tracker_homes])
582
583         # build customized request handler class
584         class RequestHandler(RoundupRequestHandler):
585             LOG_IPADDRESS = not self["LOG_HOSTNAMES"]
586             TRACKER_HOMES = dict(tracker_homes)
587             TRACKERS = trackers
588             DEBUG_MODE = self["MULTIPROCESS"] == "debug"
589             CONFIG = self
590
591             def setup(self):
592                 if self.CONFIG["SSL"]:
593                     # perform initial ssl handshake. This will set
594                     # internal state correctly so that later closing SSL
595                     # socket works (with SSL end-handshake started)
596                     self.request.do_handshake()
597                 RoundupRequestHandler.setup(self)
598
599             def finish(self):
600                 RoundupRequestHandler.finish(self)
601                 if self.CONFIG["SSL"]:
602                     self.request.shutdown()
603                     self.request.close()
604
605         if self["SSL"]:
606             base_server = SecureHTTPServer
607         else:
608             # time out after a minute if we can
609             # This sets the socket to non-blocking. SSL needs a blocking
610             # socket, so we do this only for non-SSL connections.
611             if hasattr(socket, 'setdefaulttimeout'):
612                 socket.setdefaulttimeout(60)
613             base_server = BaseHTTPServer.HTTPServer
614
615         # obtain request server class
616         if self["MULTIPROCESS"] not in MULTIPROCESS_TYPES:
617             print _("Multiprocess mode \"%s\" is not available, "
618                 "switching to single-process") % self["MULTIPROCESS"]
619             self["MULTIPROCESS"] = "none"
620             server_class = base_server
621         elif self["MULTIPROCESS"] == "fork":
622             class ForkingServer(SocketServer.ForkingMixIn,
623                 base_server):
624                     pass
625             server_class = ForkingServer
626         elif self["MULTIPROCESS"] == "thread":
627             class ThreadingServer(SocketServer.ThreadingMixIn,
628                 base_server):
629                     pass
630             server_class = ThreadingServer
631         else:
632             server_class = base_server
633
634         # obtain server before changing user id - allows to
635         # use port < 1024 if started as root
636         try:
637             args = ((self["HOST"], self["PORT"]), RequestHandler)
638             kwargs = {}
639             if self["SSL"]:
640                 kwargs['ssl_pem'] = self["PEM"]
641             httpd = server_class(*args, **kwargs)
642         except socket.error, e:
643             if e[0] == errno.EADDRINUSE:
644                 raise socket.error, \
645                     _("Unable to bind to port %s, port already in use.") \
646                     % self["PORT"]
647             raise
648         # change user and/or group
649         setgid(self["GROUP"])
650         setuid(self["USER"])
651         # return the server
652         return httpd
653
654 try:
655     import win32serviceutil
656 except:
657     RoundupService = None
658 else:
659
660     # allow the win32
661     import win32service
662
663     class SvcShutdown(Exception):
664         pass
665
666     class RoundupService(win32serviceutil.ServiceFramework):
667
668         _svc_name_ = "roundup"
669         _svc_display_name_ = "Roundup Bug Tracker"
670
671         running = 0
672         server = None
673
674         def SvcDoRun(self):
675             import servicemanager
676             self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
677             config = ServerConfig()
678             (optlist, args) = config.getopt(sys.argv[1:])
679             if not config["LOGFILE"]:
680                 servicemanager.LogMsg(servicemanager.EVENTLOG_ERROR_TYPE,
681                     servicemanager.PYS_SERVICE_STOPPED,
682                     (self._svc_display_name_, "\r\nMissing logfile option"))
683                 self.ReportServiceStatus(win32service.SERVICE_STOPPED)
684                 return
685             config.set_logging()
686             self.server = config.get_server()
687             self.running = 1
688             self.ReportServiceStatus(win32service.SERVICE_RUNNING)
689             servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
690                 servicemanager.PYS_SERVICE_STARTED, (self._svc_display_name_,
691                     " at %s:%s" % (config["HOST"], config["PORT"])))
692             while self.running:
693                 self.server.handle_request()
694             servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
695                 servicemanager.PYS_SERVICE_STOPPED,
696                 (self._svc_display_name_, ""))
697             self.ReportServiceStatus(win32service.SERVICE_STOPPED)
698
699         def SvcStop(self):
700             self.running = 0
701             # make dummy connection to self to terminate blocking accept()
702             addr = self.server.socket.getsockname()
703             if addr[0] == "0.0.0.0":
704                 addr = ("127.0.0.1", addr[1])
705             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
706             sock.connect(addr)
707             sock.close()
708             self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
709
710 def usage(message=''):
711     if RoundupService:
712         os_part = \
713 ""''' -c <Command>  Windows Service options.
714                If you want to run the server as a Windows Service, you
715                must use configuration file to specify tracker homes.
716                Logfile option is required to run Roundup Tracker service.
717                Typing "roundup-server -c help" shows Windows Services
718                specifics.'''
719     else:
720         os_part = ""''' -u <UID>      runs the Roundup web server as this UID
721  -g <GID>      runs the Roundup web server as this GID
722  -d <PIDfile>  run the server in the background and write the server's PID
723                to the file indicated by PIDfile. The -l option *must* be
724                specified if -d is used.'''
725     if message:
726         message += '\n'
727     print _('''%(message)sUsage: roundup-server [options] [name=tracker home]*
728
729 Options:
730  -v            print the Roundup version number and exit
731  -h            print this text and exit
732  -S            create or update configuration file and exit
733  -C <fname>    use configuration file <fname>
734  -n <name>     set the host name of the Roundup web server instance
735  -p <port>     set the port to listen on (default: %(port)s)
736  -l <fname>    log to the file indicated by fname instead of stderr/stdout
737  -N            log client machine names instead of IP addresses (much slower)
738  -i <fname>    set tracker index template
739  -s            enable SSL
740  -e <fname>    PEM file containing SSL key and certificate
741  -t <mode>     multiprocess mode (default: %(mp_def)s).
742                Allowed values: %(mp_types)s.
743 %(os_part)s
744
745 Long options:
746  --version          print the Roundup version number and exit
747  --help             print this text and exit
748  --save-config      create or update configuration file and exit
749  --config <fname>   use configuration file <fname>
750  All settings of the [main] section of the configuration file
751  also may be specified in form --<name>=<value>
752
753 Examples:
754
755  roundup-server -S -C /opt/roundup/etc/roundup-server.ini \\
756     -n localhost -p 8917 -l /var/log/roundup.log \\
757     support=/var/spool/roundup-trackers/support
758
759  roundup-server -C /opt/roundup/etc/roundup-server.ini
760
761  roundup-server support=/var/spool/roundup-trackers/support
762
763  roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\
764     support=/var/spool/roundup-trackers/support
765
766 Configuration file format:
767    Roundup Server configuration file has common .ini file format.
768    Configuration file created with 'roundup-server -S' contains
769    detailed explanations for each option.  Please see that file
770    for option descriptions.
771
772 How to use "name=tracker home":
773    These arguments set the tracker home(s) to use. The name is how the
774    tracker is identified in the URL (it's the first part of the URL path).
775    The tracker home is the directory that was identified when you did
776    "roundup-admin init". You may specify any number of these name=home
777    pairs on the command-line. Make sure the name part doesn't include
778    any url-unsafe characters like spaces, as these confuse IE.
779 ''') % {
780     "message": message,
781     "os_part": os_part,
782     "port": DEFAULT_PORT,
783     "mp_def": DEFAULT_MULTIPROCESS,
784     "mp_types": ", ".join(MULTIPROCESS_TYPES),
785 }
786
787
788 def writepidfile(pidfile):
789     ''' Write a pidfile (only). Do not daemonize. '''
790     pid = os.getpid()
791     if pid:
792         pidfile = open(pidfile, 'w')
793         pidfile.write(str(pid))
794         pidfile.close()
795
796 def daemonize(pidfile):
797     ''' Turn this process into a daemon.
798         - make sure the sys.std(in|out|err) are completely cut off
799         - make our parent PID 1
800
801         Write our new PID to the pidfile.
802
803         From A.M. Kuuchling (possibly originally Greg Ward) with
804         modification from Oren Tirosh, and finally a small mod from me.
805     '''
806     # Fork once
807     if os.fork() != 0:
808         os._exit(0)
809
810     # Create new session
811     os.setsid()
812
813     # Second fork to force PPID=1
814     pid = os.fork()
815     if pid:
816         pidfile = open(pidfile, 'w')
817         pidfile.write(str(pid))
818         pidfile.close()
819         os._exit(0)
820
821     os.chdir("/")
822
823     # close off std(in|out|err), redirect to devnull so the file
824     # descriptors can't be used again
825     devnull = os.open('/dev/null', 0)
826     os.dup2(devnull, 0)
827     os.dup2(devnull, 1)
828     os.dup2(devnull, 2)
829
830 undefined = []
831 def run(port=undefined, success_message=None):
832     ''' Script entry point - handle args and figure out what to to.
833     '''
834     config = ServerConfig()
835     # additional options
836     short_options = "hvS"
837     if RoundupService:
838         short_options += 'c'
839     try:
840         (optlist, args) = config.getopt(sys.argv[1:],
841             short_options, ("help", "version", "save-config",))
842     except (getopt.GetoptError, configuration.ConfigurationError), e:
843         usage(str(e))
844         return
845
846     # if running in windows service mode, don't do any other stuff
847     if ("-c", "") in optlist:
848         # acquire command line options recognized by service
849         short_options = "cC:"
850         long_options = ["config"]
851         for (long_name, short_name) in config.OPTIONS.items():
852             short_options += short_name
853             long_name = long_name.lower().replace("_", "-")
854             if short_name[-1] == ":":
855                 long_name += "="
856             long_options.append(long_name)
857         optlist = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
858         svc_args = []
859         for (opt, arg) in optlist:
860             if opt in ("-C", "-l"):
861                 # make sure file name is absolute
862                 svc_args.extend((opt, os.path.abspath(arg)))
863             elif opt in ("--config", "--logfile"):
864                 # ditto, for long options
865                 svc_args.append("=".join(opt, os.path.abspath(arg)))
866             elif opt != "-c":
867                 svc_args.extend(opt)
868         RoundupService._exe_args_ = " ".join(svc_args)
869         # pass the control to serviceutil
870         win32serviceutil.HandleCommandLine(RoundupService,
871             argv=sys.argv[:1] + args)
872         return
873
874     # add tracker names from command line.
875     # this is done early to let '--save-config' handle the trackers.
876     if args:
877         for arg in args:
878             try:
879                 name, home = arg.split('=')
880             except ValueError:
881                 raise ValueError, _("Instances must be name=home")
882             config.add_option(TrackerHomeOption(config, "trackers", name))
883             config["TRACKERS_" + name.upper()] = home
884
885     # handle remaining options
886     if optlist:
887         for (opt, arg) in optlist:
888             if opt in ("-h", "--help"):
889                 usage()
890             elif opt in ("-v", "--version"):
891                 print '%s (python %s)' % (roundup_version,
892                     sys.version.split()[0])
893             elif opt in ("-S", "--save-config"):
894                 config.save()
895                 print _("Configuration saved to %s") % config.filepath
896         # any of the above options prevent server from running
897         return
898
899     # port number in function arguments overrides config and command line
900     if port is not undefined:
901         config.PORT = port
902
903     if config["LOGFILE"]:
904         config["LOGFILE"] = os.path.abspath(config["LOGFILE"])
905         # switch logging from stderr/stdout to logfile
906         config.set_logging()
907     if config["PIDFILE"]:
908         config["PIDFILE"] = os.path.abspath(config["PIDFILE"])
909
910     # fork the server from our parent if a pidfile is specified
911     if config["PIDFILE"]:
912         if not hasattr(os, 'fork'):
913             print _("Sorry, you can't run the server as a daemon"
914                 " on this Operating System")
915             sys.exit(0)
916         else:
917             if config['NODAEMON']:
918                 writepidfile(config["PIDFILE"])
919             else:
920                 daemonize(config["PIDFILE"])
921
922     # create the server
923     httpd = config.get_server()
924
925     if success_message:
926         print success_message
927     else:
928         print _('Roundup server started on %(HOST)s:%(PORT)s') \
929             % config
930
931     try:
932         httpd.serve_forever()
933     except KeyboardInterrupt:
934         print 'Keyboard Interrupt: exiting'
935
936 if __name__ == '__main__':
937     run()
938
939 # vim: sts=4 sw=4 et si