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