Mercurial > p > roundup > code
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') \ |
