1 """ |
|
2 Irker client. |
|
3 """ |
|
4 |
|
5 import pvl.syslog.file # for stdin |
|
6 import pvl.socket # for tcp |
|
7 |
|
8 import optparse, sys |
|
9 |
|
10 import logging; log = logging.getLogger('pvl.irk') |
|
11 |
|
12 import json |
|
13 |
|
14 def parser (parser, connect='tcp://localhost/', target=None) : |
|
15 """ |
|
16 Optparse option group. |
|
17 """ |
|
18 |
|
19 irker = optparse.OptionGroup(parser, 'Irker output') |
|
20 |
|
21 irker.add_option('--irker', metavar='URL', default=connect, |
|
22 help="Irker daemon URL") |
|
23 |
|
24 irker.add_option('--irker-notice', action='store_true', |
|
25 help="Use irker NOTICE") |
|
26 |
|
27 irker.add_option('--irker-part', action='store_true', |
|
28 help="Use irker PART") |
|
29 |
|
30 return irker |
|
31 |
|
32 def apply (options) : |
|
33 """ |
|
34 Return Irker (XXX: target) from options. |
|
35 """ |
|
36 |
|
37 # None -> stdout |
|
38 return Irker(options.irker, options) # options.irker_* |
|
39 |
|
40 class IrkError (Exception) : |
|
41 """ |
|
42 Irk write error. |
|
43 """ |
|
44 |
|
45 class Irk (object) : |
|
46 """ |
|
47 Irker JSON connection speaks JSON over a stream. |
|
48 |
|
49 TODO: timeouts? |
|
50 """ |
|
51 |
|
52 PORT = 6659 |
|
53 |
|
54 @classmethod |
|
55 def connect (cls, url) : |
|
56 """ |
|
57 Connect to given URL string, or None -> stdout |
|
58 """ |
|
59 |
|
60 if not url : |
|
61 # no read |
|
62 return cls(pvl.syslog.file.File(sys.stdout), recv=False) |
|
63 |
|
64 else : |
|
65 sock = pvl.socket.connect(url, port=cls.PORT) |
|
66 |
|
67 # just to make things a bit more exciting... and we really don't want to be blocking on our output.. |
|
68 sock.setblocking(False) |
|
69 |
|
70 return cls( |
|
71 pvl.socket.WriteStream(sock, buffer=None), |
|
72 pvl.socket.ReadStream(sock) |
|
73 ) |
|
74 |
|
75 def __init__ (self, send, recv=None) : |
|
76 """ |
|
77 Use given file-like object (write, flush, fileno) for output. |
|
78 """ |
|
79 |
|
80 self.send = send |
|
81 self.recv = recv |
|
82 |
|
83 log.debug("%s <-> %s", send, recv) |
|
84 |
|
85 def fileno (self) : |
|
86 """ |
|
87 Return fd. Useful for detecting error conditions (connection lost). |
|
88 |
|
89 Only valid if self.recv is True. |
|
90 """ |
|
91 |
|
92 return self.recv.fileno() |
|
93 |
|
94 def __call__ (self, **opts) : |
|
95 """ |
|
96 Send given json. |
|
97 |
|
98 Raises IrkError on write EOF. |
|
99 |
|
100 XXX: Raises socket.error/IOError on write errors? |
|
101 """ |
|
102 |
|
103 log.debug("%s", opts) |
|
104 |
|
105 try : |
|
106 # write line + flush |
|
107 self.send(json.dumps(opts)) |
|
108 |
|
109 except EOFError as ex : |
|
110 # XXX: also socket.error etc? |
|
111 raise IrkError("%s: send eof: %s" % (self, ex)) |
|
112 |
|
113 # XXX: self.send.flush() |
|
114 |
|
115 def __iter__ (self) : |
|
116 """ |
|
117 Yield JSON inputs from source. |
|
118 """ |
|
119 |
|
120 if not self.recv : |
|
121 # never going to be anything |
|
122 return |
|
123 |
|
124 for line in self.recv : |
|
125 # XXX: error handling? |
|
126 yield json.loads(line) |
|
127 |
|
128 class IrkerTarget (object) : |
|
129 """ |
|
130 A channel on an Irk connection. |
|
131 |
|
132 Raises IrkError if irk(..) fails. |
|
133 """ |
|
134 |
|
135 def __init__ (self, irker, target, notice=None, part=None) : |
|
136 self.irker = irker |
|
137 self.target = target |
|
138 |
|
139 self._notice = notice |
|
140 self._part = part |
|
141 |
|
142 def join (self) : |
|
143 log.info("%s", self) |
|
144 self.irker(to=str(self), privmsg='') |
|
145 |
|
146 def privmsg (self, *args) : |
|
147 for arg in args : |
|
148 log.info("%s: %s", self, arg) |
|
149 self.irker(to=str(self), privmsg=arg) |
|
150 |
|
151 def notice (self, *args) : |
|
152 for arg in args : |
|
153 log.info("%s: %s", self, arg) |
|
154 self.irker(to=str(self), notice=arg) |
|
155 |
|
156 def part (self, msg='') : |
|
157 log.info("%s: %s", self, msg) |
|
158 |
|
159 if self._part : |
|
160 self.irker(to=str(self), part=msg) |
|
161 else : |
|
162 log.warn("%s: no --irker-part", self) |
|
163 |
|
164 def __call__ (self, *args) : |
|
165 # default msg policy |
|
166 if self._notice : |
|
167 return self.notice(*args) |
|
168 else : |
|
169 return self.privmsg(*args) |
|
170 |
|
171 def __str__ (self) : |
|
172 return self.target |
|
173 |
|
174 class Irker (object) : |
|
175 """ |
|
176 Reconnecting Irk. |
|
177 """ |
|
178 |
|
179 def __init__ (self, url=None, options=None) : |
|
180 """ |
|
181 url - irker to connect to |
|
182 options - irker_* configs |
|
183 """ |
|
184 |
|
185 self.url = url |
|
186 self.targets = {} |
|
187 self.options = options |
|
188 |
|
189 self.connect() |
|
190 |
|
191 def connect (self) : |
|
192 """ |
|
193 Connect, and fix up our targets. |
|
194 """ |
|
195 |
|
196 self.irk = Irk.connect(self.url) |
|
197 |
|
198 # rejoin |
|
199 for target in self.targets.itervalues() : |
|
200 target.join() |
|
201 |
|
202 def __call__ (self, **opts) : |
|
203 """ |
|
204 Send on current irker connection. |
|
205 |
|
206 Raises IrkError if irk(..) fails. |
|
207 |
|
208 TODO: handle errors and reconnect? |
|
209 """ |
|
210 |
|
211 self.irk(**opts) |
|
212 |
|
213 def target (self, target, join=True) : |
|
214 """ |
|
215 Bind to given target URL, returning an IrkerTarget for sending messages. |
|
216 """ |
|
217 |
|
218 if target not in self.targets : |
|
219 self.targets[target] = IrkerTarget(self, target, |
|
220 notice = self.options and self.options.irker_notice, |
|
221 part = self.options and self.options.irker_part, |
|
222 ) |
|
223 |
|
224 if join : |
|
225 self.targets[target].join() |
|
226 |
|
227 return self.targets[target] |
|
228 |
|
229 __getitem__ = target |
|
230 |
|
231 def __delitem__ (self, target) : |
|
232 """ |
|
233 Unbind given target URL. |
|
234 """ |
|
235 |
|
236 target = self.targets.pop(target) |
|
237 target.part() |
|
238 |
|
239 def __iter__ (self) : |
|
240 return iter(self.targets) |
|