comparison roundup/scripts/roundup_server.py @ 2835:9a6b451b1ba6

implement threading server; added config oprion 'multiprocess' and command line option '-t' for multiprocess type; by default, start in the best available multiprocess mode: fork, thread, none; most of the server configuration is done by ServerConfig object. this is the first step to command line options support in windows service.
author Alexander Smishlajev <a1s@users.sourceforge.net>
date Fri, 29 Oct 2004 13:41:25 +0000
parents 22c381f3f448
children 91b2d50f0b1a
comparison
equal deleted inserted replaced
2834:3f93d4b29620 2835:9a6b451b1ba6
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 # 16 #
17 17
18 """Command-line script that runs a server over roundup.cgi.client. 18 """Command-line script that runs a server over roundup.cgi.client.
19 19
20 $Id: roundup_server.py,v 1.66 2004-10-18 07:43:58 a1s Exp $ 20 $Id: roundup_server.py,v 1.67 2004-10-29 13:41:25 a1s Exp $
21 """ 21 """
22 __docformat__ = 'restructuredtext' 22 __docformat__ = 'restructuredtext'
23
24 import socket
25 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
26 import SocketServer, BaseHTTPServer, socket, errno, ConfigParser
23 27
24 # python version check 28 # python version check
25 from roundup import configuration, version_check 29 from roundup import configuration, version_check
26 from roundup import __version__ as roundup_version 30 from roundup import __version__ as roundup_version
27
28 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
29 import SocketServer, BaseHTTPServer, socket, errno, ConfigParser
30 31
31 # Roundup modules of use here 32 # Roundup modules of use here
32 from roundup.cgi import cgitb, client 33 from roundup.cgi import cgitb, client
33 import roundup.instance 34 import roundup.instance
34 from roundup.i18n import _ 35 from roundup.i18n import _
53 bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g 54 bn3Zuj8M9Hepux6VfZtW1yA6K7cfGqVu8TL325u+fHTb71QKbk+7TZQ+lTc6RcnpqW8qmVQBoj/g
54 23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI= 55 23eo0sr/NIGvB37K+lOWXMvJ+uWFeKGU/03Cb7n3D4M3wxI=
55 '''.strip())) 56 '''.strip()))
56 57
57 DEFAULT_PORT = 8080 58 DEFAULT_PORT = 8080
59
60 # See what types of multiprocess server are available
61 MULTIPROCESS_TYPES = ["none"]
62 try:
63 import thread
64 except ImportError:
65 pass
66 else:
67 MULTIPROCESS_TYPES.append("thread")
68 if hasattr(os, 'fork'):
69 MULTIPROCESS_TYPES.append("fork")
70 DEFAULT_MULTIPROCESS = MULTIPROCESS_TYPES[-1]
58 71
59 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 72 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
60 TRACKER_HOMES = {} 73 TRACKER_HOMES = {}
61 LOG_IPADDRESS = 1 74 LOG_IPADDRESS = 1
62 75
214 227
215 def error(): 228 def error():
216 exc_type, exc_value = sys.exc_info()[:2] 229 exc_type, exc_value = sys.exc_info()[:2]
217 return _('Error: %s: %s' % (exc_type, exc_value)) 230 return _('Error: %s: %s' % (exc_type, exc_value))
218 231
219 # See whether we can use the forking server 232 def setgid(group):
220 if hasattr(os, 'fork'): 233 if group is None:
221 class server_class(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): 234 return
222 pass 235 if not hasattr(os, 'setgid'):
223 else: 236 return
224 server_class = BaseHTTPServer.HTTPServer 237
238 # if root, setgid to the running user
239 if not os.getuid():
240 print _('WARNING: ignoring "-g" argument, not root')
241 return
242
243 try:
244 import grp
245 except ImportError:
246 raise ValueError, _("Can't change groups - no grp module")
247 try:
248 try:
249 gid = int(group)
250 except ValueError:
251 gid = grp.getgrnam(group)[2]
252 else:
253 grp.getgrgid(gid)
254 except KeyError:
255 raise ValueError,_("Group %(group)s doesn't exist")%locals()
256 os.setgid(gid)
257
258 def setuid(user):
259 if not hasattr(os, 'getuid'):
260 return
261
262 # People can remove this check if they're really determined
263 if user is None:
264 if os.getuid():
265 return
266 raise ValueError, _("Can't run as root!")
267
268 if os.getuid():
269 print _('WARNING: ignoring "-u" argument, not root')
270
271 try:
272 import pwd
273 except ImportError:
274 raise ValueError, _("Can't change users - no pwd module")
275 try:
276 try:
277 uid = int(user)
278 except ValueError:
279 uid = pwd.getpwnam(user)[2]
280 else:
281 pwd.getpwuid(uid)
282 except KeyError:
283 raise ValueError, _("User %(user)s doesn't exist")%locals()
284 os.setuid(uid)
285
286 class TrackerHomeOption(configuration.FilePathOption):
287
288 # Tracker homes do not need any description strings
289 def format(self):
290 return "%(name)s = %(value)s\n" % {
291 "name": self.setting,
292 "value": self.value2str(self._value),
293 }
294
295 class ServerConfig(configuration.Config):
296
297 SETTINGS = (
298 ("main", (
299 (configuration.Option, "host", "",
300 "Host name of the Roundup web server instance.\n"
301 "If empty, listen on all network interfaces."),
302 (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
303 "Port to listen on."),
304 (configuration.NullableOption, "user", "",
305 "User ID as which the server will answer requests.\n"
306 "In order to use this option, "
307 "the server must be run initially as root.\n"
308 "Availability: Unix."),
309 (configuration.NullableOption, "group", "",
310 "Group ID as which the server will answer requests.\n"
311 "In order to use this option, "
312 "the server must be run initially as root.\n"
313 "Availability: Unix."),
314 (configuration.BooleanOption, "log_hostnames", "no",
315 "Log client machine names instead of IP addresses "
316 "(much slower)"),
317 (configuration.NullableFilePathOption, "pidfile", "",
318 "File to which the server records "
319 "the process id of the daemon.\n"
320 "If this option is not set, "
321 "the server will run in foreground\n"),
322 (configuration.NullableFilePathOption, "logfile", "",
323 "Log file path. If unset, log to stderr."),
324 (configuration.Option, "multiprocess", DEFAULT_MULTIPROCESS,
325 "Set processing of each request in separate subprocess.\n"
326 "Allowed values: %s." % ", ".join(MULTIPROCESS_TYPES)),
327 )),
328 ("trackers", (), "Roundup trackers to serve.\n"
329 "Each option in this section defines single Roundup tracker.\n"
330 "Option name identifies the tracker and will appear in the URL.\n"
331 "Option value is tracker home directory path.\n"
332 "The path may be either absolute or relative\n"
333 "to the directory containig this config file."),
334 )
335
336 # options recognized by config
337 OPTIONS = {
338 "host": "n:",
339 "port": "p:",
340 "group": "g:",
341 "user": "u:",
342 "logfile": "l:",
343 "pidfile": "d:",
344 "log_hostnames": "N",
345 "multiprocess": "t:",
346 }
347
348 def __init__(self, config_file=None):
349 configuration.Config.__init__(self, config_file, self.SETTINGS)
350
351 def _adjust_options(self, config):
352 """Add options for tracker homes"""
353 # return early if there are no tracker definitions.
354 # trackers must be specified on the command line.
355 if not config.has_section("trackers"):
356 return
357 # config defaults appear in all sections.
358 # filter them out.
359 defaults = config.defaults().keys()
360 for name in config.options("trackers"):
361 if name not in defaults:
362 self.add_option(TrackerHomeOption(self, "trackers", name))
363
364 def getopt(self, args, short_options="", long_options=(),
365 config_load_options=("C", "config"), **options
366 ):
367 options.update(self.OPTIONS)
368 return configuration.Config.getopt(self, args,
369 short_options, long_options, **options)
370
371 def _get_name(self):
372 return "Roundup server"
373
374 def trackers(self):
375 """Return tracker definitions as a list of (name, home) pairs"""
376 trackers = []
377 for option in self._get_section_options("trackers"):
378 trackers.append((option, os.path.abspath(
379 self["TRACKERS_" + option.upper()])))
380 return trackers
381
382 def get_server(self):
383 """Return HTTP server object to run"""
384 # redirect stdout/stderr to our logfile
385 # this is done early to have following messages go to this logfile
386 if self.LOGFILE:
387 # appending, unbuffered
388 sys.stdout = sys.stderr = open(self["LOGFILE"], 'a', 0)
389 # we don't want the cgi module interpreting the command-line args ;)
390 sys.argv = sys.argv[:1]
391 # build customized request handler class
392 class RequestHandler(RoundupRequestHandler):
393 LOG_IPADDRESS = not self.LOG_HOSTNAMES
394 TRACKER_HOMES = dict(self.trackers())
395 # obtain request server class
396 if self.MULTIPROCESS not in MULTIPROCESS_TYPES:
397 print _("Multiprocess mode \"%s\" is not available, "
398 "switching to single-process") % self.MULTIPROCESS
399 self.MULTIPROCESS = "none"
400 server_class = BaseHTTPServer.HTTPServer
401 elif self.MULTIPROCESS == "fork":
402 class server_class(SocketServer.ForkingMixIn,
403 BaseHTTPServer.HTTPServer):
404 pass
405 elif self.MULTIPROCESS == "thread":
406 class server_class(SocketServer.ThreadingMixIn,
407 BaseHTTPServer.HTTPServer):
408 pass
409 else:
410 server_class = BaseHTTPServer.HTTPServer
411 # obtain server before changing user id - allows to
412 # use port < 1024 if started as root
413 try:
414 httpd = server_class((self.HOST, self.PORT), RequestHandler)
415 except socket.error, e:
416 if e[0] == errno.EADDRINUSE:
417 raise socket.error, \
418 _("Unable to bind to port %s, port already in use.") \
419 % self.PORT
420 raise
421 # change user and/or group
422 setgid(self.GROUP)
423 setuid(self.USER)
424 # return the server
425 return httpd
225 426
226 try: 427 try:
227 import win32serviceutil 428 import win32serviceutil
228 except: 429 except:
229 RoundupService = None 430 RoundupService = None
405 devnull = os.open('/dev/null', 0) 606 devnull = os.open('/dev/null', 0)
406 os.dup2(devnull, 0) 607 os.dup2(devnull, 0)
407 os.dup2(devnull, 1) 608 os.dup2(devnull, 1)
408 os.dup2(devnull, 2) 609 os.dup2(devnull, 2)
409 610
410 def setgid(group):
411 if group is None:
412 return
413 if not hasattr(os, 'setgid'):
414 return
415
416 # if root, setgid to the running user
417 if not os.getuid():
418 print _('WARNING: ignoring "-g" argument, not root')
419 return
420
421 try:
422 import grp
423 except ImportError:
424 raise ValueError, _("Can't change groups - no grp module")
425 try:
426 try:
427 gid = int(group)
428 except ValueError:
429 gid = grp.getgrnam(group)[2]
430 else:
431 grp.getgrgid(gid)
432 except KeyError:
433 raise ValueError,_("Group %(group)s doesn't exist")%locals()
434 os.setgid(gid)
435
436 def setuid(user):
437 if not hasattr(os, 'getuid'):
438 return
439
440 # People can remove this check if they're really determined
441 if user is None:
442 if os.getuid():
443 return
444 raise ValueError, _("Can't run as root!")
445
446 if os.getuid():
447 print _('WARNING: ignoring "-u" argument, not root')
448
449 try:
450 import pwd
451 except ImportError:
452 raise ValueError, _("Can't change users - no pwd module")
453 try:
454 try:
455 uid = int(user)
456 except ValueError:
457 uid = pwd.getpwnam(user)[2]
458 else:
459 pwd.getpwuid(uid)
460 except KeyError:
461 raise ValueError, _("User %(user)s doesn't exist")%locals()
462 os.setuid(uid)
463
464 class TrackerHomeOption(configuration.FilePathOption):
465
466 # Tracker homes do not need any description strings
467 def format(self):
468 return "%(name)s = %(value)s\n" % {
469 "name": self.setting,
470 "value": self.value2str(self._value),
471 }
472
473 class ServerConfig(configuration.Config):
474
475 SETTINGS = (
476 ("main", (
477 (configuration.Option, "host", "",
478 "Host name of the Roundup web server instance.\n"
479 "If empty, listen on all network interfaces."),
480 (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
481 "Port to listen on."),
482 (configuration.NullableOption, "user", "",
483 "User ID as which the server will answer requests.\n"
484 "In order to use this option, "
485 "the server must be run initially as root.\n"
486 "Availability: Unix."),
487 (configuration.NullableOption, "group", "",
488 "Group ID as which the server will answer requests.\n"
489 "In order to use this option, "
490 "the server must be run initially as root.\n"
491 "Availability: Unix."),
492 (configuration.BooleanOption, "log_hostnames", "no",
493 "Log client machine names instead of IP addresses "
494 "(much slower)"),
495 (configuration.NullableFilePathOption, "pidfile", "",
496 "File to which the server records "
497 "the process id of the daemon.\n"
498 "If this option is not set, "
499 "the server will run in foreground\n"),
500 (configuration.NullableFilePathOption, "logfile", "",
501 "Log file path. If unset, log to stderr."),
502 )),
503 ("trackers", (), "Roundup trackers to serve.\n"
504 "Each option in this section defines single Roundup tracker.\n"
505 "Option name identifies the tracker and will appear in the URL.\n"
506 "Option value is tracker home directory path.\n"
507 "The path may be either absolute or relative\n"
508 "to the directory containig this config file."),
509 )
510
511 def __init__(self, config_file=None):
512 configuration.Config.__init__(self, config_file, self.SETTINGS)
513
514 def _adjust_options(self, config):
515 """Add options for tracker homes"""
516 # return early if there are no tracker definitions.
517 # trackers must be specified on the command line.
518 if not config.has_section("trackers"):
519 return
520 # config defaults appear in all sections.
521 # filter them out.
522 defaults = config.defaults().keys()
523 for name in config.options("trackers"):
524 if name not in defaults:
525 self.add_option(TrackerHomeOption(self, "trackers", name))
526
527 def _get_name(self):
528 return "Roundup server"
529
530 def trackers(self):
531 """Return tracker definitions as a list of (name, home) pairs"""
532 trackers = []
533 for option in self._get_section_options("trackers"):
534 trackers.append((option, self["TRACKERS_" + option.upper()]))
535 return trackers
536
537 undefined = [] 611 undefined = []
538 def run(port=undefined, success_message=None): 612 def run(port=undefined, success_message=None):
539 ''' Script entry point - handle args and figure out what to to. 613 ''' Script entry point - handle args and figure out what to to.
540 ''' 614 '''
541 # time out after a minute if we can 615 # time out after a minute if we can
542 import socket
543 if hasattr(socket, 'setdefaulttimeout'): 616 if hasattr(socket, 'setdefaulttimeout'):
544 socket.setdefaulttimeout(60) 617 socket.setdefaulttimeout(60)
545 618
546 config = ServerConfig() 619 config = ServerConfig()
547 620 # additional options
548 options = "hvS" 621 short_options = "hvS"
549 if RoundupService: 622 if RoundupService:
550 options += 'c' 623 short_options += 'c'
551 try: 624 try:
552 (optlist, args) = config.getopt(sys.argv[1:], 625 (optlist, args) = config.getopt(sys.argv[1:],
553 options, ("help", "version", "save-config",), 626 short_options, ("help", "version", "save-config",))
554 host="n:", port="p:", group="g:", user="u:",
555 logfile="l:", pidfile="d:", log_hostnames="N")
556 except (getopt.GetoptError, configuration.ConfigurationError), e: 627 except (getopt.GetoptError, configuration.ConfigurationError), e:
557 usage(str(e)) 628 usage(str(e))
558 return 629 return
559 630
560 # if running in windows service mode, don't do any other stuff 631 # if running in windows service mode, don't do any other stuff
561 if ("-c", "") in optlist: 632 if ("-c", "") in optlist:
562 RoundupService.address = (config.HOST, config.PORT)
563 # XXX why the 1st argument to the service is "-c"
564 # instead of the script name???
565 return win32serviceutil.HandleCommandLine(RoundupService, 633 return win32serviceutil.HandleCommandLine(RoundupService,
566 argv=["-c"] + args) 634 argv=sys.argv[:1] + args)
567 635
568 # add tracker names from command line. 636 # add tracker names from command line.
569 # this is done early to let '--save-config' handle the trackers. 637 # this is done early to let '--save-config' handle the trackers.
570 if args: 638 if args:
571 for arg in args: 639 for arg in args:
588 config.save() 656 config.save()
589 print _("Configuration saved to %s") % config.filepath 657 print _("Configuration saved to %s") % config.filepath
590 # any of the above options prevent server from running 658 # any of the above options prevent server from running
591 return 659 return
592 660
593 RoundupRequestHandler.LOG_IPADDRESS = not config.LOG_HOSTNAMES
594
595 # port number in function arguments overrides config and command line 661 # port number in function arguments overrides config and command line
596 if port is not undefined: 662 if port is not undefined:
597 config.PORT = port 663 config.PORT = port
598
599 # obtain server before changing user id - allows
600 # to use port < 1024 if started as root
601 try:
602 httpd = server_class((config.HOST, config.PORT), RoundupRequestHandler)
603 except socket.error, e:
604 if e[0] == errno.EADDRINUSE:
605 raise socket.error, \
606 _("Unable to bind to port %s, port already in use.") \
607 % config.PORT
608 raise
609
610 # change user and/or group
611 setgid(config.GROUP)
612 setuid(config.USER)
613
614 # apply tracker specs
615 for (name, home) in config.trackers():
616 home = os.path.abspath(home)
617 RoundupRequestHandler.TRACKER_HOMES[name] = home
618
619 # we don't want the cgi module interpreting the command-line args ;)
620 sys.argv = sys.argv[:1]
621 664
622 # fork the server from our parent if a pidfile is specified 665 # fork the server from our parent if a pidfile is specified
623 if config.PIDFILE: 666 if config.PIDFILE:
624 if not hasattr(os, 'fork'): 667 if not hasattr(os, 'fork'):
625 print _("Sorry, you can't run the server as a daemon" 668 print _("Sorry, you can't run the server as a daemon"
626 " on this Operating System") 669 " on this Operating System")
627 sys.exit(0) 670 sys.exit(0)
628 else: 671 else:
629 daemonize(config.PIDFILE) 672 daemonize(config.PIDFILE)
630 673
631 # redirect stdout/stderr to our logfile 674 # create the server
632 if config.LOGFILE: 675 httpd = config.get_server()
633 # appending, unbuffered
634 sys.stdout = sys.stderr = open(config.LOGFILE, 'a', 0)
635 676
636 if success_message: 677 if success_message:
637 print success_message 678 print success_message
638 else: 679 else:
639 print _('Roundup server started on %(HOST)s:%(PORT)s') \ 680 print _('Roundup server started on %(HOST)s:%(PORT)s') \

Roundup Issue Tracker: http://roundup-tracker.org/