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, 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)
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 while True:
126 try:
127 return self.__fileobj.readline(*args)
128 except SSL.WantReadError:
129 time.sleep(.1)
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)
139 def __getattr__(self, attrib):
140 return getattr(self.__fileobj, attrib)
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)
151 def __getattr__(self, attrib):
152 return getattr(self.__conn, attrib)
154 conn = ConnFixer(conn)
155 return (conn, info)
157 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
158 TRACKER_HOMES = {}
159 TRACKERS = None
160 LOG_IPADDRESS = 1
161 DEBUG_MODE = False
162 CONFIG = None
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
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 save_stdin = sys.stdin
192 sys.stdin = self.rfile
193 try:
194 self.inner_run_cgi()
195 except client.NotFound:
196 self.send_error(404, self.path)
197 except client.Unauthorised, message:
198 self.send_error(403, '%s (%s)'%(self.path, message))
199 except:
200 exc, val, tb = sys.exc_info()
201 if hasattr(socket, 'timeout') and isinstance(val, socket.timeout):
202 self.log_error('timeout')
203 else:
204 # it'd be nice to be able to detect if these are going to have
205 # any effect...
206 self.send_response(400)
207 self.send_header('Content-Type', 'text/html')
208 self.end_headers()
209 if self.DEBUG_MODE:
210 try:
211 reload(cgitb)
212 self.wfile.write(cgitb.breaker())
213 self.wfile.write(cgitb.html())
214 except:
215 s = StringIO.StringIO()
216 traceback.print_exc(None, s)
217 self.wfile.write("<pre>")
218 self.wfile.write(cgi.escape(s.getvalue()))
219 self.wfile.write("</pre>\n")
220 else:
221 # user feedback
222 self.wfile.write(cgitb.breaker())
223 ts = time.ctime()
224 self.wfile.write('''<p>%s: An error occurred. Please check
225 the server log for more infomation.</p>'''%ts)
226 # out to the logfile
227 print 'EXCEPTION AT', ts
228 traceback.print_exc()
229 sys.stdin = save_stdin
231 do_GET = do_POST = do_HEAD = run_cgi
233 def index(self):
234 ''' Print up an index of the available trackers
235 '''
236 keys = self.TRACKER_HOMES.keys()
237 if len(keys) == 1:
238 self.send_response(302)
239 self.send_header('Location', urllib.quote(keys[0]) + '/index')
240 self.end_headers()
241 else:
242 self.send_response(200)
244 self.send_header('Content-Type', 'text/html')
245 self.end_headers()
246 w = self.wfile.write
248 if self.CONFIG and self.CONFIG['TEMPLATE']:
249 template = open(self.CONFIG['TEMPLATE']).read()
250 pt = PageTemplate()
251 pt.write(template)
252 extra = { 'trackers': self.TRACKERS,
253 'nothing' : None,
254 'true' : 1,
255 'false' : 0,
256 }
257 w(pt.pt_render(extra_context=extra))
258 else:
259 w(_('<html><head><title>Roundup trackers index</title></head>\n'
260 '<body><h1>Roundup trackers index</h1><ol>\n'))
261 keys.sort()
262 for tracker in keys:
263 w('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n'%{
264 'tracker_url': urllib.quote(tracker),
265 'tracker_name': cgi.escape(tracker)})
266 w('</ol></body></html>')
268 def inner_run_cgi(self):
269 ''' This is the inner part of the CGI handling
270 '''
271 rest = self.path
273 # file-like object for the favicon.ico file information
274 favicon_fileobj = None
276 if rest == '/favicon.ico':
277 # check to see if a custom favicon was specified, and set
278 # favicon_fileobj to the input file
279 if self.CONFIG is not None:
280 favicon_filepath = os.path.abspath(self.CONFIG['FAVICON'])
282 if os.access(favicon_filepath, os.R_OK):
283 favicon_fileobj = open(favicon_filepath, 'rb')
286 if favicon_fileobj is None:
287 favicon_fileobj = StringIO.StringIO(favico)
289 self.send_response(200)
290 self.send_header('Content-Type', 'image/x-icon')
291 self.end_headers()
293 # this bufsize is completely arbitrary, I picked 4K because it sounded good.
294 # if someone knows of a better buffer size, feel free to plug it in.
295 bufsize = 4 * 1024
296 Processing = True
297 while Processing:
298 data = favicon_fileobj.read(bufsize)
299 if len(data) > 0:
300 self.wfile.write(data)
301 else:
302 Processing = False
304 favicon_fileobj.close()
306 return
308 i = rest.rfind('?')
309 if i >= 0:
310 rest, query = rest[:i], rest[i+1:]
311 else:
312 query = ''
314 # no tracker - spit out the index
315 if rest == '/':
316 self.index()
317 return
319 # figure the tracker
320 l_path = rest.split('/')
321 tracker_name = urllib.unquote(l_path[1]).lower()
323 # handle missing trailing '/'
324 if len(l_path) == 2:
325 self.send_response(301)
326 # redirect - XXX https??
327 protocol = 'http'
328 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
329 self.send_header('Location', url)
330 self.end_headers()
331 self.wfile.write('Moved Permanently')
332 return
334 # figure out what the rest of the path is
335 if len(l_path) > 2:
336 rest = '/'.join(l_path[2:])
337 else:
338 rest = '/'
340 # Set up the CGI environment
341 env = {}
342 env['TRACKER_NAME'] = tracker_name
343 env['REQUEST_METHOD'] = self.command
344 env['PATH_INFO'] = urllib.unquote(rest)
345 if query:
346 env['QUERY_STRING'] = query
347 if self.headers.typeheader is None:
348 env['CONTENT_TYPE'] = self.headers.type
349 else:
350 env['CONTENT_TYPE'] = self.headers.typeheader
351 length = self.headers.getheader('content-length')
352 if length:
353 env['CONTENT_LENGTH'] = length
354 co = filter(None, self.headers.getheaders('cookie'))
355 if co:
356 env['HTTP_COOKIE'] = ', '.join(co)
357 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
358 env['SCRIPT_NAME'] = ''
359 env['SERVER_NAME'] = self.server.server_name
360 env['SERVER_PORT'] = str(self.server.server_port)
361 env['HTTP_HOST'] = self.headers['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')
366 # do the roundup thing
367 tracker = self.get_tracker(tracker_name)
368 tracker.Client(tracker, self, env).main()
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)
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
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()
393 def error():
394 exc_type, exc_value = sys.exc_info()[:2]
395 return _('Error: %s: %s' % (exc_type, exc_value))
397 def setgid(group):
398 if group is None:
399 return
400 if not hasattr(os, 'setgid'):
401 return
403 # if root, setgid to the running user
404 if os.getuid():
405 print _('WARNING: ignoring "-g" argument, not root')
406 return
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)
423 def setuid(user):
424 if not hasattr(os, 'getuid'):
425 return
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!")
433 if os.getuid():
434 print _('WARNING: ignoring "-u" argument, not root')
435 return
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)
452 class TrackerHomeOption(configuration.FilePathOption):
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 }
461 class ServerConfig(configuration.Config):
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 )
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 }
530 def __init__(self, config_file=None):
531 configuration.Config.__init__(self, config_file, self.SETTINGS)
532 self.sections.append("trackers")
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))
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)
554 def _get_name(self):
555 return "Roundup server"
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
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)
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]
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])
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
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)
599 def finish(self):
600 RoundupRequestHandler.finish(self)
601 if self.CONFIG["SSL"]:
602 self.request.shutdown()
603 self.request.close()
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
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
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
654 try:
655 import win32serviceutil
656 except:
657 RoundupService = None
658 else:
660 # allow the win32
661 import win32service
663 class SvcShutdown(Exception):
664 pass
666 class RoundupService(win32serviceutil.ServiceFramework):
668 _svc_name_ = "roundup"
669 _svc_display_name_ = "Roundup Bug Tracker"
671 running = 0
672 server = None
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)
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)
710 def usage(message=''):
711 if RoundupService:
712 os_part = \
713 ""''' -c <Command> Windows Service options.
714 If you want to run the server as a Windows Service, you
715 must use configuration file to specify tracker homes.
716 Logfile option is required to run Roundup Tracker service.
717 Typing "roundup-server -c help" shows Windows Services
718 specifics.'''
719 else:
720 os_part = ""''' -u <UID> runs the Roundup web server as this UID
721 -g <GID> runs the Roundup web server as this GID
722 -d <PIDfile> run the server in the background and write the server's PID
723 to the file indicated by PIDfile. The -l option *must* be
724 specified if -d is used.'''
725 if message:
726 message += '\n'
727 print _('''%(message)sUsage: roundup-server [options] [name=tracker home]*
729 Options:
730 -v print the Roundup version number and exit
731 -h print this text and exit
732 -S create or update configuration file and exit
733 -C <fname> use configuration file <fname>
734 -n <name> set the host name of the Roundup web server instance
735 -p <port> set the port to listen on (default: %(port)s)
736 -l <fname> log to the file indicated by fname instead of stderr/stdout
737 -N log client machine names instead of IP addresses (much slower)
738 -i <fname> set tracker index template
739 -s enable SSL
740 -e <fname> PEM file containing SSL key and certificate
741 -t <mode> multiprocess mode (default: %(mp_def)s).
742 Allowed values: %(mp_types)s.
743 %(os_part)s
745 Long options:
746 --version print the Roundup version number and exit
747 --help print this text and exit
748 --save-config create or update configuration file and exit
749 --config <fname> use configuration file <fname>
750 All settings of the [main] section of the configuration file
751 also may be specified in form --<name>=<value>
753 Examples:
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
759 roundup-server -C /opt/roundup/etc/roundup-server.ini
761 roundup-server support=/var/spool/roundup-trackers/support
763 roundup-server -d /var/run/roundup.pid -l /var/log/roundup.log \\
764 support=/var/spool/roundup-trackers/support
766 Configuration file format:
767 Roundup Server configuration file has common .ini file format.
768 Configuration file created with 'roundup-server -S' contains
769 detailed explanations for each option. Please see that file
770 for option descriptions.
772 How to use "name=tracker home":
773 These arguments set the tracker home(s) to use. The name is how the
774 tracker is identified in the URL (it's the first part of the URL path).
775 The tracker home is the directory that was identified when you did
776 "roundup-admin init". You may specify any number of these name=home
777 pairs on the command-line. Make sure the name part doesn't include
778 any url-unsafe characters like spaces, as these confuse IE.
779 ''') % {
780 "message": message,
781 "os_part": os_part,
782 "port": DEFAULT_PORT,
783 "mp_def": DEFAULT_MULTIPROCESS,
784 "mp_types": ", ".join(MULTIPROCESS_TYPES),
785 }
788 def writepidfile(pidfile):
789 ''' Write a pidfile (only). Do not daemonize. '''
790 pid = os.getpid()
791 if pid:
792 pidfile = open(pidfile, 'w')
793 pidfile.write(str(pid))
794 pidfile.close()
796 def daemonize(pidfile):
797 ''' Turn this process into a daemon.
798 - make sure the sys.std(in|out|err) are completely cut off
799 - make our parent PID 1
801 Write our new PID to the pidfile.
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)
810 # Create new session
811 os.setsid()
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)
821 os.chdir("/")
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)
830 undefined = []
831 def run(port=undefined, success_message=None):
832 ''' Script entry point - handle args and figure out what to to.
833 '''
834 config = ServerConfig()
835 # additional options
836 short_options = "hvS"
837 if RoundupService:
838 short_options += 'c'
839 try:
840 (optlist, args) = config.getopt(sys.argv[1:],
841 short_options, ("help", "version", "save-config",))
842 except (getopt.GetoptError, configuration.ConfigurationError), e:
843 usage(str(e))
844 return
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
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
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
899 # port number in function arguments overrides config and command line
900 if port is not undefined:
901 config.PORT = port
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"])
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"])
922 # create the server
923 httpd = config.get_server()
925 if success_message:
926 print success_message
927 else:
928 print _('Roundup server started on %(HOST)s:%(PORT)s') \
929 % config
931 try:
932 httpd.serve_forever()
933 except KeyboardInterrupt:
934 print 'Keyboard Interrupt: exiting'
936 if __name__ == '__main__':
937 run()
939 # vim: sts=4 sw=4 et si