terom@80: """ terom@80: Irker client. terom@80: """ terom@80: terom@116: import pvl.syslog.file # for stdin terom@116: import pvl.socket # for tcp terom@116: terom@48: import optparse, sys terom@48: terom@80: import logging; log = logging.getLogger('pvl.irk') terom@48: terom@111: import json terom@111: terom@48: def parser (parser, connect='tcp://localhost/', target=None) : terom@48: """ terom@48: Optparse option group. terom@48: """ terom@48: terom@48: irker = optparse.OptionGroup(parser, 'Irker output') terom@48: terom@72: irker.add_option('--irker', metavar='URL', default=connect, terom@48: help="Irker daemon URL") terom@48: terom@83: irker.add_option('--irker-notice', action='store_true', terom@83: help="Use irker NOTICE") terom@83: terom@128: irker.add_option('--irker-part', action='store_true', terom@128: help="Use irker PART") terom@128: terom@48: return irker terom@48: terom@72: def apply (options) : terom@48: """ terom@48: Return Irker (XXX: target) from options. terom@48: """ terom@48: terom@79: # None -> stdout terom@128: return Irker(options.irker, options) # options.irker_* terom@48: terom@145: class IrkError (Exception) : terom@145: """ terom@145: Irk write error. terom@145: """ terom@145: terom@48: class Irk (object) : terom@48: """ terom@116: Irker JSON connection speaks JSON over a stream. terom@79: terom@116: TODO: timeouts? terom@48: """ terom@48: terom@48: PORT = 6659 terom@48: terom@79: @classmethod terom@79: def connect (cls, url) : terom@79: """ terom@116: Connect to given URL string, or None -> stdout terom@79: """ terom@79: terom@79: if not url : terom@116: # no read terom@116: return cls(pvl.syslog.file.File(sys.stdout), recv=False) terom@79: terom@79: else : terom@116: sock = pvl.socket.connect(url, port=cls.PORT) terom@79: terom@116: # just to make things a bit more exciting... and we really don't want to be blocking on our output.. terom@116: sock.setblocking(False) terom@116: terom@116: return cls( terom@116: pvl.socket.WriteStream(sock, buffer=None), terom@116: pvl.socket.ReadStream(sock) terom@116: ) terom@111: terom@116: def __init__ (self, send, recv=None) : terom@79: """ terom@111: Use given file-like object (write, flush, fileno) for output. terom@79: """ terom@79: terom@116: self.send = send terom@111: self.recv = recv terom@116: terom@116: log.debug("%s <-> %s", send, recv) terom@79: terom@111: def fileno (self) : terom@111: """ terom@111: Return fd. Useful for detecting error conditions (connection lost). terom@116: terom@116: Only valid if self.recv is True. terom@111: """ terom@111: terom@111: return self.recv.fileno() terom@111: terom@116: def __call__ (self, **opts) : terom@79: """ terom@145: Send given json. terom@145: terom@145: Raises IrkError on write EOF. terom@145: terom@145: XXX: Raises socket.error/IOError on write errors? terom@79: """ terom@79: terom@79: log.debug("%s", opts) terom@79: terom@145: try : terom@145: # write line + flush terom@145: self.send(json.dumps(opts)) terom@145: terom@145: except EOFError as ex : terom@145: # XXX: also socket.error etc? terom@145: raise IrkError("%s: send eof: %s" % (self, ex)) terom@111: terom@116: # XXX: self.send.flush() terom@111: terom@116: def __iter__ (self) : terom@111: """ terom@116: Yield JSON inputs from source. terom@111: """ terom@111: terom@121: if not self.recv : terom@121: # never going to be anything terom@121: return terom@121: terom@116: for line in self.recv : terom@116: # XXX: error handling? terom@116: yield json.loads(line) terom@111: terom@79: class IrkerTarget (object) : terom@79: """ terom@79: A channel on an Irk connection. terom@145: terom@145: Raises IrkError if irk(..) fails. terom@79: """ terom@79: terom@128: def __init__ (self, irker, target, notice=None, part=None) : terom@79: self.irker = irker terom@79: self.target = target terom@79: terom@83: self._notice = notice terom@128: self._part = part terom@83: terom@79: def join (self) : terom@79: log.info("%s", self) terom@116: self.irker(to=str(self), privmsg='') terom@79: terom@79: def privmsg (self, *args) : terom@79: for arg in args : terom@79: log.info("%s: %s", self, arg) terom@116: self.irker(to=str(self), privmsg=arg) terom@79: terom@83: def notice (self, *args) : terom@83: for arg in args : terom@83: log.info("%s: %s", self, arg) terom@116: self.irker(to=str(self), notice=arg) terom@83: terom@128: def part (self, msg='') : terom@128: log.info("%s: %s", self, msg) terom@128: terom@128: if self._part : terom@128: self.irker(to=str(self), part=msg) terom@128: else : terom@130: log.warn("%s: no --irker-part", self) terom@128: terom@83: def __call__ (self, *args) : terom@83: # default msg policy terom@83: if self._notice : terom@83: return self.notice(*args) terom@83: else : terom@83: return self.privmsg(*args) terom@79: terom@79: def __str__ (self) : terom@79: return self.target terom@79: terom@79: class Irker (object) : terom@79: """ terom@116: Reconnecting Irk. terom@79: """ terom@79: terom@128: def __init__ (self, url=None, options=None) : terom@128: """ terom@128: url - irker to connect to terom@128: options - irker_* configs terom@128: """ terom@128: terom@116: self.url = url terom@48: self.targets = {} terom@128: self.options = options terom@48: terom@79: self.connect() terom@48: terom@48: def connect (self) : terom@79: """ terom@79: Connect, and fix up our targets. terom@79: """ terom@48: terom@79: self.irk = Irk.connect(self.url) terom@48: terom@48: # rejoin terom@79: for target in self.targets.itervalues() : terom@79: target.join() terom@48: terom@116: def __call__ (self, **opts) : terom@79: """ terom@79: Send on current irker connection. terom@48: terom@145: Raises IrkError if irk(..) fails. terom@145: terom@79: TODO: handle errors and reconnect? terom@134: """ terom@79: terom@116: self.irk(**opts) terom@48: terom@134: def target (self, target, join=True) : terom@48: """ terom@79: Bind to given target URL, returning an IrkerTarget for sending messages. terom@48: """ terom@48: terom@79: if target not in self.targets : terom@128: self.targets[target] = IrkerTarget(self, target, terom@128: notice = self.options and self.options.irker_notice, terom@128: part = self.options and self.options.irker_part, terom@128: ) terom@134: terom@134: if join : terom@134: self.targets[target].join() terom@79: terom@79: return self.targets[target] terom@134: terom@134: __getitem__ = target terom@128: terom@128: def __delitem__ (self, target) : terom@128: """ terom@128: Unbind given target URL. terom@128: """ terom@128: terom@128: target = self.targets.pop(target) terom@128: target.part() terom@128: terom@128: def __iter__ (self) : terom@128: return iter(self.targets)