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