diff -r 530c2aa73a97 -r 231d3de7081a pvl/irk.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/irk.py Fri Jan 11 17:12:51 2013 +0200 @@ -0,0 +1,199 @@ +""" + Irker client. +""" + +import optparse, sys + +import logging; log = logging.getLogger('pvl.irk') + +# proto +import socket, json + +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") + + return irker + +def apply (options) : + """ + Return Irker (XXX: target) from options. + """ + + # None -> stdout + return Irker(options.irker) + +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) + + 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) + + return cls(sock.makefile('w')) + + def __init__ (self, file) : + """ + Use given file-like object for output. + """ + + self.file = file + + log.debug("%s", file) + + 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() + +class IrkerTarget (object) : + """ + A channel on an Irk connection. + """ + + def __init__ (self, irker, target) : + self.irker = irker + self.target = target + + 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) + + __call__ = privmsg + + def __str__ (self) : + return self.target + +class Irker (object) : + """ + Reconnecting irker. + """ + + def __init__ (self, url=None) : + if url : + self.url = urlparse.urlparse(url) + else : + self.url = None + + self.targets = {} + + 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) + self.targets[target].join() + + return self.targets[target] + + __getitem__ = target