Alien-SVN

 view release on metacpan or  search on metacpan

src/subversion/tools/server-side/svnpubsub/svnpubsub/client.py  view on Meta::CPAN

# 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 json
try:
  import urlparse
except ImportError:
  import urllib.parse as urlparse

# 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 SvnpubsubClientException(Exception):
  pass

class Client(asynchat.async_chat):

  def __init__(self, url, commit_callback, event_callback):
    asynchat.async_chat.__init__(self)

    self.last_activity = time.time()
    self.ibuffer = []

    self.url = url
    parsed_url = urlparse.urlsplit(url)
    if parsed_url.scheme != 'http':
      raise ValueError("URL scheme must be http: '%s'" % url)
    host = parsed_url.hostname
    port = parsed_url.port
    resource = parsed_url.path
    if parsed_url.query:
      resource += "?%s" % parsed_url.query
    if parsed_url.fragment:
      resource += "#%s" % parsed_url.fragment

    self.event_callback = event_callback

    self.parser = JSONRecordHandler(commit_callback, event_callback)

    # Wait for the end of headers. Then we start parsing JSON.
    self.set_terminator(b'\r\n\r\n')
    self.skipping_headers = True

    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
      self.connect((host, port))
    except:
      self.handle_error()
      return

    self.push(('GET %s HTTP/1.0\r\n\r\n' % resource).encode('ascii'))

  def handle_connect(self):
    self.event_callback('connected', None)

  def handle_close(self):
    self.event_callback('closed', None)
    self.close()

  def handle_error(self):
    self.event_callback('error', None)
    self.close()

  def found_terminator(self):
    if self.skipping_headers:
      self.skipping_headers = False
      # Each JSON record is terminated by a null character
      self.set_terminator(b'\0')
    else:
      record = b"".join(self.ibuffer)
      self.ibuffer = []
      self.parser.feed(record.decode())

  def collect_incoming_data(self, data):
    # Remember the last time we saw activity
    self.last_activity = time.time()

    if not self.skipping_headers:
      self.ibuffer.append(data)


class JSONRecordHandler:
  def __init__(self, commit_callback, event_callback):
    self.commit_callback = commit_callback
    self.event_callback = event_callback

  def feed(self, record):
    obj = json.loads(record)
    if 'svnpubsub' in obj:
      actual_version = obj['svnpubsub'].get('version')
      EXPECTED_VERSION = 1
      if actual_version != EXPECTED_VERSION:
        raise SvnpubsubClientException("Unknown svnpubsub format: %r != %d"
                                       % (actual_format, expected_format))



( run in 1.103 second using v1.01-cache-2.11-cpan-b16cb0d3907 )