Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # -*- Mode: Python; tab-width: 4 -*- |
2 | # Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp | |
3 | # Author: Sam Rushing <rushing@nightmare.com> | |
4 | ||
5 | # ====================================================================== | |
6 | # Copyright 1996 by Sam Rushing | |
7 | # | |
8 | # All Rights Reserved | |
9 | # | |
10 | # Permission to use, copy, modify, and distribute this software and | |
11 | # its documentation for any purpose and without fee is hereby | |
12 | # granted, provided that the above copyright notice appear in all | |
13 | # copies and that both that copyright notice and this permission | |
14 | # notice appear in supporting documentation, and that the name of Sam | |
15 | # Rushing not be used in advertising or publicity pertaining to | |
16 | # distribution of the software without specific, written prior | |
17 | # permission. | |
18 | # | |
19 | # SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, | |
20 | # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN | |
21 | # NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR | |
22 | # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS | |
23 | # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, | |
24 | # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |
25 | # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
26 | # ====================================================================== | |
27 | ||
28 | r"""A class supporting chat-style (command/response) protocols. | |
29 | ||
30 | This class adds support for 'chat' style protocols - where one side | |
31 | sends a 'command', and the other sends a response (examples would be | |
32 | the common internet protocols - smtp, nntp, ftp, etc..). | |
33 | ||
34 | The handle_read() method looks at the input stream for the current | |
35 | 'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n' | |
36 | for multi-line output), calling self.found_terminator() on its | |
37 | receipt. | |
38 | ||
39 | for example: | |
40 | Say you build an async nntp client using this class. At the start | |
41 | of the connection, you'll have self.terminator set to '\r\n', in | |
42 | order to process the single-line greeting. Just before issuing a | |
43 | 'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST | |
44 | command will be accumulated (using your own 'collect_incoming_data' | |
45 | method) up to the terminator, and then control will be returned to | |
46 | you - by calling your self.found_terminator() method. | |
47 | """ | |
48 | ||
49 | import socket | |
50 | import asyncore | |
51 | from collections import deque | |
52 | ||
53 | class async_chat (asyncore.dispatcher): | |
54 | """This is an abstract class. You must derive from this class, and add | |
55 | the two methods collect_incoming_data() and found_terminator()""" | |
56 | ||
57 | # these are overridable defaults | |
58 | ||
59 | ac_in_buffer_size = 4096 | |
60 | ac_out_buffer_size = 4096 | |
61 | ||
62 | def __init__ (self, conn=None): | |
63 | self.ac_in_buffer = '' | |
64 | self.ac_out_buffer = '' | |
65 | self.producer_fifo = fifo() | |
66 | asyncore.dispatcher.__init__ (self, conn) | |
67 | ||
68 | def collect_incoming_data(self, data): | |
69 | raise NotImplementedError, "must be implemented in subclass" | |
70 | ||
71 | def found_terminator(self): | |
72 | raise NotImplementedError, "must be implemented in subclass" | |
73 | ||
74 | def set_terminator (self, term): | |
75 | "Set the input delimiter. Can be a fixed string of any length, an integer, or None" | |
76 | self.terminator = term | |
77 | ||
78 | def get_terminator (self): | |
79 | return self.terminator | |
80 | ||
81 | # grab some more data from the socket, | |
82 | # throw it to the collector method, | |
83 | # check for the terminator, | |
84 | # if found, transition to the next state. | |
85 | ||
86 | def handle_read (self): | |
87 | ||
88 | try: | |
89 | data = self.recv (self.ac_in_buffer_size) | |
90 | except socket.error, why: | |
91 | self.handle_error() | |
92 | return | |
93 | ||
94 | self.ac_in_buffer = self.ac_in_buffer + data | |
95 | ||
96 | # Continue to search for self.terminator in self.ac_in_buffer, | |
97 | # while calling self.collect_incoming_data. The while loop | |
98 | # is necessary because we might read several data+terminator | |
99 | # combos with a single recv(1024). | |
100 | ||
101 | while self.ac_in_buffer: | |
102 | lb = len(self.ac_in_buffer) | |
103 | terminator = self.get_terminator() | |
104 | if terminator is None or terminator == '': | |
105 | # no terminator, collect it all | |
106 | self.collect_incoming_data (self.ac_in_buffer) | |
107 | self.ac_in_buffer = '' | |
108 | elif isinstance(terminator, int): | |
109 | # numeric terminator | |
110 | n = terminator | |
111 | if lb < n: | |
112 | self.collect_incoming_data (self.ac_in_buffer) | |
113 | self.ac_in_buffer = '' | |
114 | self.terminator = self.terminator - lb | |
115 | else: | |
116 | self.collect_incoming_data (self.ac_in_buffer[:n]) | |
117 | self.ac_in_buffer = self.ac_in_buffer[n:] | |
118 | self.terminator = 0 | |
119 | self.found_terminator() | |
120 | else: | |
121 | # 3 cases: | |
122 | # 1) end of buffer matches terminator exactly: | |
123 | # collect data, transition | |
124 | # 2) end of buffer matches some prefix: | |
125 | # collect data to the prefix | |
126 | # 3) end of buffer does not match any prefix: | |
127 | # collect data | |
128 | terminator_len = len(terminator) | |
129 | index = self.ac_in_buffer.find(terminator) | |
130 | if index != -1: | |
131 | # we found the terminator | |
132 | if index > 0: | |
133 | # don't bother reporting the empty string (source of subtle bugs) | |
134 | self.collect_incoming_data (self.ac_in_buffer[:index]) | |
135 | self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:] | |
136 | # This does the Right Thing if the terminator is changed here. | |
137 | self.found_terminator() | |
138 | else: | |
139 | # check for a prefix of the terminator | |
140 | index = find_prefix_at_end (self.ac_in_buffer, terminator) | |
141 | if index: | |
142 | if index != lb: | |
143 | # we found a prefix, collect up to the prefix | |
144 | self.collect_incoming_data (self.ac_in_buffer[:-index]) | |
145 | self.ac_in_buffer = self.ac_in_buffer[-index:] | |
146 | break | |
147 | else: | |
148 | # no prefix, collect it all | |
149 | self.collect_incoming_data (self.ac_in_buffer) | |
150 | self.ac_in_buffer = '' | |
151 | ||
152 | def handle_write (self): | |
153 | self.initiate_send () | |
154 | ||
155 | def handle_close (self): | |
156 | self.close() | |
157 | ||
158 | def push (self, data): | |
159 | self.producer_fifo.push (simple_producer (data)) | |
160 | self.initiate_send() | |
161 | ||
162 | def push_with_producer (self, producer): | |
163 | self.producer_fifo.push (producer) | |
164 | self.initiate_send() | |
165 | ||
166 | def readable (self): | |
167 | "predicate for inclusion in the readable for select()" | |
168 | return (len(self.ac_in_buffer) <= self.ac_in_buffer_size) | |
169 | ||
170 | def writable (self): | |
171 | "predicate for inclusion in the writable for select()" | |
172 | # return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected) | |
173 | # this is about twice as fast, though not as clear. | |
174 | return not ( | |
175 | (self.ac_out_buffer == '') and | |
176 | self.producer_fifo.is_empty() and | |
177 | self.connected | |
178 | ) | |
179 | ||
180 | def close_when_done (self): | |
181 | "automatically close this channel once the outgoing queue is empty" | |
182 | self.producer_fifo.push (None) | |
183 | ||
184 | # refill the outgoing buffer by calling the more() method | |
185 | # of the first producer in the queue | |
186 | def refill_buffer (self): | |
187 | while 1: | |
188 | if len(self.producer_fifo): | |
189 | p = self.producer_fifo.first() | |
190 | # a 'None' in the producer fifo is a sentinel, | |
191 | # telling us to close the channel. | |
192 | if p is None: | |
193 | if not self.ac_out_buffer: | |
194 | self.producer_fifo.pop() | |
195 | self.close() | |
196 | return | |
197 | elif isinstance(p, str): | |
198 | self.producer_fifo.pop() | |
199 | self.ac_out_buffer = self.ac_out_buffer + p | |
200 | return | |
201 | data = p.more() | |
202 | if data: | |
203 | self.ac_out_buffer = self.ac_out_buffer + data | |
204 | return | |
205 | else: | |
206 | self.producer_fifo.pop() | |
207 | else: | |
208 | return | |
209 | ||
210 | def initiate_send (self): | |
211 | obs = self.ac_out_buffer_size | |
212 | # try to refill the buffer | |
213 | if (len (self.ac_out_buffer) < obs): | |
214 | self.refill_buffer() | |
215 | ||
216 | if self.ac_out_buffer and self.connected: | |
217 | # try to send the buffer | |
218 | try: | |
219 | num_sent = self.send (self.ac_out_buffer[:obs]) | |
220 | if num_sent: | |
221 | self.ac_out_buffer = self.ac_out_buffer[num_sent:] | |
222 | ||
223 | except socket.error, why: | |
224 | self.handle_error() | |
225 | return | |
226 | ||
227 | def discard_buffers (self): | |
228 | # Emergencies only! | |
229 | self.ac_in_buffer = '' | |
230 | self.ac_out_buffer = '' | |
231 | while self.producer_fifo: | |
232 | self.producer_fifo.pop() | |
233 | ||
234 | ||
235 | class simple_producer: | |
236 | ||
237 | def __init__ (self, data, buffer_size=512): | |
238 | self.data = data | |
239 | self.buffer_size = buffer_size | |
240 | ||
241 | def more (self): | |
242 | if len (self.data) > self.buffer_size: | |
243 | result = self.data[:self.buffer_size] | |
244 | self.data = self.data[self.buffer_size:] | |
245 | return result | |
246 | else: | |
247 | result = self.data | |
248 | self.data = '' | |
249 | return result | |
250 | ||
251 | class fifo: | |
252 | def __init__ (self, list=None): | |
253 | if not list: | |
254 | self.list = deque() | |
255 | else: | |
256 | self.list = deque(list) | |
257 | ||
258 | def __len__ (self): | |
259 | return len(self.list) | |
260 | ||
261 | def is_empty (self): | |
262 | return not self.list | |
263 | ||
264 | def first (self): | |
265 | return self.list[0] | |
266 | ||
267 | def push (self, data): | |
268 | self.list.append(data) | |
269 | ||
270 | def pop (self): | |
271 | if self.list: | |
272 | return (1, self.list.popleft()) | |
273 | else: | |
274 | return (0, None) | |
275 | ||
276 | # Given 'haystack', see if any prefix of 'needle' is at its end. This | |
277 | # assumes an exact match has already been checked. Return the number of | |
278 | # characters matched. | |
279 | # for example: | |
280 | # f_p_a_e ("qwerty\r", "\r\n") => 1 | |
281 | # f_p_a_e ("qwertydkjf", "\r\n") => 0 | |
282 | # f_p_a_e ("qwerty\r\n", "\r\n") => <undefined> | |
283 | ||
284 | # this could maybe be made faster with a computed regex? | |
285 | # [answer: no; circa Python-2.0, Jan 2001] | |
286 | # new python: 28961/s | |
287 | # old python: 18307/s | |
288 | # re: 12820/s | |
289 | # regex: 14035/s | |
290 | ||
291 | def find_prefix_at_end (haystack, needle): | |
292 | l = len(needle) - 1 | |
293 | while l and not haystack.endswith(needle[:l]): | |
294 | l -= 1 | |
295 | return l |