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