You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by gs...@apache.org on 2012/02/28 19:52:13 UTC
svn commit: r1294778 - in /subversion/trunk/tools/server-side/svnpubsub: ./
rc.d/ svnpubsub/
Author: gstein
Date: Tue Feb 28 18:52:13 2012
New Revision: 1294778
URL: http://svn.apache.org/viewvc?rev=1294778&view=rev
Log:
Add the SvnPubSub framework.
This is copied from r806478 of:
https://svn.apache.org/repos/infra/infrastructure/trunk/projects/svnpubsub
History has not been carried over. See the original source for history.
Added:
subversion/trunk/tools/server-side/svnpubsub/
subversion/trunk/tools/server-side/svnpubsub/README.txt
subversion/trunk/tools/server-side/svnpubsub/commit-hook.py (with props)
subversion/trunk/tools/server-side/svnpubsub/example.conf
subversion/trunk/tools/server-side/svnpubsub/rc.d/
subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub (with props)
subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian (with props)
subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris (with props)
subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub (with props)
subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian (with props)
subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris (with props)
subversion/trunk/tools/server-side/svnpubsub/svnpubsub/
subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac
subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py
subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py
subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py
subversion/trunk/tools/server-side/svnpubsub/svntweet.py
subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py (with props)
subversion/trunk/tools/server-side/svnpubsub/test.conf
subversion/trunk/tools/server-side/svnpubsub/testserver.py (with props)
subversion/trunk/tools/server-side/svnpubsub/watcher.py (with props)
Added: subversion/trunk/tools/server-side/svnpubsub/README.txt
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/README.txt?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/README.txt (added)
+++ subversion/trunk/tools/server-side/svnpubsub/README.txt Tue Feb 28 18:52:13 2012
@@ -0,0 +1 @@
+### write a README
Added: subversion/trunk/tools/server-side/svnpubsub/commit-hook.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/commit-hook.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/commit-hook.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/commit-hook.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,89 @@
+#!/usr/local/bin/python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+SVNLOOK="/usr/local/svn-install/current/bin/svnlook"
+#SVNLOOK="/usr/local/bin/svnlook"
+
+import sys
+import subprocess
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import urllib2
+
+HOST="127.0.0.1"
+PORT=2069
+
+def svncmd(cmd):
+ return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+
+def svncmd_uuid(repo):
+ cmd = "%s uuid %s" % (SVNLOOK, repo)
+ p = svncmd(cmd)
+ return p.stdout.read().strip()
+
+def svncmd_info(repo, revision):
+ cmd = "%s info -r %s %s" % (SVNLOOK, revision, repo)
+ p = svncmd(cmd)
+ data = p.stdout.read().strip().split("\n")
+ #print data
+ return {'author': data[0],
+ 'date': data[1],
+ 'log': "".join(data[3:])}
+
+def svncmd_dirs(repo, revision):
+ cmd = "%s dirs-changed -r %s %s" % (SVNLOOK, revision, repo)
+ p = svncmd(cmd)
+ dirs = []
+ while True:
+ line = p.stdout.readline()
+ if not line:
+ break
+ dirs.append(line.strip())
+ return dirs
+
+def do_put(body):
+ opener = urllib2.build_opener(urllib2.HTTPHandler)
+ request = urllib2.Request("http://%s:%d/dirs-changed" %(HOST, PORT), data=body)
+ request.add_header('Content-Type', 'application/json')
+ request.get_method = lambda: 'PUT'
+ url = opener.open(request)
+
+
+def main(repo, revision):
+ i = svncmd_info(repo, revision)
+ data = {'revision': int(revision),
+ 'dirs_changed': [],
+ 'repos': svncmd_uuid(repo),
+ 'author': i['author'],
+ 'log': i['log'],
+ 'date': i['date'],
+ }
+ data['dirs_changed'].extend(svncmd_dirs(repo, revision))
+ body = json.dumps(data)
+ #print body
+ do_put(body)
+
+if __name__ == "__main__":
+ if len(sys.argv) != 3:
+ print "invalid args"
+ sys.exit(0)
+
+ main(sys.argv[1], sys.argv[2])
Propchange: subversion/trunk/tools/server-side/svnpubsub/commit-hook.py
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/example.conf
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/example.conf?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/example.conf (added)
+++ subversion/trunk/tools/server-side/svnpubsub/example.conf Tue Feb 28 18:52:13 2012
@@ -0,0 +1,116 @@
+### turn this into an example
+
+[DEFAULT]
+svnbin: /usr/local/bin/svn
+streams: http://svn-master.apache.org:2069/commits/xml
+ http://dist.apache.org:2069/commits/xml
+
+ASF: http://svn-master.apache.org/repos/asf
+INFRA: https://svn-master.apache.org/repos/infra
+CMS: %(INFRA)s/websites/production
+DIST: https://dist.apache.org/repos/dist
+
+[env]
+HOME: /home/svnwc
+LANG: en_US.UTF-8
+
+[track]
+/x1/www/apr.apache.org: %(ASF)s/apr/site/trunk/docs
+/x1/www/apr.apache.org/dev/dist: %(DIST)s/dev/apr
+/x1/www/www.apache.org/dist/apr: %(DIST)s/release/apr
+/x1/www/www.apache.org/dist/poi: %(DIST)s/release/poi
+/x1/www/www.apache.org/dist/pivot: %(DIST)s/release/pivot
+/x1/www/www.apache.org/dist/httpd: %(DIST)s/release/httpd
+/x1/www/www.apache.org/dist/incubator/airavata: %(DIST)s/release/incubator/airavata
+/x1/www/www.apache.org/: %(CMS)s/www
+/x1/www/httpd.apache.org/: %(ASF)s/httpd/site/trunk/docs
+/x1/www/httpd.apache.org/docs/2.0: %(ASF)s/httpd/httpd/branches/2.0.x/docs/manual
+/x1/www/httpd.apache.org/docs/2.2: %(ASF)s/httpd/httpd/branches/2.2.x/docs/manual
+/x1/www/httpd.apache.org/docs/1.3: %(ASF)s/httpd/httpd/branches/1.3.x/htdocs/manual
+/x1/www/httpd.apache.org/docs/2.4: %(ASF)s/httpd/httpd/branches/2.4.x/docs/manual
+/x1/www/httpd.apache.org/docs/trunk: %(ASF)s/httpd/httpd/trunk/docs/manual
+/x1/www/httpd.apache.org/mod_fcgid/mod: %(ASF)s/httpd/mod_fcgid/trunk/docs/manual/mod
+/x1/www/httpd.apache.org/mod_fcgid/style: %(ASF)s/httpd/httpd/trunk/docs/manual/style
+/x1/www/httpd.apache.org/mod_fcgid/images: %(ASF)s/httpd/httpd/trunk/docs/manual/images
+/x1/www/httpd.apache.org/dev/dist: %(DIST)s/dev/httpd
+/x1/www/httpd.apache.org/mod_ftp/mod: %(ASF)s/httpd/mod_ftp/trunk/docs/manual/mod
+/x1/www/httpd.apache.org/mod_ftp/style: %(ASF)s/httpd/httpd/trunk/docs/manual/style
+/x1/www/httpd.apache.org/mod_ftp/images: %(ASF)s/httpd/httpd/trunk/docs/manual/images
+/x1/www/httpd.apache.org/mod_ftp/ftp: %(ASF)s/httpd/mod_ftp/trunk/docs/manual/ftp
+/x1/www/libcloud.apache.org: %(CMS)s/libcloud
+/x1/www/river.apache.org: %(CMS)s/river/content
+/x1/www/incubator.apache.org/stanbol: %(CMS)s/stanbol/content/stanbol
+/x1/www/incubator.apache.org/kitty: %(CMS)s/kitty/content/kitty
+/x1/www/www.apache.org/dist/trafficserver: %(DIST)s/release/trafficserver
+/x1/staging/harmony.apache.org/eclipse/update: %(ASF)s/harmony/enhanced/tools/trunk/eclipse/org.apache.harmony.eclipse.site
+/x1/staging/harmony.apache.org/externals/security: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/security
+/x1/staging/harmony.apache.org/externals/regex: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/regex
+/x1/staging/harmony.apache.org: %(ASF)s/harmony/standard/site/trunk/docs
+/x1/www/harmony.apache.org/eclipse/update: %(ASF)s/harmony/enhanced/tools/trunk/eclipse/org.apache.harmony.eclipse.site
+/x1/www/harmony.apache.org/externals/security: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/security
+/x1/www/harmony.apache.org/externals/regex: %(ASF)s/harmony/enhanced/java/trunk/classlib/doc/regex
+/x1/www/harmony.apache.org: %(ASF)s/harmony/standard/site/branches/live/docs
+/x1/www/subversion.apache.org/: %(ASF)s/subversion/site
+/x1/www/trafficserver.apache.org/: %(CMS)s/trafficserver
+/x1/www/qpid.apache.org: %(ASF)s/qpid/site/docs
+/x1/www/pdfbox.apache.org: %(ASF)s/pdfbox/site/publish
+/x1/www/cassandra.apache.org: %(ASF)s/cassandra/site/publish
+/x1/www/community.apache.org: %(CMS)s/community
+/x1/www/nutch.apache.org: %(ASF)s/nutch/site/publish
+/x1/www/wicket.apache.org: %(ASF)s/wicket/common/site/trunk/_site
+/x1/www/wicket.apache.org/apidocs: %(ASF)s/wicket/common/site/apidocs
+/x1/www/incubator.apache.org/callback: %(CMS)s/callback/content/callback
+/x1/www/incubator.apache.org/zetacomponents: %(ASF)s/incubator/zetacomponents/website/htdocs
+/x1/www/incubator.apache.org/flex: %(CMS)s/flex/content/flex
+/x1/www/incubator.apache.org/jena: %(CMS)s/jena/content/jena
+/x1/www/incubator.apache.org/celix: %(CMS)s/celix/content/celix
+/x1/www/incubator.apache.org/lucene.net: %(CMS)s/lucene.net/content/lucene.net
+/x1/www/incubator.apache.org/easyant: %(ASF)s/incubator/easyant/site/production
+/x1/www/incubator.apache.org/etch: %(CMS)s/etch/content/etch
+/x1/www/incubator.apache.org/rave: %(CMS)s/rave/content/rave
+/x1/www/incubator.apache.org/wave: %(CMS)s/wave/content/wave
+/x1/www/incubator.apache.org/lucy: %(CMS)s/lucy/content/lucy
+/x1/www/incubator.apache.org/openmeetings: %(ASF)s/incubator/openmeetings/trunk/singlewebapp/docs
+/x1/www/incubator.apache.org/openofficeorg: %(CMS)s/openofficeorg/content/openofficeorg
+/x1/www/incubator.apache.org/odftoolkit: %(CMS)s/odftoolkit/content/odftoolkit
+/x1/www/incubator.apache.org/airavata: %(CMS)s/airavata/content/airavata
+/x1/www/incubator.apache.org/wookie: %(CMS)s/wookie/content/wookie
+/x1/www/incubator.apache.org/accumulo: %(CMS)s/accumulo/content/accumulo
+/x1/www/gora.apache.org: %(ASF)s/gora/site/publish
+/x1/www/incubator.apache.org/devicemap: %(CMS)s/devicemap/content/devicemap
+/x1/www/aries.apache.org: %(CMS)s/aries/content
+/x1/www/tika.apache.org: %(ASF)s/tika/site/publish
+/x1/www/uima.apache.org/pubsub: %(ASF)s/uima/site/trunk/uima-website/docs
+/x1/www/zookeeper.apache.org: %(CMS)s/zookeeper
+/x1/www/chemistry.apache.org: %(CMS)s/chemistry
+/x1/www/ant.apache.org: %(ASF)s/ant/site/ant/production
+/x1/www/ant.apache.org/ivy: %(ASF)s/ant/site/ivy/production
+/x1/www/ant.apache.org/ivy/ivyde: %(ASF)s/ant/site/ivyde/production
+/x1/www/www.apache.org/dist/esme: %(DIST)s/release/esme
+/x1/www/www.apache.org/dist/libcloud: %(DIST)s/release/libcloud
+/x1/www/archive.apachecon.com: %(INFRA)s/apachecon/archive.apachecon.com
+/x1/www/oodt.apache.org: %(ASF)s/oodt/site
+/x1/www/esme.apache.org: %(CMS)s/esme/content
+/x1/www/ooo-site.apache.org: %(CMS)s/ooo-site
+/x1/www/openejb.apache.org: %(CMS)s/openejb
+/x1/www/deltacloud.apache.org: %(ASF)s/deltacloud/trunk/site/output
+/x1/www/ace.apache.org: %(CMS)s/ace
+/x1/www/stdcxx.apache.org/doc: %(ASF)s/stdcxx/trunk/doc
+/x1/www/stdcxx.apache.org: %(ASF)s/stdcxx/site
+/x1/www/www.apache.org/dist/tomcat: %(DIST)s/release/tomcat
+/x1/www/incubator.apache.org/any23: %(ASF)s/incubator/any23/site
+/x1/www/incubator.apache.org/bloodhound: %(ASF)s/incubator/bloodhound/site
+/x1/www/labs.apache.org/: %(CMS)s/labs
+/x1/www/lucene.apache.org: %(CMS)s/lucene
+/x1/www/lucene.apache.org/content/core/old_versioned_docs: %(ASF)s/lucene/old_versioned_docs
+/x1/www/pivot.apache.org: %(ASF)s/pivot/site/trunk/deploy
+/x1/www/www.apache.org/dist/empire-db: %(DIST)s/release/empire-db
+/x1/www/empire-db.apache.org: %(ASF)s/empire-db/site
+/x1/www/www.apache.org/dist/subversion: %(DIST)s/release/subversion
+/x1/www/avro.apache.org: %(ASF)s/avro/site/publish
+/x1/www/incubator.apache.org/cordova: %(ASF)s/incubator/cordova/site/public
+/x1/www/mahout.apache.org: %(ASF)s/mahout/site/new_website
+/x1/www/opennlp.apache.org: %(CMS)s/opennlp
+/x1/www/gump.apache.org: %(ASF)s/gump/site
+/x1/www/incubator.apache.org/syncope: %(ASF)s/incubator/syncope/site
+/x1/staging/www.apache.org: %(ASF)s/infrastructure/site/branches/flamebait/docs
Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub Tue Feb 28 18:52:13 2012
@@ -0,0 +1,35 @@
+#!/bin/sh
+#
+# PROVIDE: svnpubsub
+# REQUIRE: DAEMON
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="svnpubsub"
+rcvar=`set_rcvar`
+
+load_rc_config $name
+
+#
+# DO NOT CHANGE THESE DEFAULT VALUES HERE
+# SET THEM IN THE /etc/rc.conf FILE
+#
+svnpubsub_enable=${svnpubsub_enable-"NO"}
+svnpubsub_user=${svnpubsub_user-"svn"}
+svnpubsub_group=${svnpubsub_group-"svn"}
+svnpubsub_reactor=${svnpubsub_reactor-"poll"}
+svnpubsub_pidfile=${svnpubsub_pidfile-"/var/run/svnpubsub/svnpubsub.pid"}
+pidfile="${svnpubsub_pidfile}"
+
+export PYTHON_EGG_CACHE="/home/svn/.python-eggs"
+
+command="/usr/local/bin/twistd"
+command_args="-y /usr/local/svnpubsub/svnpubsub.tac \
+ --logfile=/var/log/vc/svnpubsub.log \
+ --pidfile=${pidfile} \
+ --uid=${svnpubsub_user} --gid=${svnpubsub_user} \
+ -r${svnpubsub_reactor}"
+
+
+run_rc_command "$1"
Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian Tue Feb 28 18:52:13 2012
@@ -0,0 +1,62 @@
+#!/bin/bash
+### BEGIN INIT INFO
+# Provides: svnpubsub
+# Required-Start: $remote_fs
+# Required-Stop: $remote_fs
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: SvnPubSub
+# Description: start SvnPubSub daemon
+#### END INIT INFO
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+svnpubsub_user=${svnpubsub_user-"daemon"}
+svnpubsub_group=${svnpubsub_group-"daemon"}
+svnpubsub_reactor=${svnpubsub_reactor-"poll"}
+svnpubsub_pidfile=${svnpubsub_pidfile-"/var/run/svnpubsub.pid"}
+pidfile="${svnpubsub_pidfile}"
+
+TWSITD_CMD="/usr/bin/twistd -y /opt/svnpubsub/svnpubsub.tac \
+ --logfile=/var/bwlog/svnpubsub/svnpubsub.log \
+ --pidfile=${pidfile} \
+ --uid=${svnpubsub_user} --gid=${svnpubsub_user} \
+ -r${svnpubsub_reactor}"
+
+RETVAL=0
+
+start() {
+ echo "Starting SvnPubSub Server: "
+ $TWSITD_CMD
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+stop() {
+ echo "Stopping SvnPubSub Server: "
+ THE_PID=`cat ${pidfile}`
+ kill $THE_PID
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ restart)
+ stop
+ start
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $RETVAL
Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.debian
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris Tue Feb 28 18:52:13 2012
@@ -0,0 +1,53 @@
+#!/usr/bin/bash
+#
+# a dumb init script for twistd on solaris. cus like, writing XML for SMF is f'ing lame.
+#
+
+svnpubsub_user=${svnpubsub_user-"daemon"}
+svnpubsub_group=${svnpubsub_group-"daemon"}
+svnpubsub_reactor=${svnpubsub_reactor-"poll"}
+svnpubsub_pidfile=${svnpubsub_pidfile-"/var/run/svnpubsub/svnpubsub.pid"}
+pidfile="${svnpubsub_pidfile}"
+
+TWSITD_CMD="/opt/local/bin//twistd -y /usr/local/svnpubsub/svnpubsub.tac \
+ --logfile=/x1/log/svnpubsub.log \
+ --pidfile=${pidfile} \
+ --uid=${svnpubsub_user} --gid=${svnpubsub_user} \
+ -r${svnpubsub_reactor}"
+
+RETVAL=0
+
+start() {
+ echo "Starting SvnPubSub Server: "
+ $TWSITD_CMD
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+stop() {
+ echo "Stopping SvnPubSub Server: "
+ THE_PID=`cat ${pidfile}`
+ kill $THE_PID
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ restart)
+ stop
+ start
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $RETVAL
Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnpubsub.solaris
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub Tue Feb 28 18:52:13 2012
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# PROVIDE: svnwcsub
+# REQUIRE: DAEMON
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="svnwcsub"
+rcvar=`set_rcvar`
+
+load_rc_config $name
+
+#
+# DO NOT CHANGE THESE DEFAULT VALUES HERE
+# SET THEM IN THE /etc/rc.conf FILE
+#
+svnwcsub_enable=${svnwcsub_enable-"NO"}
+svnwcsub_user=${svnwcsub_user-"svnwc"}
+svnwcsub_group=${svnwcsub_group-"svnwc"}
+svnwcsub_reactor=${svnwcsub_reactor-"poll"}
+svnwcsub_pidfile=${svnwcsub_pidfile-"/var/run/svnwcsub/svnwcsub.pub"}
+svnwcsub_program=${svnwcsub_program-"/usr/local/bin/twistd"}
+svnwcsub_env="PYTHON_EGG_CACHE"
+svnwcsub_cmd_int=${svnwcsub_cmd_int-"python"}
+pidfile="${svnwcsub_pidfile}"
+
+export PYTHON_EGG_CACHE="/var/run/svnwcsub"
+
+command="/usr/local/bin/twistd"
+command_interpreter="/usr/local/bin/${svnwcsub_cmd_int}"
+command_args="-y /usr/local/svnpubsub/svnwcsub.tac \
+ --logfile=/var/log/svnwcsub.log \
+ --pidfile=${pidfile} \
+ --uid=${svnwcsub_user} --gid=${svnwcsub_group} \
+ --umask=002 -r${svnwcsub_reactor}"
+
+run_rc_command "$1"
+
Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian Tue Feb 28 18:52:13 2012
@@ -0,0 +1,62 @@
+#!/bin/bash
+### BEGIN INIT INFO
+# Provides: svnwcsub
+# Required-Start: $remote_fs
+# Required-Stop: $remote_fs
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: SvnWcSub
+# Description: start SvnWcSub daemon
+#### END INIT INFO
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+svnwcsub_user=${svnwcsub_user-"svnwc"}
+svnwcsub_group=${svnwcsub_group-"svnwc"}
+svnwcsub_reactor=${svnwcsub_reactor-"poll"}
+svnwcsub_pidfile=${svnwcsub_pidfile-"/var/run/svnwcsub.pid"}
+pidfile="${svnwcsub_pidfile}"
+
+TWSITD_CMD="/usr/bin/twistd -y /opt/svnpubsub/svnwcsub.tac \
+ --logfile=/var/bwlog/svnpubsub/svnwcsub.log \
+ --pidfile=${pidfile} \
+ --uid=${svnwcsub_user} --gid=${svnwcsub_group} \
+ -r${svnwcsub_reactor}"
+
+RETVAL=0
+
+start() {
+ echo "Starting SvnWcSub Server: "
+ $TWSITD_CMD
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+stop() {
+ echo "Stopping SvnWcSub Server: "
+ THE_PID=`cat ${pidfile}`
+ kill $THE_PID
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ restart)
+ stop
+ start
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $RETVAL
Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.debian
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris (added)
+++ subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris Tue Feb 28 18:52:13 2012
@@ -0,0 +1,54 @@
+#!/usr/bin/bash
+#
+# a dumb init script for twistd on solaris. cus like, writing XML for SMF is f'ing lame.
+#
+
+svnwcsub_user=${svnwcsub_user-"svnwc"}
+svnwcsub_group=${svnwcsub_group-"other"}
+svnwcsub_reactor=${svnwcsub_reactor-"poll"}
+svnwcsub_pidfile=${svnwcsub_pidfile-"/var/run/svnwcsub/svnwcsub.pid"}
+pidfile="${svnwcsub_pidfile}"
+
+TWSITD_CMD="/opt/python/2.6.2/bin/twistd -y /usr/local/svnpubsub/svnwcsub.tac \
+ --logfile=/x1/log/svnwcsub.log \
+ --pidfile=${pidfile} \
+ --umask=002 \
+ --uid=${svnwcsub_user} --gid=${svnwcsub_group} \
+ -r${svnwcsub_reactor}"
+
+RETVAL=0
+
+start() {
+ echo "Starting SvnWcSub Server: "
+ $TWSITD_CMD
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+stop() {
+ echo "Stopping SvnWcSub Server: "
+ THE_PID=`cat ${pidfile}`
+ kill $THE_PID
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && echo "ok" || echo "failed"
+ return $RETVAL
+}
+
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ restart)
+ stop
+ start
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|restart}"
+ exit 1
+esac
+
+exit $RETVAL
Propchange: subversion/trunk/tools/server-side/svnpubsub/rc.d/svnwcsub.solaris
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub.tac Tue Feb 28 18:52:13 2012
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import sys
+import os
+from twisted.application import service, internet
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from svnpubsub.server import svnpubsub_server
+
+application = service.Application("SvnPubSub")
+
+def get_service():
+ return internet.TCPServer(2069, svnpubsub_server())
+
+service = get_service()
+service.setServiceParent(application)
Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub/__init__.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1 @@
+# Turn svnpubsub/ into a package.
Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub/client.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,232 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Generic client for SvnPubSub
+#
+# ### usage...
+#
+#
+# EVENTS
+#
+# connected: a connection to the server has been opened (though not
+# necessarily established)
+# closed: the connection was closed. reconnect will be attempted.
+# error: an error closed the connection. reconnect will be attempted.
+# ping: the server has sent a keepalive
+# stale: no activity has been seen, so the connection will be closed
+# and reopened
+#
+
+import asyncore
+import asynchat
+import socket
+import functools
+import time
+import xml.sax
+
+# How long the polling loop should wait for activity before returning.
+TIMEOUT = 30.0
+
+# Always delay a bit when trying to reconnect. This is not precise, but sets
+# a minimum amount of delay. At the moment, there is no further backoff.
+RECONNECT_DELAY = 25.0
+
+# If we don't see anything from the server for this amount time, then we
+# will drop and reconnect. The TCP connection may have gone down without
+# us noticing it somehow.
+STALE_DELAY = 60.0
+
+
+class Client(asynchat.async_chat):
+
+ def __init__(self, host, port, commit_callback, event_callback):
+ asynchat.async_chat.__init__(self)
+
+ self.last_activity = time.time()
+
+ self.host = host
+ self.port = port
+ self.event_callback = event_callback
+
+ handler = XMLStreamHandler(commit_callback, event_callback)
+
+ self.parser = xml.sax.make_parser(['xml.sax.expatreader'])
+ self.parser.setContentHandler(handler)
+
+ # Wait for the end of headers. Then we start parsing XML.
+ self.set_terminator('\r\n\r\n')
+ self.skipping_headers = True
+
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.connect((host, port))
+ ### should we allow for repository restrictions?
+ self.push('GET /commits/xml HTTP/1.0\r\n\r\n')
+
+ def handle_connect(self):
+ self.event_callback('connected')
+
+ def handle_close(self):
+ self.event_callback('closed')
+ self.close()
+
+ def handle_error(self):
+ self.event_callback('error')
+ self.close()
+
+ def found_terminator(self):
+ self.skipping_headers = False
+
+ # From here on, collect everything. Never look for a terminator.
+ self.set_terminator(None)
+
+ def collect_incoming_data(self, data):
+ # Remember the last time we saw activity
+ self.last_activity = time.time()
+
+ if not self.skipping_headers:
+ # Just shove this into the XML parser. As the elements are processed,
+ # we'll collect them into an appropriate structure, and then invoke
+ # the callback when we have fully received a commit.
+ self.parser.feed(data)
+
+
+class XMLStreamHandler(xml.sax.handler.ContentHandler):
+
+ def __init__(self, commit_callback, event_callback):
+ self.commit_callback = commit_callback
+ self.event_callback = event_callback
+
+ self.rev = None
+ self.chars = ''
+
+ def startElement(self, name, attrs):
+ if name == 'commit':
+ self.rev = Revision(attrs['repository'], attrs['revision'])
+ # No other elements to worry about.
+
+ def characters(self, data):
+ self.chars += data
+
+ def endElement(self, name):
+ if name == 'commit':
+ self.commit_callback(self.rev)
+ self.rev = None
+ elif name == 'stillalive':
+ self.event_callback('ping')
+ elif self.chars and self.rev:
+ value = self.chars.strip()
+ if name == 'path':
+ self.rev.dirs_changed.append(value)
+ elif name == 'author':
+ self.rev.author = value
+ elif name == 'date':
+ self.rev.date = value
+ elif name == 'log':
+ self.rev.log = value
+
+ # Toss out any accumulated characters for this element.
+ self.chars = ''
+
+
+class Revision(object):
+ def __init__(self, repos, rev):
+ self.repos = repos
+ self.rev = rev
+ self.dirs_changed = [ ]
+ self.author = None
+ self.date = None
+ self.log = None
+
+
+class MultiClient(object):
+ def __init__(self, hostports, commit_callback, event_callback):
+ self.commit_callback = commit_callback
+ self.event_callback = event_callback
+
+ # No target time, as no work to do
+ self.target_time = 0
+ self.work_items = [ ]
+
+ for host, port in hostports:
+ self._add_channel(host, port)
+
+ def _reconnect(self, host, port, event_name):
+ if event_name == 'closed' or event_name == 'error':
+ # Stupid connection closed for some reason. Set up a reconnect. Note
+ # that it should have been removed from asyncore.socket_map already.
+ self._reconnect_later(host, port)
+
+ # Call the user's callback now.
+ self.event_callback(host, port, event_name)
+
+ def _reconnect_later(self, host, port):
+ # Set up a work item to reconnect in a little while.
+ self.work_items.append((host, port))
+
+ # Only set a target if one has not been set yet. Otherwise, we could
+ # create a race condition of continually moving out towards the future
+ if not self.target_time:
+ self.target_time = time.time() + RECONNECT_DELAY
+
+ def _add_channel(self, host, port):
+ # Simply instantiating the client will install it into the global map
+ # for processing in the main event loop.
+ Client(host, port,
+ functools.partial(self.commit_callback, host, port),
+ functools.partial(self._reconnect, host, port))
+
+ def _check_stale(self):
+ now = time.time()
+ for client in asyncore.socket_map.values():
+ if client.last_activity + STALE_DELAY < now:
+ # Whoops. No activity in a while. Signal this fact, Close the
+ # Client, then have it reconnected later on.
+ self.event_callback(client.host, client.port, 'stale')
+
+ # This should remove it from .socket_map.
+ client.close()
+
+ self._reconnect_later(client.host, client.port)
+
+ def _maybe_work(self):
+ # If we haven't reach the targetted time, or have no work to do,
+ # then fast-path exit
+ if time.time() < self.target_time or not self.work_items:
+ return
+
+ # We'll take care of all the work items, so no target for future work
+ self.target_time = 0
+
+ # Play a little dance just in case work gets added while we're
+ # currently working on stuff
+ work = self.work_items
+ self.work_items = [ ]
+
+ for host, port in work:
+ self._add_channel(host, port)
+
+ def run_forever(self):
+ while True:
+ if asyncore.socket_map:
+ asyncore.loop(timeout=TIMEOUT, count=1)
+ else:
+ time.sleep(TIMEOUT)
+
+ self._check_stale()
+ self._maybe_work()
Added: subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnpubsub/server.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,271 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SvnPubSub - Simple Push Notification of Subversion commits
+#
+# Based on the theory behind the Live Journal Atom Streaming Service:
+# <http://atom.services.livejournal.com/>
+#
+# Instead of using a complicated XMPP/AMPQ/JMS/super messaging service,
+# we have simple HTTP GETs and PUTs to get data in and out.
+#
+# Currently supports both XML and JSON serialization.
+#
+# Example Sub clients:
+# curl -i http://127.0.0.1:2069/dirs-changed/xml
+# curl -i http://127.0.0.1:2069/dirs-changed/json
+# curl -i http://127.0.0.1:2069/commits/json
+# curl -i http://127.0.0.1:2069/commits/13f79535-47bb-0310-9956-ffa450edef68/json
+# curl -i http://127.0.0.1:2069/dirs-changed/13f79535-47bb-0310-9956-ffa450edef68/json
+#
+# URL is built into 3 parts:
+# /${type}/${optional_repo_uuid}/${format}
+#
+# If the repository UUID is included in the URl, you will only recieve
+# messages about that repository.
+#
+# Example Pub clients:
+# curl -T revinfo.json -i http://127.0.0.1:2069/commit
+#
+# TODO:
+# - Add Real access controls (not just 127.0.0.1)
+# - Document PUT format
+# - Convert to twisted.python.log
+
+
+
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import sys
+
+import twisted
+from twisted.internet import reactor
+from twisted.internet import defer
+from twisted.web import server, static
+from twisted.web import resource
+from twisted.python import log
+
+try:
+ from xml.etree import cElementTree as ET
+except:
+ from xml.etree import ElementTree as ET
+import time
+
+class Revision:
+ def __init__(self, r):
+ self.rev = r.get('revision')
+ self.repos = r.get('repos')
+ self.dirs_changed = [x.encode('unicode_escape') for x in r.get('dirs_changed')]
+ self.author = r.get('author').encode('unicode_escape')
+ self.log = r.get('log').encode('unicode_escape')
+ self.date = r.get('date').encode('unicode_escape')
+
+ def render_commit(self, format):
+ if format == "json":
+ return json.dumps({'commit': {'repository': self.repos,
+ 'revision': self.rev,
+ 'dirs_changed': self.dirs_changed,
+ 'author': self.author,
+ 'log': self.log,
+ 'date': self.date}}) +","
+ elif format == "xml":
+ c = ET.Element('commit', {'repository': self.repos, 'revision': "%d" % (self.rev)})
+ ET.SubElement(c, 'author').text = self.author
+ ET.SubElement(c, 'date').text = self.date
+ ET.SubElement(c, 'log').text = self.log
+ d = ET.SubElement(c, 'dirs_changed')
+ for p in self.dirs_changed:
+ x = ET.SubElement(d, 'path')
+ x.text = p
+ str = ET.tostring(c, 'UTF-8') + "\n"
+ return str[39:]
+ else:
+ raise Exception("Ooops, invalid format")
+
+ def render_dirs_changed(self, format):
+ if format == "json":
+ return json.dumps({'commit': {'repository': self.repos,
+ 'revision': self.rev,
+ 'dirs_changed': self.dirs_changed}}) +","
+ elif format == "xml":
+ c = ET.Element('commit', {'repository': self.repos, 'revision': "%d" % (self.rev)})
+ d = ET.SubElement(c, 'dirs_changed')
+ for p in self.dirs_changed:
+ x = ET.SubElement(d, 'path')
+ x.text = p
+ str = ET.tostring(c, 'UTF-8') + "\n"
+ return str[39:]
+ else:
+ raise Exception("Ooops, invalid format")
+
+HEARTBEAT_TIME = 15
+
+class Client(object):
+ def __init__(self, pubsub, r, repos, fmt):
+ self.pubsub = pubsub
+ r.notifyFinish().addErrback(self.finished)
+ self.r = r
+ self.format = fmt
+ self.repos = repos
+ self.alive = True
+ log.msg("OPEN: %s:%d (%d clients online)"% (r.getClientIP(), r.client.port, pubsub.cc()+1))
+
+ def finished(self, reason):
+ self.alive = False
+ log.msg("CLOSE: %s:%d (%d clients online)"% (self.r.getClientIP(), self.r.client.port, self.pubsub.cc()))
+ try:
+ self.pubsub.remove(self)
+ except ValueError:
+ pass
+
+ def interested_in(self, uuid):
+ if self.repos is None:
+ return True
+ if uuid == self.repos:
+ return True
+ return False
+
+ def notify(self, data):
+ self.write(data)
+
+ def start(self):
+ self.write_start()
+ reactor.callLater(HEARTBEAT_TIME, self.heartbeat, None)
+
+ def heartbeat(self, args):
+ if self.alive:
+ self.write_heartbeat()
+ reactor.callLater(HEARTBEAT_TIME, self.heartbeat, None)
+
+ def write_data(self, data):
+ self.write(data[self.format] + "\n")
+
+ """ "Data must not be unicode" is what the interfaces.ITransport says... grr. """
+ def write(self, input):
+ self.r.write(str(input))
+
+class JSONClient(Client):
+ def write_start(self):
+ self.r.setHeader('content-type', 'application/json')
+ self.write('{"commits": [\n')
+
+ def write_heartbeat(self):
+ self.write(json.dumps({"stillalive": time.time()}) + ",\n")
+
+class XMLClient(Client):
+ def write_start(self):
+ self.r.setHeader('content-type', 'application/xml')
+ self.write("<?xml version='1.0' encoding='UTF-8'?>\n<commits>")
+
+ def write_heartbeat(self):
+ self.write("<stillalive>%f</stillalive>\n" % (time.time()))
+
+class SvnPubSub(resource.Resource):
+ isLeaf = True
+ clients = {'commits': [],
+ 'dirs-changed': []}
+
+ def cc(self):
+ return reduce(lambda x,y: len(x)+len(y), self.clients.values())
+
+ def remove(self, c):
+ for k in self.clients.keys():
+ self.clients[k].remove(c)
+
+ def render_GET(self, request):
+ log.msg("REQUEST: %s" % (request.uri))
+ uri = request.uri.split('/')
+
+ request.setHeader('content-type', 'text/plain')
+ if len(uri) != 3 and len(uri) != 4:
+ request.setResponseCode(400)
+ return "Invalid path\n"
+
+ uuid = None
+ fmt = None
+ type = uri[1]
+
+ if len(uri) == 3:
+ fmt = uri[2]
+ else:
+ fmt = uri[3]
+ uuid = uri[2]
+
+ if type not in self.clients.keys():
+ request.setResponseCode(400)
+ return "Invalid Reuqest Type\n"
+
+ clients = {'json': JSONClient, 'xml': XMLClient}
+ clientCls = clients.get(fmt)
+ if clientCls == None:
+ request.setResponseCode(400)
+ return "Invalid Format Requested\n"
+
+ c = clientCls(self, request, uuid, fmt)
+ self.clients[type].append(c)
+ c.start()
+ return twisted.web.server.NOT_DONE_YET
+
+ def notifyAll(self, rev):
+ data = {'commits': {},
+ 'dirs-changed': {}}
+ for x in ['xml', 'json']:
+ data['commits'][x] = rev.render_commit(x)
+ data['dirs-changed'][x] = rev.render_dirs_changed(x)
+
+ log.msg("COMMIT: r%d in %d paths (%d clients)" % (rev.rev,
+ len(rev.dirs_changed),
+ self.cc()))
+ for k in self.clients.keys():
+ for c in self.clients[k]:
+ if c.interested_in(rev.repos):
+ c.write_data(data[k])
+
+ def render_PUT(self, request):
+ request.setHeader('content-type', 'text/plain')
+ ip = request.getClientIP()
+ if ip != "127.0.0.1":
+ request.setResponseCode(401)
+ return "Access Denied"
+ input = request.content.read()
+ #import pdb;pdb.set_trace()
+ #print "input: %s" % (input)
+ r = json.loads(input)
+ rev = Revision(r)
+ self.notifyAll(rev)
+ return "Ok"
+
+def svnpubsub_server():
+ root = static.File("/dev/null")
+ s = SvnPubSub()
+ root.putChild("dirs-changed", s)
+ root.putChild("commits", s)
+ root.putChild("commit", s)
+ return server.Site(root)
+
+if __name__ == "__main__":
+ log.startLogging(sys.stdout)
+ # Port 2069 "HTTP Event Port", whatever, sounds good to me
+ reactor.listenTCP(2069, svnpubsub_server())
+ reactor.run()
+
Added: subversion/trunk/tools/server-side/svnpubsub/svntweet.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svntweet.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svntweet.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svntweet.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,258 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SvnTweet - Subscribe to a SvnPubSub stream, and Twitter about it!
+#
+# Example:
+# svntweet.py my-config.json
+#
+# With my-config.json containing stream paths and the twitter auth info:
+# {"stream": "http://svn-master.apache.org:2069/commits/xml",
+# "username": "asfcommits",
+# "password": "MyLuggageComboIs1234"}
+#
+#
+#
+
+import threading
+import sys
+import os
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+from twisted.internet import defer, reactor, task, threads
+from twisted.python import failure, log
+from twisted.web.client import HTTPClientFactory, HTTPPageDownloader
+
+from urlparse import urlparse
+from xml.sax import handler, make_parser
+import time
+
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "twitty-twister", "lib"))
+try:
+ import twitter
+except:
+ print "Get a copy of twitty-twister from <http://github.com/dustin/twitty-twister>"
+ sys.exit(-1)
+class Config(object):
+ def __init__(self, path):
+ self.path = path
+ self.mtime_path = 0
+ self.config = {}
+ self._load_config()
+
+ def _load_config(self):
+ mtime = os.path.getmtime(self.path)
+ if mtime != self.mtime_path:
+ fp = open(self.path, "rb")
+ self.mtime_path = mtime
+ self.config = json.loads(fp.read())
+
+class HTTPStream(HTTPClientFactory):
+ protocol = HTTPPageDownloader
+
+ def __init__(self, url):
+ HTTPClientFactory.__init__(self, url, method="GET", agent="SvnTweet/0.1.0")
+
+ def pageStart(self, partial):
+ pass
+
+ def pagePart(self, data):
+ pass
+
+ def pageEnd(self):
+ pass
+
+class Revision:
+ def __init__(self, repos, rev):
+ self.repos = repos
+ self.rev = rev
+ self.dirs_changed = []
+ self.author = None
+ self.log = None
+ self.date = None
+
+class StreamHandler(handler.ContentHandler):
+ def __init__(self, bdec):
+ handler.ContentHandler.__init__(self)
+ self.bdec = bdec
+ self.rev = None
+ self.text_value = None
+
+ def startElement(self, name, attrs):
+ #print "start element: %s" % (name)
+ """
+ <commit repository="13f79535-47bb-0310-9956-ffa450edef68"
+ revision="815618">
+ <author>joehni</author>
+ <date>2009-09-16 06:00:21 +0000 (Wed, 16 Sep 2009)</date>
+ <log>pom.xml is not executable.</log>
+ <dirs_changed><path>commons/proper/commons-parent/trunk/</path></dirs_changed>
+ </commit>
+ """
+ if name == "commit":
+ self.rev = Revision(repos=attrs['repository'],
+ rev=int(attrs['revision']))
+ elif name == "stillalive":
+ self.bdec.stillalive()
+ def characters(self, data):
+ if self.text_value is not None:
+ self.text_value = self.text_value + data
+ else:
+ self.text_value = data
+
+ def endElement(self, name):
+ #print "end element: %s" % (name)
+ if name == "commit":
+ self.bdec.commit(self.rev)
+ self.rev = None
+ if self.text_value is not None and self.rev is not None:
+ if name == "path":
+ self.rev.dirs_changed.append(self.text_value.strip())
+ if name == "author":
+ self.rev.author = self.text_value.strip()
+ if name == "date":
+ self.rev.date = self.text_value.strip()
+ if name == "log":
+ self.rev.log = self.text_value.strip()
+ self.text_value = None
+
+
+class XMLHTTPStream(HTTPStream):
+ def __init__(self, url, bdec):
+ HTTPStream.__init__(self, url)
+ self.bdec = bdec
+ self.parser = make_parser(['xml.sax.expatreader'])
+ self.handler = StreamHandler(bdec)
+ self.parser.setContentHandler(self.handler)
+
+ def pageStart(self, parital):
+ self.bdec.pageStart()
+
+ def pagePart(self, data):
+ self.parser.feed(data)
+
+def connectTo(url, bdec):
+ u = urlparse(url)
+ port = u.port
+ if not port:
+ port = 80
+ s = XMLHTTPStream(url, bdec)
+ conn = reactor.connectTCP(u.hostname, u.port, s)
+ return [s, conn]
+
+
+CHECKBEAT_TIME = 90
+
+class BigDoEverythingClasss(object):
+ def __init__(self, config):
+ self.c = config
+ self.c._load_config()
+ self.url = str(self.c.config.get('stream'))
+ self.failures = 0
+ self.alive = time.time()
+ self.checker = task.LoopingCall(self._checkalive)
+ self.transport = None
+ self.stream = None
+ self._restartStream()
+ self.watch = []
+ self.twit = twitter.Twitter(self.c.config.get('username'), self.c.config.get('password'))
+
+ def pageStart(self):
+ log.msg("Stream Connection Established")
+ self.failures = 0
+
+ def _restartStream(self):
+ (self.stream, self.transport) = connectTo(self.url, self)
+ self.stream.deferred.addBoth(self.streamDead)
+ self.alive = time.time()
+ self.checker.start(CHECKBEAT_TIME)
+
+ def _checkalive(self):
+ n = time.time()
+ if n - self.alive > CHECKBEAT_TIME:
+ log.msg("Stream is dead, reconnecting")
+ self.transport.disconnect()
+
+ def stillalive(self):
+ self.alive = time.time()
+
+ def streamDead(self, v):
+ BACKOFF_SECS = 5
+ BACKOFF_MAX = 60
+ self.checker.stop()
+
+ self.stream = None
+ self.failures += 1
+ backoff = min(self.failures * BACKOFF_SECS, BACKOFF_MAX)
+ log.msg("Stream disconnected, trying again in %d seconds.... %s" % (backoff, self.url))
+ reactor.callLater(backoff, self._restartStream)
+
+ def _normalize_path(self, path):
+ if path[0] != '/':
+ return "/" + path
+ return os.path.abspath(path)
+
+ def tweet(self, msg):
+ log.msg("SEND TWEET: %s" % (msg))
+ self.twit.update(msg).addCallback(self.tweet_done).addErrback(log.msg)
+
+ def tweet_done(self, x):
+ log.msg("TWEET: Success!")
+
+ def build_tweet(self, rev):
+ maxlen = 144
+ left = maxlen
+ paths = map(self._normalize_path, rev.dirs_changed)
+ if not len(paths):
+ return None
+ path = os.path.commonprefix(paths)
+ if path[0:1] == '/' and len(path) > 1:
+ path = path[1:]
+
+ #TODO: shorter link
+ link = " - http://svn.apache.org/viewvc?view=rev&revision=%d" % (rev.rev)
+ left -= len(link)
+ msg = "r%d in %s by %s: " % (rev.rev, path, rev.author)
+ left -= len(msg)
+ if left > 3:
+ msg += rev.log[0:left]
+ msg += link
+ return msg
+
+ def commit(self, rev):
+ log.msg("COMMIT r%d (%d paths)" % (rev.rev, len(rev.dirs_changed)))
+ msg = self.build_tweet(rev)
+ if msg:
+ self.tweet(msg)
+ #print "Common Prefix: %s" % (pre)
+
+def main(config_file):
+ c = Config(config_file)
+ big = BigDoEverythingClasss(c)
+ reactor.run()
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print "invalid args, read source code"
+ sys.exit(0)
+ log.startLogging(sys.stdout)
+ main(sys.argv[1])
Added: subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,466 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# SvnWcSub - Subscribe to a SvnPubSub stream, and keep a set of working copy
+# paths in sync
+#
+# Example:
+# svnwcsub.py svnwcsub.conf
+#
+# On startup svnwcsub checks the working copy's path, runs a single svn update
+# and then watches for changes to that path.
+#
+# See svnwcsub.conf for more information on its contents.
+#
+
+import subprocess
+import threading
+import sys
+import os
+import re
+import ConfigParser
+import time
+
+from twisted.internet import defer, reactor, task, threads
+from twisted.internet.utils import getProcessOutput
+from twisted.application import internet
+from twisted.python import failure, log
+from twisted.web.client import HTTPClientFactory, HTTPPageDownloader
+from urlparse import urlparse
+from xml.sax import handler, make_parser
+from twisted.internet import protocol
+
+
+"""
+Wrapper around svn(1), just to keep it from spreading everywhere, incase
+we ever convert to another python-subversion bridge api. (This has happened;
+this class used to wrap pysvn.)
+
+Yes, this exposes accessors for each piece of info we need, but it keeps it
+simpler.
+"""
+class SvnClient(object):
+ def __init__(self, path, url):
+ self.path = path
+ self.url = url
+ self.info = {}
+
+ def cleanup(self):
+ # despite the name, this just deletes the client context ---
+ # which is a no-op for svn(1) wrappers.
+ pass
+
+ def _run_info(self):
+ "run `svn info` and return the output"
+ argv = ["svn", "info", "--non-interactive", "--", self.path]
+ output = None
+
+ if not os.path.isdir(self.path):
+ log.msg("autopopulate %s from %s" % ( self.path, self.url))
+ subprocess.check_call(['svn', 'co', '-q', '--non-interactive', '--config-dir', '/home/svnwc/.subversion', '--', self.url, self.path])
+
+ if hasattr(subprocess, 'check_output'):
+ output = subprocess.check_output(argv)
+ else:
+ pipe = subprocess.Popen(argv, stdout=subprocess.PIPE)
+ output, _ = pipe.communicate()
+ if pipe.returncode:
+ raise subprocess.CalledProcessError(pipe.returncode, argv)
+ return output
+
+ def _get_info(self, force=False):
+ "run `svn info` and parse that info self.info"
+ if force or not self.info:
+ info = {}
+ for line in self._run_info().split("\n"):
+ # Ensure there's at least one colon-space in the line, to avoid
+ # unpack errors.
+ name, value = ("%s: " % line).split(': ', 1)
+ # Canonicalize the key names.
+ info[{
+ "Repository Root": 'repos',
+ "URL": 'url',
+ "Repository UUID": 'uuid',
+ "Revision": 'revision',
+ }.get(name, None)] = value[:-2] # unadd the colon-space
+ self.info = info
+
+ def get_repos(self):
+ self._get_info()
+ return unicode(self.info['repos'])
+
+ def get_url(self):
+ self._get_info()
+ return unicode(self.info['url'])
+
+ def get_uuid(self):
+ self._get_info()
+ return unicode(self.info['uuid'])
+
+ def update(self):
+ subprocess.check_call(
+ ["svn", "update", "--non-interactive", "-q", "--", self.path]
+ )
+ self._get_info(True)
+ return int(self.info['revision'])
+
+ # TODO: Wrap status
+ def status(self):
+ return None
+
+"""This has been historically implemented via svn(1) even when SvnClient
+used pysvn."""
+class ProcSvnClient(SvnClient):
+ def __init__(self, path, svnbin="svn", env=None, url=None):
+ super(ProcSvnClient, self).__init__(path, url)
+ self.svnbin = svnbin
+ self.env = env
+
+ @defer.inlineCallbacks
+ def update(self):
+ # removed since this breaks when the SSL certificate names are mismatched, even
+ # if we marked them as trust worthy
+ # '--trust-server-cert',
+ cmd = [self.svnbin, '--config-dir', '/home/svnwc/.subversion', '--trust-server-cert', '--non-interactive', 'cleanup', self.path]
+ output = yield getProcessOutput(cmd[0], args=cmd[1:], env=self.env)
+ cmd = [self.svnbin, '--config-dir', '/home/svnwc/.subversion', '--trust-server-cert', '--non-interactive', 'update', '--ignore-externals', self.path]
+ output = yield getProcessOutput(cmd[0], args=cmd[1:], env=self.env)
+ rev = int(output[output.rfind("revision ")+len("revision "):].replace('.', ''))
+ defer.returnValue(rev)
+
+class WorkingCopy(object):
+ def __init__(self, bdec, path, url):
+ self.lock = threading.Lock()
+ self.bdec = bdec
+ self.path = path
+ self.url = url
+ self.repos = None
+ self.match = None
+ d = threads.deferToThread(self._get_match)
+ d.addCallback(self._set_match)
+
+ def _set_match(self, value):
+ self.match = str(value[0])
+ self.url = value[1]
+ self.repos = value[2]
+ self.uuid = value[3]
+ self.bdec.wc_ready(self)
+
+ def update_applies(self, uuid, path):
+ if self.uuid != uuid:
+ return False
+
+ path = str(path)
+ if path == self.match:
+ #print "ua: Simple match"
+ # easy case. woo.
+ return True
+ if len(path) < len(self.match):
+ # path is potentially a parent directory of match?
+ #print "ua: parent check"
+ if self.match[0:len(path)] == path:
+ return True
+ if len(path) > len(self.match):
+ # path is potentially a sub directory of match
+ #print "ua: sub dir check"
+ if path[0:len(self.match)] == self.match:
+ return True
+ return False
+
+ @defer.inlineCallbacks
+ def update(self):
+ c = ProcSvnClient(self.path, self.bdec.svnbin, self.bdec.env, self.url)
+ rev = yield c.update()
+ c.cleanup()
+ defer.returnValue(rev)
+# return threads.deferToThread(self._update)
+
+ def _update(self):
+ self.lock.acquire()
+ try:
+ c = ProcSvnClient(self.path, self.bdec.svnbin, self.bdec.env, self.url)
+ rev = c.update()
+ c.cleanup()
+ return rev
+ finally:
+ self.lock.release()
+
+ def _get_match(self):
+ self.lock.acquire()
+ try:
+ c = SvnClient(self.path, self.url)
+ repos = c.get_repos()
+ url = c.get_url()
+ uuid = c.get_uuid()
+ match = url[len(repos):]
+ c.cleanup()
+ return [match, url, repos, uuid]
+ finally:
+ self.lock.release()
+
+
+class HTTPStream(HTTPClientFactory):
+ protocol = HTTPPageDownloader
+
+ def __init__(self, url):
+ self.url = url
+ HTTPClientFactory.__init__(self, url, method="GET", agent="SvnWcSub/0.1.0")
+
+ def pageStart(self, partial):
+ pass
+
+ def pagePart(self, data):
+ pass
+
+ def pageEnd(self):
+ pass
+
+class Revision:
+ def __init__(self, repos, rev):
+ self.repos = repos
+ self.rev = rev
+ self.dirs_changed = []
+
+class StreamHandler(handler.ContentHandler):
+ def __init__(self, stream, bdec):
+ handler.ContentHandler.__init__(self)
+ self.stream = stream
+ self.bdec = bdec
+ self.rev = None
+ self.text_value = None
+
+ def startElement(self, name, attrs):
+ #print "start element: %s" % (name)
+ """
+ <commit revision="7">
+ <dirs_changed><path>/</path></dirs_changed>
+ </commit>
+ """
+ if name == "commit":
+ self.rev = Revision(attrs['repository'], int(attrs['revision']))
+ elif name == "stillalive":
+ self.bdec.stillalive(self.stream)
+ def characters(self, data):
+ if self.text_value is not None:
+ self.text_value = self.text_value + data
+ else:
+ self.text_value = data
+
+ def endElement(self, name):
+ #print "end element: %s" % (name)
+ if name == "commit":
+ self.bdec.commit(self.stream, self.rev)
+ self.rev = None
+ if name == "path" and self.text_value is not None and self.rev is not None:
+ self.rev.dirs_changed.append(self.text_value.strip())
+ self.text_value = None
+
+
+class XMLHTTPStream(HTTPStream):
+ def __init__(self, url, bdec):
+ HTTPStream.__init__(self, url)
+ self.alive = 0
+ self.bdec = bdec
+ self.parser = make_parser(['xml.sax.expatreader'])
+ self.handler = StreamHandler(self, bdec)
+ self.parser.setContentHandler(self.handler)
+
+ def pageStart(self, parital):
+ self.bdec.pageStart(self)
+
+ def pagePart(self, data):
+ self.parser.feed(data)
+
+ def pageEnd(self):
+ self.bdec.pageEnd(self)
+
+def connectTo(url, bdec):
+ u = urlparse(url)
+ port = u.port
+ if not port:
+ port = 80
+ s = XMLHTTPStream(url, bdec)
+ if bdec.service:
+ conn = internet.TCPClient(u.hostname, u.port, s)
+ conn.setServiceParent(bdec.service)
+ else:
+ conn = reactor.connectTCP(u.hostname, u.port, s)
+ return [s, conn]
+
+
+CHECKBEAT_TIME = 60
+PRODUCTION_RE_FILTER = re.compile("/websites/production/[^/]+/")
+
+class BigDoEverythingClasss(object):
+ def __init__(self, config, service = None):
+ self.urls = [s.strip() for s in config.get_value('streams').split()]
+ self.svnbin = config.get_value('svnbin')
+ self.env = config.get_env()
+ self.service = service
+ self.failures = 0
+ self.alive = time.time()
+ self.checker = task.LoopingCall(self._checkalive)
+ self.transports = {}
+ self.streams = {}
+ for u in self.urls:
+ self._restartStream(u)
+ self.watch = []
+ for path, url in config.get_track().items():
+ # working copies auto-register with the BDEC when they are ready.
+ WorkingCopy(self, path, url)
+ self.checker.start(CHECKBEAT_TIME)
+
+ def pageStart(self, stream):
+ log.msg("Stream %s Connection Established" % (stream.url))
+ self.failures = 0
+
+ def pageEnd(self, stream):
+ log.msg("Stream %s Connection Dead" % (stream.url))
+ self.streamDead(stream.url)
+
+ def _restartStream(self, url):
+ (self.streams[url], self.transports[url]) = connectTo(url, self)
+ self.streams[url].deferred.addBoth(self.streamDead, url)
+ self.streams[url].alive = time.time()
+
+ def _checkalive(self):
+ n = time.time()
+ for k in self.streams.keys():
+ s = self.streams[k]
+ if n - s.alive > CHECKBEAT_TIME:
+ log.msg("Stream %s is dead, reconnecting" % (s.url))
+ #self.transports[s.url].disconnect()
+ self.streamDead(self, s.url)
+
+# d=filter(lambda x:x not in self.streams.keys(), self.urls)
+# for u in d:
+# self._restartStream(u)
+
+ def stillalive(self, stream):
+ stream.alive = time.time()
+
+ def streamDead(self, url, result=None):
+ s = self.streams.get(url)
+ if not s:
+ log.msg("Stream %s is messed up" % (url))
+ return
+ BACKOFF_SECS = 5
+ BACKOFF_MAX = 60
+ #self.checker.stop()
+
+ self.streams[url] = None
+ self.transports[url] = None
+ self.failures += 1
+ backoff = min(self.failures * BACKOFF_SECS, BACKOFF_MAX)
+ log.msg("Stream disconnected, trying again in %d seconds.... %s" % (backoff, s.url))
+ reactor.callLater(backoff, self._restartStream, url)
+
+ @defer.inlineCallbacks
+ def wc_ready(self, wc):
+ # called when a working copy object has its basic info/url,
+ # Add it to our watchers, and trigger an svn update.
+ log.msg("Watching WC at %s <-> %s" % (wc.path, wc.url))
+ self.watch.append(wc)
+ rev = yield wc.update()
+ log.msg("wc update: %s is at r%d" % (wc.path, rev))
+
+ def _normalize_path(self, path):
+ if path[0] != '/':
+ return "/" + path
+ return os.path.abspath(path)
+
+ @defer.inlineCallbacks
+ def commit(self, stream, rev):
+ log.msg("COMMIT r%d (%d paths) via %s" % (rev.rev, len(rev.dirs_changed), stream.url))
+ paths = map(self._normalize_path, rev.dirs_changed)
+ if len(paths):
+ pre = os.path.commonprefix(paths)
+ if pre == "/websites/":
+ # special case for svnmucc "dynamic content" buildbot commits
+ # just take the first production path to avoid updating all cms working copies
+ for p in paths:
+ m = PRODUCTION_RE_FILTER.match(p)
+ if m:
+ pre = m.group(0)
+ break
+
+ #print "Common Prefix: %s" % (pre)
+ wcs = [wc for wc in self.watch if wc.update_applies(rev.repos, pre)]
+ log.msg("Updating %d WC for r%d" % (len(wcs), rev.rev))
+ for wc in wcs:
+ rev = yield wc.update()
+ log.msg("wc update: %s is at r%d" % (wc.path, rev))
+
+
+class ReloadableConfig(ConfigParser.SafeConfigParser):
+ def __init__(self, fname):
+ ConfigParser.SafeConfigParser.__init__(self)
+
+ self.fname = fname
+ self.read(fname)
+
+ ### install a signal handler to set SHOULD_RELOAD. BDEC should
+ ### poll this flag, and then adjust its internal structures after
+ ### the reload.
+ self.should_reload = False
+
+ def reload(self):
+ # Delete everything. Just re-reading would overlay, and would not
+ # remove sections/options. Note that [DEFAULT] will not be removed.
+ for section in self.sections():
+ self.remove_section(section)
+
+ # Now re-read the configuration file.
+ self.read(fname)
+
+ def get_value(self, which):
+ return self.get(ConfigParser.DEFAULTSECT, which)
+
+ def get_env(self):
+ env = os.environ.copy()
+ default_options = self.defaults().keys()
+ for name, value in self.items('env'):
+ print name, value
+ if name not in default_options:
+ env[name] = value
+ return env
+
+ def get_track(self):
+ "Return the {PATH: URL} dictionary of working copies to track."
+ track = dict(self.items('track'))
+ for name in self.defaults().keys():
+ del track[name]
+ return track
+
+ def optionxform(self, option):
+ # Do not lowercase the option name.
+ return str(option)
+
+
+def main(config_file):
+ c = ReloadableConfig(config_file)
+ big = BigDoEverythingClasss(c)
+ reactor.run()
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print "invalid args, read source code"
+ sys.exit(0)
+ log.startLogging(sys.stdout)
+ main(sys.argv[1])
Propchange: subversion/trunk/tools/server-side/svnpubsub/svnwcsub.py
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/test.conf
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/test.conf?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/test.conf (added)
+++ subversion/trunk/tools/server-side/svnpubsub/test.conf Tue Feb 28 18:52:13 2012
@@ -0,0 +1,4 @@
+# For use with testserver.py
+
+[DEFAULT]
+streams: http://127.0.0.1:2069/commits/xml
Added: subversion/trunk/tools/server-side/svnpubsub/testserver.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/testserver.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/testserver.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/testserver.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# A simple test server for responding in different ways to SvnPubSub clients.
+# This avoids the complexity of the Twisted framework in order to direct
+# various (abnormal) conditions at the client.
+#
+# ### usage...
+#
+
+import sys
+import BaseHTTPServer
+
+
+PORT = 2069
+
+TEST_BODY = '<commit repository="12345678-1234-1234-1234-123456789012" revision="1234"><author>johndoe</author><date>2012-01-01 01:01:01 +0000 (Sun, 01 Jan 2012)</date><log>Frob the ganoozle with the snookish</log><dirs_changed><path>one/path/</path><path>some/other/directory/</path></dirs_changed></commit>'
+
+SEND_KEEPALIVE = True
+
+
+class TestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_GET(self):
+ self.send_response(200)
+ self.send_header('Content-Length', str(len(TEST_BODY)))
+ self.send_header('Connection', 'keep-alive')
+ self.end_headers()
+ self.wfile.write(TEST_BODY)
+
+
+if __name__ == '__main__':
+ server = BaseHTTPServer.HTTPServer(('', PORT), TestHandler)
+ sys.stderr.write('Now listening on port %d...\n' % (PORT,))
+ server.serve_forever()
Propchange: subversion/trunk/tools/server-side/svnpubsub/testserver.py
------------------------------------------------------------------------------
svn:executable = *
Added: subversion/trunk/tools/server-side/svnpubsub/watcher.py
URL: http://svn.apache.org/viewvc/subversion/trunk/tools/server-side/svnpubsub/watcher.py?rev=1294778&view=auto
==============================================================================
--- subversion/trunk/tools/server-side/svnpubsub/watcher.py (added)
+++ subversion/trunk/tools/server-side/svnpubsub/watcher.py Tue Feb 28 18:52:13 2012
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Watch for events from SvnPubSub and print them to stdout
+#
+# ### usage...
+#
+
+import sys
+import urlparse
+import pprint
+
+import svnpubsub.client
+import svnwcsub ### for ReloadableConfig
+
+
+def _commit(host, port, rev):
+ print 'COMMIT: from %s:%s' % (host, port)
+ pprint.pprint(vars(rev), indent=2)
+
+
+def _event(host, port, event_name):
+ print 'EVENT: from %s:%s "%s"' % (host, port, event_name)
+
+
+def main(config_file):
+ config = svnwcsub.ReloadableConfig(config_file)
+ hostports = [ ]
+ for url in config.get_value('streams').split():
+ parsed = urlparse.urlparse(url)
+ hostports.append((parsed.hostname, parsed.port))
+
+ mc = svnpubsub.client.MultiClient(hostports, _commit, _event)
+ mc.run_forever()
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print "invalid args, read source code"
+ sys.exit(0)
+ main(sys.argv[1])
Propchange: subversion/trunk/tools/server-side/svnpubsub/watcher.py
------------------------------------------------------------------------------
svn:executable = *