Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar_network/toolkit/application.py
diff options
context:
space:
mode:
Diffstat (limited to 'sugar_network/toolkit/application.py')
-rw-r--r--sugar_network/toolkit/application.py397
1 files changed, 397 insertions, 0 deletions
diff --git a/sugar_network/toolkit/application.py b/sugar_network/toolkit/application.py
new file mode 100644
index 0000000..1694301
--- /dev/null
+++ b/sugar_network/toolkit/application.py
@@ -0,0 +1,397 @@
+# 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/>.
+
+"""Main process startup routines.
+
+$Repo: git://git.sugarlabs.org/alsroot/codelets.git$
+$File: src/application.py$
+$Date: 2012-10-22$
+
+"""
+
+import os
+import sys
+import time
+import signal
+import logging
+import textwrap
+from optparse import OptionParser
+from os.path import join, abspath, exists, basename
+from gettext import gettext as _
+
+from .options import Option
+from . import printf, util
+enforce = util.enforce
+
+
+debug = Option(
+ _('debug logging level; multiple argument'),
+ default=0, type_cast=int, short_option='-D', action='count',
+ name='debug')
+
+foreground = Option(
+ _('do not send the application into the background'),
+ default=False, type_cast=Option.bool_cast, short_option='-F',
+ action='store_true', name='foreground')
+
+replace = Option(
+ 'if application is already launched, replace it by new instance',
+ default=False, type_cast=Option.bool_cast,
+ action='store_true', name='replace')
+
+no_hints = Option(
+ _('suppress suggesting hints'),
+ default=False, short_option='-H', action='store_true',
+ name='no-hints')
+
+logdir = Option(
+ 'path to the directory to place log files',
+ name='logdir', default='/var/log')
+
+rundir = Option(
+ 'path to the directory to place pid files',
+ name='rundir')
+
+
+_LOGFILE_FORMAT = '%(asctime)s %(levelname)s %(name)s: %(message)s'
+
+
+def command(description='', name=None, args=None, hidden=False, **options):
+
+ def decorator(func):
+ func._is_command = True
+ func.name = name
+ func.description = description
+ func.args = args
+ func.options = options
+ func.hidden = hidden
+ return func
+
+ return decorator
+
+
+class Application(object):
+
+ def __init__(self, name, description=None, version=None, epilog=None,
+ where=None, **parse_args):
+ self._rundir = None
+ self.args = None
+ self.name = name
+
+ self._commands = {}
+ for name in dir(self.__class__):
+ attr = getattr(self.__class__, name)
+ if hasattr(attr, '_is_command') and \
+ (attr.name != 'config' or 'config_files' in parse_args):
+ self._commands[attr.name or name] = getattr(self, name)
+
+ parser = OptionParser(usage='%prog [OPTIONS]', description=description,
+ add_help_option=False)
+
+ if version:
+ parser.add_option('-V', '--version',
+ help=_('show version number and exit'),
+ action='version')
+ parser.print_version = lambda: sys.stdout.write('%s\n' % version)
+
+ parser.add_option('-h', '--help',
+ help=_('show this help message and exit'),
+ action='store_true')
+
+ options, self.args = Option.parse_args(parser, **parse_args)
+
+ def print_desc(term, desc):
+ text = []
+ for num, line in enumerate(desc.split('\n')):
+ if num == 0:
+ for i in line:
+ if i.isalpha() and not i.isupper():
+ break
+ else:
+ term += ' ' + line
+ continue
+ text.extend(textwrap.wrap(line, 54))
+ if len(term) < 24:
+ sys.stdout.write(' %-22s' % term)
+ else:
+ text.insert(0, '')
+ sys.stdout.write(' %s' % term)
+ print ('\n' + ' ' * 24).join(text)
+
+ def print_commands():
+ if not self._commands:
+ return
+ print ''
+ print _('Commands') + ':'
+ for name, attr in sorted(self._commands.items(),
+ lambda x, y: cmp(x[0], y[0])):
+ if attr.hidden:
+ continue
+ if attr.args:
+ name += ' ' + attr.args
+ print_desc(name, attr.description)
+
+ 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 description
+ print_commands()
+ if epilog:
+ print ''
+ print epilog
+ exit(0)
+
+ if options.help:
+ parser.print_help()
+ print_commands()
+ if where:
+ print ''
+ print _('Where') + ':'
+ for term in sorted(where):
+ print_desc(term, where[term])
+ if epilog:
+ print ''
+ print epilog
+ exit(0)
+
+ if not debug.value:
+ logging_level = logging.WARNING
+ elif debug.value == 1:
+ logging_level = logging.INFO
+ elif debug.value == 2:
+ logging_level = logging.DEBUG
+ elif debug.value > 2:
+ logging_level = 0
+ logging_format = _LOGFILE_FORMAT
+
+ root_logger = logging.getLogger('')
+ for i in root_logger.handlers:
+ root_logger.removeHandler(i)
+
+ logging.basicConfig(level=logging_level, format=logging_format)
+
+ def epilog(self):
+ pass
+
+ def start(self):
+ self._rundir = abspath(rundir.value or '/var/run/' + self.name)
+
+ cmd_name = self.args.pop(0)
+ try:
+ cmd = self._commands.get(cmd_name)
+ enforce(cmd is not None, 'Unknown command "%s"' % cmd_name)
+
+ if Option.config_files:
+ logging.info('Load configuration from %s file(s)',
+ ', '.join(Option.config_files))
+
+ if cmd.options.get('keep_stdout') and not foreground.value:
+ self._keep_stdout()
+
+ exit(cmd() or 0)
+ except Exception:
+ printf.exception('%s %s', _('Aborted'), self.name)
+ exit(1)
+ finally:
+ self.epilog()
+ if not no_hints.value:
+ printf.flush_hints()
+
+ def check_for_instance(self):
+ pid = None
+ pidfile = join(self._rundir, '%s.pid' % self.name)
+ if exists(pidfile):
+ try:
+ pid = int(file(pidfile).read().strip())
+ os.getpgid(pid)
+ if basename(sys.argv[0]) not in _get_cmdline(pid):
+ # In case if pidfile was not removed after reboot
+ # and another process launched with pidfile's pid
+ pid = None
+ except (ValueError, OSError):
+ pid = None
+ return pid
+
+ def ensure_pidfile_path(self):
+ if not exists(self._rundir):
+ os.makedirs(self._rundir)
+ enforce(os.access(self._rundir, os.W_OK),
+ 'No write access to %r to store pidfile', self._rundir)
+
+ def new_instance(self):
+ self.ensure_pidfile_path()
+ pidfile_path = join(self._rundir, '%s.pid' % self.name)
+ with file(pidfile_path, 'w') as f:
+ f.write(str(os.getpid()))
+ return pidfile_path
+
+ @command('output current configuration', name='config')
+ def _cmd_config(self):
+ if self.args:
+ opt = self.args.pop(0)
+ enforce(opt in Option.items, 'Unknown option "%s"', opt)
+ exit(0 if bool(Option.items[opt].value) else 1)
+ else:
+ print '\n'.join(Option.export())
+
+ def _keep_stdout(self):
+ log_dir = abspath(logdir.value)
+ if not exists(log_dir):
+ os.makedirs(log_dir)
+ enforce(os.access(log_dir, os.W_OK), 'No write access to %s', log_dir)
+
+ # printf should still output to original streams
+ printf.stdout = os.fdopen(os.dup(sys.stdout.fileno()), 'w')
+ printf.stderr = os.fdopen(os.dup(sys.stderr.fileno()), 'w')
+
+ log_path = join(log_dir, '%s.log' % self.name)
+ logfile = file(log_path, 'a+')
+ os.dup2(logfile.fileno(), sys.stdout.fileno())
+ os.dup2(logfile.fileno(), sys.stderr.fileno())
+ logfile.close()
+
+
+class Daemon(Application):
+
+ _accept_pipe = None
+
+ def run(self):
+ raise NotImplementedError()
+
+ def shutdown(self):
+ pass
+
+ @command('start in daemon mode', name='start', keep_stdout=True)
+ def cmd_start(self):
+ while True:
+ pid = self.check_for_instance()
+ if not pid:
+ break
+ if not replace.value:
+ printf.info('%s is already run with pid %s', self.name, pid)
+ return 1
+ try:
+ printf.info('Kill previous %r instance', pid)
+ os.kill(pid, signal.SIGTERM)
+ except Exception:
+ pass
+ time.sleep(.5)
+
+ if foreground.value:
+ self._launch()
+ else:
+ self.ensure_pidfile_path()
+ self._daemonize()
+
+ return 0
+
+ @command('stop daemon', name='stop')
+ def cmd_stop(self):
+ pid = self.check_for_instance()
+ if pid:
+ os.kill(pid, signal.SIGTERM)
+ return 0
+ else:
+ printf.info('%s is not run', self.name)
+ return 1
+
+ @command('check for launched daemon', name='status')
+ 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
+
+ @command('reopen log files in daemon mode', name='reload')
+ def cmd_reload(self):
+ pid = self.check_for_instance()
+ if not pid:
+ printf.info('%s is not run', self.name)
+ return 1
+ os.kill(pid, signal.SIGHUP)
+ logging.info('Reload %s process', self.name)
+
+ def accept(self):
+ if self._accept_pipe is not None:
+ os.close(self._accept_pipe)
+ self._accept_pipe = None
+
+ def _launch(self):
+ logging.info('Start %s', self.name)
+
+ def sigterm_cb(signum, frame):
+ logging.info('Got signal %s to stop %s', signum, self.name)
+ self.shutdown()
+
+ def sighup_cb(signum, frame):
+ logging.info('Reload %s on SIGHUP signal', self.name)
+ self._keep_stdout()
+
+ signal.signal(signal.SIGINT, sigterm_cb)
+ signal.signal(signal.SIGTERM, sigterm_cb)
+ signal.signal(signal.SIGHUP, sighup_cb)
+
+ pid_path = self.new_instance()
+ try:
+ self.run()
+ finally:
+ self.epilog()
+ os.unlink(pid_path)
+
+ def _daemonize(self):
+ accept_pipe = os.pipe()
+ if os.fork() > 0:
+ os.close(accept_pipe[1])
+ os.read(accept_pipe[0], 1)
+ # Exit parent of the first child
+ return
+
+ os.close(accept_pipe[0])
+ self._accept_pipe = accept_pipe[1]
+
+ # 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())
+
+ try:
+ self._launch()
+ except Exception:
+ logging.exception('Aborted %s', self.name)
+ status = 1
+ else:
+ logging.info('Stopped %s', self.name)
+ status = 0
+
+ exit(status)
+
+
+def _get_cmdline(pid):
+ with file('/proc/%s/cmdline' % pid) as f:
+ return f.read()