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