diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2012-01-15 01:24:08 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2012-01-15 01:24:08 (GMT) |
commit | 0b3bc7dabbe9c3bf344174539fdcef608a894bbf (patch) | |
tree | a2e2a176e8419a6af81c255cb315a5035b6bbfb9 | |
parent | 235acae98ad8893c3139038962cb916a847dd368 (diff) |
Implement Server class to fast run wsgi server
-rw-r--r-- | restful_document/env.py | 27 | ||||
-rw-r--r-- | restful_document/printf.py | 224 | ||||
-rw-r--r-- | restful_document/server.py | 255 |
3 files changed, 506 insertions, 0 deletions
diff --git a/restful_document/env.py b/restful_document/env.py index bb4d991..52f04d7 100644 --- a/restful_document/env.py +++ b/restful_document/env.py @@ -18,12 +18,39 @@ import threading from urlparse import parse_qsl from gettext import gettext as _ +from restful_document import util from restful_document.util import enforce _default = object() +host = util.Option( + _('hostname to listen incomming connections'), + default='0.0.0.0') + +port = util.Option( + _('port number to listen incomming connections'), + default=8000, type_cast=int) + +debug = util.Option( + _('debug logging level; multiple argument'), + default=0, type_cast=int, short_option='-D', action='count') + +foreground = util.Option( + _('do not send the process into the background'), + default=False, type_cast=util.Option.bool_cast, short_option='-F', + action='store_true') + +logdir = util.Option( + _('path to the directory to place log files'), + default='/var/log') + +rundir = util.Option( + _('path to the directory to place pid files'), + default='/var/run') + + def pop_str(name, kwargs, default=_default): if name in kwargs: return kwargs.pop(name) diff --git a/restful_document/printf.py b/restful_document/printf.py new file mode 100644 index 0000000..cfa7644 --- /dev/null +++ b/restful_document/printf.py @@ -0,0 +1,224 @@ +# Copyright (C) 2011, Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Console output routines. + +$Repo: git://git.sugarlabs.org/alsroot/codelets.git$ +$File: src/printf.py$ +$Date$ + +""" + +import sys +import logging +from gettext import gettext as _ + + +#: Disable/enable non-status output. +VERBOSE = True +#: Disable/enable any output. +QUIET = False + +RESET = '\033[0m' +BOLD = '\033[1m' +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = \ + ['\033[1;%dm' % (30 + i_) for i_ in range(8)] + +_hints = [] +_last_line_len = 0 +_last_progress = [] +_screen_width = None + + +def dump(message, *args): + """Print verbatim text. + + :param message: + text to print + :param \*args: + `%` arguments to expand `message` value + + """ + _dump(False, sys.stdout, '', [message, args], '\n') + + +def info(message, *args): + """Print information text. + + :param message: + text to print + :param \*args: + `%` arguments to expand `message` value + + """ + _dump(True, sys.stdout, None, [message, args], '\n') + _dump_progress() + + +def exception(message=None, *args): + """Print exception text. + + Call this function in `try..except` block after getting exceptions. + + :param message: + text to print + :param \*args: + `%` arguments to expand `message` value + + """ + import traceback + + klass, error, tb = sys.exc_info() + + tb_list = [] + for line in traceback.format_exception(klass, error, tb): + tb_list.extend([i.rstrip() for i in line.strip().split('\n')]) + + if type(error).__name__ == 'dbus.exceptions.DBusException': + dbus_tb = str(error).split('\n') + if len(dbus_tb) == 1: + error = dbus_tb[0] + else: + # Strip the last empty line + del dbus_tb[-1] + error = '%s:%s' % \ + (dbus_tb[0].split(':')[0], dbus_tb[-1].split(':', 1)[-1]) + + if message and args: + message = message % args + + error = str(error) or _('Something weird happened') + if message: + message += ': %s' % error + else: + message = str(error) + _dump(True, sys.stdout, None, message, '\n') + + if logging.getLogger().level > logging.INFO: + hint(_('Use -D argument for debug info, ' \ + '-DD for full debuging output and tracebacks')) + elif logging.getLogger().level > logging.DEBUG: + hint(_('Use -DD argument for full debuging output and tracebacks')) + else: + for i in tb_list: + _dump(True, sys.stdout, ' ', i, '\n') + + _dump_progress() + + +def scan_yn(message, *args): + """Request for Y/N input. + + :param message: + prefix text to print + :param \*args: + `%` arguments to expand `message` value + :returns: + `True` if user's input was `Y` + + """ + _dump(True, sys.stderr, None, [message, args], ' [Y/N] ') + answer = raw_input() + _dump_progress() + return answer and answer in 'Yy' + + +def progress(message, *args): + """Print status line text. + + Status line will be shown as the last line all time and will be cleared + on program exit. + + :param message: + prefix text to print + :param \*args: + `%` arguments to expand `message` value + + """ + _last_progress[:] = [message, args] + _dump_progress() + + +def clear_progress(): + """Clear status line on program exit.""" + if _last_line_len: + sys.stderr.write(chr(13) + ' ' * _last_line_len + chr(13)) + + +def hint(message, *args): + """Add new hint. + + All hint will be queued to print them at once in `flush_hints()` function + on program exit. + + :param message: + prefix text to print + :param \*args: + `%` arguments to expand `message` value + + """ + if args: + message = message % args + _hints.append(message) + + +def flush_hints(): + """Print all queued hints.""" + while _hints: + _dump(True, sys.stderr, None, _hints.pop(0), '\n') + + +def _dump(is_status, stream, prefix, *args): + if not VERBOSE or QUIET: + return + + global _last_line_len + global _screen_width + + if _screen_width is None: + try: + import curses + curses.setupterm() + _screen_width = curses.tigetnum('cols') or 80 + except Exception, error: + logging.info('Cannot get screen width: %s', error) + _screen_width = 80 + + if prefix is None: + prefix = '-- ' + + clear_progress() + _last_line_len = 0 + + for i in [prefix] + list(args): + if isinstance(i, list): + if i: + message, message_args = i + if message_args: + message = message % message_args + else: + message = i + + stream.write(message) + + if is_status: + _last_line_len += len(message) + + _last_line_len = min(_last_line_len, _screen_width) + + +def _dump_progress(): + _dump(True, sys.stderr, ' ', _last_progress, chr(13)) + sys.stderr.flush() diff --git a/restful_document/server.py b/restful_document/server.py new file mode 100644 index 0000000..caa79b7 --- /dev/null +++ b/restful_document/server.py @@ -0,0 +1,255 @@ +# Copyright (C) 2012, Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import signal +import atexit +import logging +from optparse import OptionParser +from os.path import basename, join, abspath, exists +from gettext import gettext as _ + +from gevent.wsgi import WSGIServer + +import active_document as ad +import restful_document as rd + +from restful_document import env, util, printf +from restful_document.util import enforce + + +class Server(object): + + def __init__(self, classes, name, description, version, homepage): + self._classes = classes + self._name = name + self._description = description + self._version = version + self._homepage = homepage + self._args = [] + + def serve_forever(self): + parser = OptionParser( + usage='%prog [OPTIONS] [COMMAND]', + description=self._description, + add_help_option=False) + parser.print_version = \ + lambda: sys.stdout.write('%s\n' % self._version) + parser.add_option('-h', '--help', + help=_('show this help message and exit'), + action='store_true') + parser.add_option('-V', '--version', + help=_('show version number and exit'), + action='version') + + util.Option.seek('active-document', ad.env) + util.Option.seek('main', env) + + util.Option.bind(parser, [ + '/etc/%s.conf' % self._name, + '~/.config/%s/config' % self._name, + ]) + options, self._args = parser.parse_args() + util.Option.merge(options) + + if not self._args and not options.help: + prog = basename(sys.argv[0]) + print 'Usage: %s [OPTIONS] [COMMAND]' % prog + print ' %s -h|--help' % prog + print + print parser.description + print _HELP % self._homepage + exit(0) + + if options.help: + parser.print_help() + print _HELP % self._homepage + exit(0) + + command = self._args.pop(0) + + if not env.debug.value: + logging_level = logging.WARNING + elif env.debug.value == 1: + logging_level = logging.INFO + else: + logging_level = logging.DEBUG + logging_format = '%(asctime)s %(levelname)s %(name)s: %(message)s' + if env.foreground.value or command not in ['start']: + logging_format = '-- %s' % logging_format + logging.basicConfig(level=logging_level, format=logging_format) + + try: + enforce(hasattr(self, '_cmd_' + command), + _('Unknown command "%s"') % command) + exit(getattr(self, '_cmd_' + command)() or 0) + except Exception: + printf.exception(_('Aborted %s due to error'), self._name) + exit(1) + finally: + printf.flush_hints() + + def _cmd_config(self): + if self._args: + opt = self._args.pop(0) + enforce(opt in util.Option.items, _('Unknown option "%s"'), opt) + exit(0 if bool(util.Option.items[opt].value) else 1) + else: + print '\n'.join(util.Option.export()) + + def _cmd_start(self): + pidfile, pid = self._check_for_instance() + if pid: + logging.warning(_('%s is already run with pid %s'), + self._name, pid) + return 1 + if env.foreground.value: + self._launch() + else: + self._forward_stdout() + self._daemonize(pidfile) + return 0 + + def _cmd_stop(self): + __, pid = self._check_for_instance() + if pid: + os.kill(pid, signal.SIGTERM) + return 0 + else: + logging.warning(_('%s is not run'), self._name) + return 1 + + def _cmd_status(self): + __, pid = self._check_for_instance() + if pid: + printf.info(_('%s started'), self._name) + return 0 + else: + printf.info(_('%s stopped'), self._name) + return 1 + + def _cmd_reload(self): + __, pid = self._check_for_instance() + if pid: + os.kill(pid, signal.SIGHUP) + logging.info(_('Reload %s process'), self._name) + + def _cmd_restart(self): + self._cmd_stop() + # TODO More reliable method + import time + time.sleep(3) + self._cmd_start() + + def _launch(self): + logging.info(_('Start %s on %s:%s'), + self._name, env.host.value, env.port.value) + + httpd = WSGIServer((env.host.value, env.port.value), + rd.Router(self._classes)) + + def sigterm_cb(signum, frame): + logging.info(_('Got signal %s, will stop %s'), signum, self._name) + httpd.stop() + + def sighup_cb(signum, frame): + logging.info(_('Reload %s on SIGHUP signal'), self._name) + self._forward_stdout() + + signal.signal(signal.SIGINT, sigterm_cb) + signal.signal(signal.SIGTERM, sigterm_cb) + signal.signal(signal.SIGHUP, sighup_cb) + + try: + httpd.serve_forever() + finally: + httpd.stop() + ad.close() + + def _check_for_instance(self): + pid = None + pidfile = join(env.rundir.value, '%s.pid' % self._name) + if exists(pidfile): + try: + pid = int(file(pidfile).read().strip()) + os.getpgid(pid) + except (ValueError, OSError): + pid = None + return pidfile, pid + + def _forward_stdout(self): + if not exists(env.logdir.value): + os.makedirs(env.logdir.value) + log_path = abspath(join(env.logdir.value, '%s.log' % self._name)) + sys.stdout.flush() + sys.stderr.flush() + logfile = file(log_path, 'a+') + os.dup2(logfile.fileno(), sys.stdout.fileno()) + os.dup2(logfile.fileno(), sys.stderr.fileno()) + logfile.close() + + def _daemonize(self, pid_path): + if not exists(env.rundir.value): + os.makedirs(env.rundir.value) + pid_path = abspath(pid_path) + + if os.fork() > 0: + # Exit parent of the first child + return + + # Decouple from parent environment + os.chdir(os.sep) + os.setsid() + + if os.fork() > 0: + # Exit from second parent + # pylint: disable-msg=W0212 + os._exit(0) + + # Redirect standard file descriptors + if not sys.stdin.closed: + stdin = file('/dev/null') + os.dup2(stdin.fileno(), sys.stdin.fileno()) + + pidfile = file(pid_path, 'w') + pidfile.write(str(os.getpid())) + pidfile.close() + atexit.register(lambda: os.remove(pid_path)) + + try: + self._launch() + status = 0 + except Exception: + logging.exception(_('Abort sugar-server due to error')) + status = 1 + + exit(status) + + +_HELP = """ +Commands: + start start in daemon mode + restart restart daemon + reload reopen log files in daemon mode + stop stop daemon + status check for launched daemon + config output current configuration + +See %s.""" + + +if __name__ == '__main__': + Server([], 'name', 'description', 'version', 'homepage').serve_forever() |