comparison roundup/scripts/roundup_server.py @ 2771:d385f6c1d4ed

let config handle command line options. support long options (names are same as in config file settings) and config file creation from command line. WARNING! config layout is changed: - main options are in 'main' section (was: 'server' section); - 'log_ip' changed to 'log_hostnames' with reverse meaning to conform to '-N' command line option. - trackers are all defined in section 'trackers', with option name being tracker name (was: separate sections per tracker).
author Alexander Smishlajev <a1s@users.sourceforge.net>
date Sun, 17 Oct 2004 17:54:48 +0000
parents ea18be87d68d
children 52460ff89e10
comparison
equal deleted inserted replaced
2770:cdf6787ffeda 2771:d385f6c1d4ed
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.62 2004-09-21 09:29:18 a1s Exp $ 20 $Id: roundup_server.py,v 1.63 2004-10-17 17:54:48 a1s Exp $
21 """ 21 """
22 __docformat__ = 'restructuredtext' 22 __docformat__ = 'restructuredtext'
23 23
24 # python version check 24 # python version check
25 from roundup import version_check 25 from roundup import configuration, version_check
26 from roundup import __version__ as roundup_version 26 from roundup import __version__ as roundup_version
27 27
28 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp 28 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
29 import SocketServer, BaseHTTPServer, socket, errno, ConfigParser 29 import SocketServer, BaseHTTPServer, socket, errno, ConfigParser
30 30
367 The tracker home is the directory that was identified when you did 367 The tracker home is the directory that was identified when you did
368 "roundup-admin init". You may specify any number of these name=home 368 "roundup-admin init". You may specify any number of these name=home
369 pairs on the command-line. Make sure the name part doesn't include 369 pairs on the command-line. Make sure the name part doesn't include
370 any url-unsafe characters like spaces, as these confuse IE. 370 any url-unsafe characters like spaces, as these confuse IE.
371 ''')%locals() 371 ''')%locals()
372 sys.exit(0) 372 #sys.exit(0)
373 373
374 374
375 def daemonize(pidfile): 375 def daemonize(pidfile):
376 ''' Turn this process into a daemon. 376 ''' Turn this process into a daemon.
377 - make sure the sys.std(in|out|err) are completely cut off 377 - make sure the sys.std(in|out|err) are completely cut off
459 pwd.getpwuid(uid) 459 pwd.getpwuid(uid)
460 except KeyError: 460 except KeyError:
461 raise ValueError, _("User %(user)s doesn't exist")%locals() 461 raise ValueError, _("User %(user)s doesn't exist")%locals()
462 os.setuid(uid) 462 os.setuid(uid)
463 463
464 class TrackerHomeOption(configuration.FilePathOption):
465 # Tracker homes do not need description strings
466 # attached to FilePathOption. Description appears once
467 # before the trackers section.
468 class_description = ""
469
470 class ServerConfig(configuration.Config):
471
472 SETTINGS = (
473 ("main", (
474 (configuration.Option, "host", "",
475 "Host name of the Roundup web server instance.\n"
476 "If empty, listen on all network interfaces."),
477 (configuration.IntegerNumberOption, "port", DEFAULT_PORT,
478 "Port to listen on."),
479 (configuration.NullableOption, "user", "",
480 "User ID as which the server will answer requests.\n"
481 "In order to use this option, "
482 "the server must be run initially as root.\n"
483 "Availability: Unix."),
484 (configuration.NullableOption, "group", "",
485 "Group ID as which the server will answer requests.\n"
486 "In order to use this option, "
487 "the server must be run initially as root.\n"
488 "Availability: Unix."),
489 (configuration.BooleanOption, "log_hostnames", "no",
490 "Log client machine names instead of IP addresses "
491 "(much slower)"),
492 (configuration.NullableFilePathOption, "pidfile", "",
493 "File to which the server records "
494 "the process id of the daemon.\n"
495 "If this option is not set, "
496 "the server will run in foreground\n"),
497 (configuration.NullableFilePathOption, "logfile", "",
498 "Log file path. If unset, log to stderr."),
499 )),
500 ("trackers", (), "Roundup trackers to serve.\n"
501 "Each option in this section defines single Roundup tracker.\n"
502 "Option name identifies the tracker and will appear in the URL.\n"
503 "Option value is tracker home directory path.\n"
504 "The path may be either absolute or relative\n"
505 "to the directory containig this config file."),
506 )
507
508 def __init__(self, config_file=None):
509 configuration.Config.__init__(self, config_file, self.SETTINGS)
510
511 def _adjust_options(self, config):
512 """Add options for tracker homes"""
513 # return early if there are no tracker definitions.
514 # trackers must be specified on the command line.
515 if not config.has_section("trackers"):
516 return
517 # config defaults appear in all sections.
518 # filter them out.
519 defaults = config.defaults().keys()
520 for name in config.options("trackers"):
521 if name not in defaults:
522 self.add_option(TrackerHomeOption(self, "trackers", name))
523
524 def _get_name(self):
525 return "Roundup server"
526
527 def trackers(self):
528 """Return tracker definitions as a list of (name, home) pairs"""
529 trackers = []
530 for option in self._get_section_options("trackers"):
531 trackers.append((option, self["TRACKERS_" + option.upper()]))
532 return trackers
533
464 undefined = [] 534 undefined = []
465 def run(port=undefined, success_message=None): 535 def run(port=undefined, success_message=None):
466 ''' Script entry point - handle args and figure out what to to. 536 ''' Script entry point - handle args and figure out what to to.
467 ''' 537 '''
468 # time out after a minute if we can 538 # time out after a minute if we can
469 import socket 539 import socket
470 if hasattr(socket, 'setdefaulttimeout'): 540 if hasattr(socket, 'setdefaulttimeout'):
471 socket.setdefaulttimeout(60) 541 socket.setdefaulttimeout(60)
472 542
473 hostname = pidfile = logfile = user = group = svc_args = log_ip = undefined 543 config = ServerConfig()
474 config = None 544
475 545 options = "hvS"
546 if RoundupService:
547 options += 'c'
476 try: 548 try:
477 # handle the command-line args 549 (optlist, args) = config.getopt(sys.argv[1:],
478 options = 'n:p:g:u:d:l:C:hNv' 550 options, ("help", "version", "save-config",),
479 if RoundupService: 551 host="n:", port="p:", group="g:", user="u:",
480 options += 'c' 552 logfile="l:", pidfile="d:", log_hostnames="N")
481 553 except (getopt.GetoptError), e: #, configuration.ConfigurationError), e:
482 try: 554 usage(str(e))
483 optlist, args = getopt.getopt(sys.argv[1:], options) 555 return
484 except getopt.GetoptError, e: 556
485 usage(str(e)) 557 # if running in windows service mode, don't do any other stuff
486 558 if ("-c", "") in optlist:
559 RoundupService.address = (config.HOST, config.PORT)
560 # XXX why the 1st argument to the service is "-c"
561 # instead of the script name???
562 return win32serviceutil.HandleCommandLine(RoundupService,
563 argv=["-c"] + args)
564
565 # add tracker names from command line.
566 # this is done early to let '--save-config' handle the trackers.
567 if args:
568 for arg in args:
569 try:
570 name, home = arg.split('=')
571 except ValueError:
572 raise ValueError, _("Instances must be name=home")
573 config.add_option(
574 configuration.FilePathOption(config, "trackers", name))
575 config["TRACKERS_" + name.upper()] = home
576
577 # handle remaining options
578 if optlist:
487 for (opt, arg) in optlist: 579 for (opt, arg) in optlist:
488 if opt == '-n': hostname = arg 580 if opt in ("-h", "--help"):
489 elif opt == '-v': 581 usage()
490 print '%s (python %s)'%(roundup_version, sys.version.split()[0]) 582 elif opt in ("-v", "--version"):
491 return 583 print '%s (python %s)' % (roundup_version,
492 elif opt == '-p': port = int(arg) 584 sys.version.split()[0])
493 elif opt == '-u': user = arg 585 elif opt in ("-S", "--save-config"):
494 elif opt == '-g': group = arg 586 config.save()
495 elif opt == '-d': pidfile = os.path.abspath(arg) 587 print _("Configuration saved to %s") % config.filepath
496 elif opt == '-l': logfile = os.path.abspath(arg) 588 # any of the above options prevent server from running
497 elif opt == '-h': usage() 589 return
498 elif opt == '-N': log_ip = 0 590
499 elif opt == '-c': svc_args = [opt] + args; args = None 591 RoundupRequestHandler.LOG_IPADDRESS = not config.LOG_HOSTNAMES
500 elif opt == '-C': config = arg 592
501 593 # obtain server before changing user id - allows to use port <
502 if svc_args and len(optlist) > 1: 594 # 1024 if started as root
503 raise ValueError, _("windows service option must be the only one") 595 try:
504 596 httpd = server_class((config.HOST, config.PORT), RoundupRequestHandler)
505 if pidfile and not logfile: 597 except socket.error, e:
506 raise ValueError, _("logfile *must* be specified if pidfile is") 598 if e[0] == errno.EADDRINUSE:
507 599 raise socket.error, \
508 # handle the config file 600 _("Unable to bind to port %s, port already in use.") \
509 if config: 601 % config.PORT
510 cfg = ConfigParser.ConfigParser()
511 cfg.read(filename)
512 if port is undefined and cfg.has_option('server', 'port'):
513 port = cfg.get('server', 'port')
514 if user is undefined and cfg.has_option('server', 'user'):
515 user = cfg.get('server', 'user')
516 if group is undefined and cfg.has_option('server', 'group'):
517 group = cfg.get('server', 'group')
518 if log_ip is undefined and cfg.has_option('server', 'log_ip'):
519 RoundupRequestHandler.LOG_IPADDRESS = cfg.getboolean('server',
520 'log_ip')
521 if pidfile is undefined and cfg.has_option('server', 'pidfile'):
522 pidfile = cfg.get('server', 'pidfile')
523 if logfile is undefined and cfg.has_option('server', 'logfile'):
524 logfile = cfg.get('server', 'logfile')
525 homes = RoundupRequestHandler.TRACKER_HOMES
526 for section in cfg.sections():
527 if section == 'server':
528 continue
529 homes[section] = cfg.get(section, 'home')
530
531 # defaults
532 if hostname is undefined:
533 hostname = ''
534 if port is undefined:
535 port = DEFAULT_PORT
536 if group is undefined:
537 group = None
538 if user is undefined:
539 user = None
540 if svc_args is undefined:
541 svc_args = None
542
543 # obtain server before changing user id - allows to use port <
544 # 1024 if started as root
545 address = (hostname, port)
546 try:
547 httpd = server_class(address, RoundupRequestHandler)
548 except socket.error, e:
549 if e[0] == errno.EADDRINUSE:
550 raise socket.error, \
551 _("Unable to bind to port %s, port already in use."%port)
552 raise
553
554 # change user and/or group
555 setgid(group)
556 setuid(user)
557
558 # handle tracker specs
559 if args:
560 for arg in args:
561 try:
562 name, home = arg.split('=')
563 except ValueError:
564 raise ValueError, _("Instances must be name=home")
565 home = os.path.abspath(home)
566 RoundupRequestHandler.TRACKER_HOMES[name] = home
567 except SystemExit:
568 raise 602 raise
569 except ValueError: 603
570 usage(error()) 604 # change user and/or group
571 # except: 605 setgid(config.GROUP)
572 # print error() 606 setuid(config.USER)
573 # sys.exit(1) 607
608 # apply tracker specs
609 for (name, home) in config.trackers():
610 home = os.path.abspath(home)
611 RoundupRequestHandler.TRACKER_HOMES[name] = home
574 612
575 # we don't want the cgi module interpreting the command-line args ;) 613 # we don't want the cgi module interpreting the command-line args ;)
576 sys.argv = sys.argv[:1] 614 sys.argv = sys.argv[:1]
577 615
578 # fork the server from our parent if a pidfile is specified 616 # fork the server from our parent if a pidfile is specified
579 if pidfile: 617 if config.PIDFILE:
580 if not hasattr(os, 'fork'): 618 if not hasattr(os, 'fork'):
581 print _("Sorry, you can't run the server as a daemon" 619 print _("Sorry, you can't run the server as a daemon"
582 " on this Operating System") 620 " on this Operating System")
583 sys.exit(0) 621 sys.exit(0)
584 else: 622 else:
585 daemonize(pidfile) 623 daemonize(config.PIDFILE)
586
587 if svc_args is not None:
588 # don't do any other stuff
589 RoundupService.address = address
590 return win32serviceutil.HandleCommandLine(RoundupService, argv=svc_args)
591 624
592 # redirect stdout/stderr to our logfile 625 # redirect stdout/stderr to our logfile
593 if logfile: 626 if config.LOGFILE:
594 # appending, unbuffered 627 # appending, unbuffered
595 sys.stdout = sys.stderr = open(logfile, 'a', 0) 628 sys.stdout = sys.stderr = open(config.LOGFILE, 'a', 0)
596 629
597 if success_message: 630 if success_message:
598 print success_message 631 print success_message
599 else: 632 else:
600 print _('Roundup server started on %(address)s')%locals() 633 print _('Roundup server started on %(HOST)s:%(PORT)s') \
634 % config
601 635
602 try: 636 try:
603 httpd.serve_forever() 637 httpd.serve_forever()
604 except KeyboardInterrupt: 638 except KeyboardInterrupt:
605 print 'Keyboard Interrupt: exiting' 639 print 'Keyboard Interrupt: exiting'

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