pvl/irk.py
author Tero Marttila <terom@paivola.fi>
Sun, 13 Jan 2013 00:23:34 +0200
changeset 111 4b96c153c113
parent 83 1cb48f2ba1e9
child 116 89b7385d19ba
permissions -rw-r--r--
pvl.irk: crude support for reading lines from Irk
"""
    Irker client.
"""

import optparse, sys

import logging; log = logging.getLogger('pvl.irk')

# proto
import json

# XXX: socket
import socket, select

def parser (parser, connect='tcp://localhost/', target=None) :
    """
        Optparse option group.
    """

    irker = optparse.OptionGroup(parser, 'Irker output')
    
    irker.add_option('--irker', metavar='URL',  default=connect,
            help="Irker daemon URL")

    irker.add_option('--irker-notice',          action='store_true',
            help="Use irker NOTICE")

    return irker

def apply (options) :
    """
        Return Irker (XXX: target) from options.
    """
    
    # None -> stdout
    return Irker(options.irker, options.irker_notice)

def connect (host=None, port=None, family=socket.AF_UNSPEC, socktype=socket.SOCK_STREAM) :
    """
        Return a TCP/UDP socket connected to the given host/port using getaddrinfo.

        TODO: timeout?
    """

    log.debug("%s:%s: %s/%s", host, port, family, socktype)
    
    if host :
        flags = socket.AI_CANONNAME
    else :
        flags = 0

    addrinfo = socket.getaddrinfo(host, port, family, socktype, 0, flags)

    if not addrinfo :
        raise Exception("getaddrinfo: %s:%s: no results" % (host, port))

    for af, st, proto, name, addr in addrinfo :
        try :
            s = socket.socket(af, st, proto)

        except socket.error as error :
            log.warning("%s:%s: socket: %s", host, port, error)
            continue
        
        try :
            s.connect(addr)

        except socket.error as error :
            log.warning("%s:%s: connect: %s", host, port, error)
            continue
        
        log.info("%s", name)
        
        return s

    else :
        raise Exception("Unable to connect: %s:%s: %s" % (host, port, error))

import urlparse

class Irk (object) :
    """
        Irker JSON connection.

        TODO: timeout?
    """

    PORT = 6659

    SCHEME = {
        'tcp':  (socket.AF_INET, socket.SOCK_STREAM),
        'udp':  (socket.AF_INET, socket.SOCK_DGRAM),
        'unix': (socket.AF_UNIX, socket.SOCK_DGRAM),
    }

    @classmethod
    def connect (cls, url) :
        """
            Connect to given urllib URL, or None -> stdout
        """

        if not url :
            return cls(sys.stdout, recv=False)

        family, socktype = cls.SCHEME[url.scheme]
        
        if family == socket.AF_UNIX :
            raise Exception("unix:// is not supported")
        else :
            # inet
            sock = connect(url.hostname, url.port or cls.PORT, family=family, socktype=socktype)

        # XXX: just to make things a bit more exciting... and we really don't want to be blocking on our output..
        sock.setblocking(False)
        
        return cls(sock.makefile('w'), recv=sock)

    def __init__ (self, file, recv=None) :
        """
            Use given file-like object (write, flush, fileno) for output.
        """

        self.file = file

        # XXX
        self.recv = recv
        self._buf = ''

        log.debug("%s", file)

    def fileno (self) :
        """
            Return fd. Useful for detecting error conditions (connection lost).
        """

        return self.recv.fileno()

    def send (self, **opts) :
        """
            Raises IOError on write errors.
        """

        log.debug("%s", opts)
        
        # write line + flush
        json.dump(opts, self.file)
        self.file.write('\n')
        self.file.flush()

    def read (self, size=512) :
        """
            Recieve data back from irkerd.
            
            Raise EOFError.

            XXX: this is seriously crazy on a buffered file-like object..?
        """
        
        # poll
        read, _, _ = select.select((self.recv, ), (), (), 0.0)
        
        if read :
            read = self.recv.recv(size)

            if read :
                return read
            else :
                raise EOFError()
        else :
            return None # block
    
    def readline (self) :
        """
            Yield line of input, or None.

            Raise EOFError.
        """

        while '\n' not in self._buf :
            read = self.read()

            if not read :
                return None # block
            
            self._buf += read

        line, self._buf = self._buf.split('\n')

        return line
    
    def readlines (self) :
        """
            Yield lines of input, until blocking.
        """

        while True :
            line = self.readline()

            if line :
                yield line
            else :
                return
    
    __iter__ = readlines

class IrkerTarget (object) :
    """
        A channel on an Irk connection.
    """

    def __init__ (self, irker, target, notice=False) :
        self.irker = irker
        self.target = target

        self._notice = notice
        
    def join (self) :
        log.info("%s", self)
        self.irker.send(to=str(self), privmsg='')

    def privmsg (self, *args) :
        for arg in args :
            log.info("%s: %s", self, arg)
            self.irker.send(to=str(self), privmsg=arg)

    def notice (self, *args) :
        for arg in args :
            log.info("%s: %s", self, arg)
            self.irker.send(to=str(self), notice=arg)

    def __call__ (self, *args) :
        # default msg policy
        if self._notice :
            return self.notice(*args)
        else :
            return self.privmsg(*args)

    def __str__ (self) :
        return self.target

class Irker (object) :
    """
        Reconnecting irker.
    """

    def __init__ (self, url=None, notice=False) :
        if url :
            self.url = urlparse.urlparse(url)
        else :
            self.url = None

        self.targets = {}
        self.notice = notice
        
        self.connect()
    
    def connect (self) :
        """
            Connect, and fix up our targets.
        """

        self.irk = Irk.connect(self.url)

        # rejoin
        for target in self.targets.itervalues() :
            target.join()
    
    def send (self, **opts) :
        """
            Send on current irker connection.

            TODO: handle errors and reconnect?
        """

        self.irk.send(**opts)

    def target (self, target) :
        """
            Bind to given target URL, returning an IrkerTarget for sending messages.
        """

        if target not in self.targets :
            self.targets[target] = IrkerTarget(self, target, notice=self.notice)
            self.targets[target].join()
            
        return self.targets[target]

    __getitem__ = target