From 36699d8265073403f17afb4294b4dba07f52e88b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 16 Apr 2011 17:08:35 -0400 Subject: [PATCH] Add --notify to `be serve`. --- NEWS | 1 + libbe/command/serve.py | 54 +++++++++++++++++++++++++++++++++++++++--- libbe/storage/http.py | 1 + libbe/util/subproc.py | 25 ++++++++++++++----- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index a93d47e..986e887 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,7 @@ April 16, 2011 * Added --preserve-uuids to `be import-xml`. * Added --assigned, --severity, and --status to `be new`. + * Added --notify to `be serve`. March 5, 2011 * Release version 1.0.1 (bugfixes). diff --git a/libbe/command/serve.py b/libbe/command/serve.py index ba4b0d8..b311139 100644 --- a/libbe/command/serve.py +++ b/libbe/command/serve.py @@ -58,6 +58,7 @@ import libbe import libbe.command import libbe.command.util import libbe.util.encoding +import libbe.util.subproc import libbe.version if libbe.TESTING == True: @@ -507,9 +508,10 @@ class ServerApp (WSGI_AppObject): """ server_version = "BE-server/" + libbe.version.version() - def __init__(self, storage, *args, **kwargs): - WSGI_AppObject.__init__(self, *args, **kwargs) + def __init__(self, storage, notify=False, **kwargs): + WSGI_AppObject.__init__(self, **kwargs) self.storage = storage + self.notify = notify self.http_user_error = 418 self.urls = [ @@ -570,6 +572,9 @@ class ServerApp (WSGI_AppObject): directory = self.data_get_boolean( data, 'directory', default=False, source=source) self.storage.add(id, parent=parent, directory=directory) + if self.notify: + self._notify(environ, 'add', id, + [('parent', parent), ('directory', directory)]) return self.ok_response(environ, start_response, None) def exists(self, environ, start_response): @@ -593,6 +598,8 @@ class ServerApp (WSGI_AppObject): self.storage.recursive_remove(id) else: self.storage.remove(id) + if self.notify: + self._notify(environ, 'remove', id, [('recursive', recursive)]) return self.ok_response(environ, start_response, None) def ancestors(self, environ, start_response): @@ -641,6 +648,8 @@ class ServerApp (WSGI_AppObject): raise _HandlerError(406, 'Missing query key value') value = data['value'] self.storage.set(id, value) + if self.notify: + self._notify(environ, 'set', id, [('value', value)]) return self.ok_response(environ, start_response, None) def commit(self, environ, start_response): @@ -661,6 +670,10 @@ class ServerApp (WSGI_AppObject): revision = self.storage.commit(summary, body, allow_empty) except libbe.storage.EmptyCommit, e: raise _HandlerError(self.http_user_error, 'EmptyCommit') + if self.notify: + self._notify(environ, 'commit', id, + [('allow_empty', allow_empty), ('summary', summary), + ('body', body)]) return self.ok_response(environ, start_response, revision) def revision_id(self, environ, start_response): @@ -700,6 +713,35 @@ class ServerApp (WSGI_AppObject): raise _Unauthorized() # only non-guests allowed to write # allow read-only commands for all users + def _notify(self, environ, command, id, params): + message = self._format_notification(environ, command, id, params) + self._submit_notification(message) + + def _format_notification(self, environ, command, id, params): + key_length = len('command') + for key,value in params: + if len(key) > key_length and '\n' not in str(value): + key_length = len(key) + key_length += 1 + lines = [] + multi_line_params = [] + for key,value in [('address', environ.get('REMOTE_ADDR', '-')), + ('command', command), ('id', id)]+params: + v = str(value) + if '\n' in v: + multi_line_params.append((key,v)) + continue + lines.append('%*.*s %s' % (key_length, key_length, key+':', v)) + lines.append('') + for key,value in multi_line_params: + lines.extend(['=== START %s ===' % key, v, + '=== STOP %s ===' % key, '']) + lines.append('') + return '\n'.join(lines) + + def _submit_notification(self, message): + libbe.util.subproc.invoke(self.notify, stdin=message, shell=True) + class Serve (libbe.command.Command): """:class:`~libbe.command.base.Command` wrapper around @@ -721,6 +763,10 @@ class Serve (libbe.command.Command): name='host', metavar='HOST', default='')), libbe.command.Option(name='read-only', short_name='r', help='Dissable operations that require writing'), + libbe.command.Option(name='notify', short_name='n', + help='Send notification emails for changes.', + arg=libbe.command.Argument( + name='notify', metavar='EMAIL-COMMAND', default=None)), libbe.command.Option(name='ssl', short_name='s', help='Use CherryPy to serve HTTPS (HTTP over SSL/TLS)'), libbe.command.Option(name='auth', short_name='a', @@ -742,7 +788,8 @@ class Serve (libbe.command.Command): self._check_restricted_access(storage, params['auth']) users = Users(params['auth']) users.load() - app = ServerApp(storage=storage, logger=self.logger) + app = ServerApp( + storage=storage, notify=params['notify'], logger=self.logger) if params['auth'] != None: app = AdminApp(app, users=users, logger=self.logger) app = AuthenticationApp(app, realm=storage.repo, @@ -860,6 +907,7 @@ if libbe.TESTING == True: self.logger.setLevel(logging.INFO) self.default_environ = { # required by PEP 333 'REQUEST_METHOD': 'GET', # 'POST', 'HEAD' + 'REMOTE_ADDR': '192.168.0.123', 'SCRIPT_NAME':'', 'PATH_INFO': '', #'QUERY_STRING':'', # may be empty or absent diff --git a/libbe/storage/http.py b/libbe/storage/http.py index fe5bbc8..ee589a2 100644 --- a/libbe/storage/http.py +++ b/libbe/storage/http.py @@ -358,6 +358,7 @@ if TESTING == True: # duplicated from libbe.command.serve.WSGITestCase self.default_environ = { 'REQUEST_METHOD': 'GET', # 'POST', 'HEAD' + 'REMOTE_ADDR': '192.168.0.123', 'SCRIPT_NAME':'', 'PATH_INFO': '', #'QUERY_STRING':'', # may be empty or absent diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py index 412ed36..be3bf31 100644 --- a/libbe/util/subproc.py +++ b/libbe/util/subproc.py @@ -21,6 +21,7 @@ Functions for running external commands in subprocesses. from subprocess import Popen, PIPE import sys +import types import libbe from encoding import get_encoding @@ -45,7 +46,8 @@ class CommandError(Exception): self.stderr = stderr def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), - cwd=None, unicode_output=True, verbose=False, encoding=None): + cwd=None, shell=None, unicode_output=True, verbose=False, + encoding=None): """ expect should be a tuple of allowed exit codes. cwd should be the directory from which the command will be executed. When @@ -54,18 +56,29 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), """ if cwd == None: cwd = '.' + if isinstance(shell, types.StringTypes): + list_args = ' '.split(args) # sloppy, but just for logging + str_args = args + else: + list_args = args + str_args = ' '.join(args) # sloppy, but just for logging if verbose == True: - print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args)) + print >> sys.stderr, '%s$ %s' % (cwd, str_args) try : if _POSIX: - q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd) + if shell is None: + shell = False + q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, + shell=shell, cwd=cwd) else: assert _MSWINDOWS==True, 'invalid platform' + if shell is None: + shell = True # win32 don't have os.execvp() so have to run command in a shell q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, - shell=True, cwd=cwd) + shell=shell, cwd=cwd) except OSError, e: - raise CommandError(args, status=e.args[0], stderr=e) + raise CommandError(list_args, status=e.args[0], stderr=e) stdout,stderr = q.communicate(input=stdin) status = q.wait() if unicode_output == True: @@ -78,7 +91,7 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), if verbose == True: print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr) if status not in expect: - raise CommandError(args, status, stdout, stderr) + raise CommandError(list_args, status, stdout, stderr) return status, stdout, stderr class Pipe (object): -- 2.26.2