Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2012-01-15 01:24:08 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2012-01-15 01:24:08 (GMT)
commit0b3bc7dabbe9c3bf344174539fdcef608a894bbf (patch)
treea2e2a176e8419a6af81c255cb315a5035b6bbfb9
parent235acae98ad8893c3139038962cb916a847dd368 (diff)
Implement Server class to fast run wsgi server
-rw-r--r--restful_document/env.py27
-rw-r--r--restful_document/printf.py224
-rw-r--r--restful_document/server.py255
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()