Correction crash
[auf_roundup.git] / build / lib / roundup / scripts / roundup_server.py
CommitLineData
c638d827
CR
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
24import errno, cgi, getopt, os, socket, sys, traceback, urllib, time
25import ConfigParser, BaseHTTPServer, SocketServer, StringIO
26
27try:
28 from OpenSSL import SSL
29except ImportError:
30 SSL = None
31
32# python version check
33from roundup import configuration, version_check
34from roundup import __version__ as roundup_version
35
36# Roundup modules of use here
37from roundup.cgi import cgitb, client
38from roundup.cgi.PageTemplates.PageTemplate import PageTemplate
39import roundup.instance
40from roundup.i18n import _
41
42# "default" favicon.ico
43# generate by using "icotool" and tools/base64
44import zlib, base64
45favico = zlib.decompress(base64.decodestring('''
46eJztjr1PmlEUh59XgVoshdYPWorFIhaRFq0t9pNq37b60lYSTRzcTFw6GAfj5gDYaF0dTB0MxMSE
47gQQd3FzKJiEC0UCIUUN1M41pV2JCXySg/0ITn5tfzvmdc+85FwT56HSc81UJjXJsk1UsNcsSqCk1
48BS64lK+vr7OyssLJyQl2ux2j0cjU1BQajYZIJEIwGMRms+H3+zEYDExOTjI2Nsbm5iZWqxWv18vW
491hZDQ0Ok02kmJiY4Ojpienqa3d1dxsfHUSqVeDwe5ufnyeVyrK6u4nK5ODs7Y3FxEYfDwdzcHCaT
50icPDQ5LJJIIgMDIyQj6fZ39/n+3tbdbW1pAkiYWFBWZmZtjb2yMejzM8PEwgEMDn85HNZonFYqjV
51asLhMMvLy2QyGfR6PaOjowwODmKxWDg+PkalUhEKhSgUCiwtLWE2m9nZ2UGhULCxscHp6SmpVIpo
52NMrs7CwHBwdotVoSiQRXXPG/IzY7RHtt922xjFRb01H1XhKfPBNbi/7my7rrLXJ88eppvxwEfV3f
53NY3Y6exofVdsV3+2wnPFDdPjB83n7xuVpcFvygPbGwxF31LZIKrQDfR2Xvh7lmrX654L/7bvlnng
54bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g
5523eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI=
56'''.strip()))
57
58DEFAULT_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
64MULTIPROCESS_TYPES = ["debug", "none"]
65try:
66 import thread
67except ImportError:
68 pass
69else:
70 MULTIPROCESS_TYPES.append("thread")
71if hasattr(os, 'fork'):
72 MULTIPROCESS_TYPES.append("fork")
73DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
74
75def 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
96class 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
157class 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
393def error():
394 exc_type, exc_value = sys.exc_info()[:2]
395 return _('Error: %s: %s' % (exc_type, exc_value))
396
397def 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
423def 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
452class 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
461class 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
654try:
655 import win32serviceutil
656except:
657 RoundupService = None
658else:
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
710def 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
729Options:
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
745Long 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
753Examples:
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
766Configuration 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
772How 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
788def 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
796def 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
830undefined = []
831def 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
936if __name__ == '__main__':
937 run()
938
939# vim: sts=4 sw=4 et si