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 import asyncore
49 from collections import deque
50
51 from warnings import _deprecated
52
53 _DEPRECATION_MSG = ('The {name} module is deprecated and will be removed in '
54 'Python {remove}. The recommended replacement is asyncio')
55 _deprecated(__name__, _DEPRECATION_MSG, remove=(3, 12))
56
57
58
59 class ESC[4;38;5;81masync_chat(ESC[4;38;5;149masyncoreESC[4;38;5;149m.ESC[4;38;5;149mdispatcher):
60 """This is an abstract class. You must derive from this class, and add
61 the two methods collect_incoming_data() and found_terminator()"""
62
63 # these are overridable defaults
64
65 ac_in_buffer_size = 65536
66 ac_out_buffer_size = 65536
67
68 # we don't want to enable the use of encoding by default, because that is a
69 # sign of an application bug that we don't want to pass silently
70
71 use_encoding = 0
72 encoding = 'latin-1'
73
74 def __init__(self, sock=None, map=None):
75 # for string terminator matching
76 self.ac_in_buffer = b''
77
78 # we use a list here rather than io.BytesIO for a few reasons...
79 # del lst[:] is faster than bio.truncate(0)
80 # lst = [] is faster than bio.truncate(0)
81 self.incoming = []
82
83 # we toss the use of the "simple producer" and replace it with
84 # a pure deque, which the original fifo was a wrapping of
85 self.producer_fifo = deque()
86 asyncore.dispatcher.__init__(self, sock, map)
87
88 def collect_incoming_data(self, data):
89 raise NotImplementedError("must be implemented in subclass")
90
91 def _collect_incoming_data(self, data):
92 self.incoming.append(data)
93
94 def _get_data(self):
95 d = b''.join(self.incoming)
96 del self.incoming[:]
97 return d
98
99 def found_terminator(self):
100 raise NotImplementedError("must be implemented in subclass")
101
102 def set_terminator(self, term):
103 """Set the input delimiter.
104
105 Can be a fixed string of any length, an integer, or None.
106 """
107 if isinstance(term, str) and self.use_encoding:
108 term = bytes(term, self.encoding)
109 elif isinstance(term, int) and term < 0:
110 raise ValueError('the number of received bytes must be positive')
111 self.terminator = term
112
113 def get_terminator(self):
114 return self.terminator
115
116 # grab some more data from the socket,
117 # throw it to the collector method,
118 # check for the terminator,
119 # if found, transition to the next state.
120
121 def handle_read(self):
122
123 try:
124 data = self.recv(self.ac_in_buffer_size)
125 except BlockingIOError:
126 return
127 except OSError:
128 self.handle_error()
129 return
130
131 if isinstance(data, str) and self.use_encoding:
132 data = bytes(str, self.encoding)
133 self.ac_in_buffer = self.ac_in_buffer + data
134
135 # Continue to search for self.terminator in self.ac_in_buffer,
136 # while calling self.collect_incoming_data. The while loop
137 # is necessary because we might read several data+terminator
138 # combos with a single recv(4096).
139
140 while self.ac_in_buffer:
141 lb = len(self.ac_in_buffer)
142 terminator = self.get_terminator()
143 if not terminator:
144 # no terminator, collect it all
145 self.collect_incoming_data(self.ac_in_buffer)
146 self.ac_in_buffer = b''
147 elif isinstance(terminator, int):
148 # numeric terminator
149 n = terminator
150 if lb < n:
151 self.collect_incoming_data(self.ac_in_buffer)
152 self.ac_in_buffer = b''
153 self.terminator = self.terminator - lb
154 else:
155 self.collect_incoming_data(self.ac_in_buffer[:n])
156 self.ac_in_buffer = self.ac_in_buffer[n:]
157 self.terminator = 0
158 self.found_terminator()
159 else:
160 # 3 cases:
161 # 1) end of buffer matches terminator exactly:
162 # collect data, transition
163 # 2) end of buffer matches some prefix:
164 # collect data to the prefix
165 # 3) end of buffer does not match any prefix:
166 # collect data
167 terminator_len = len(terminator)
168 index = self.ac_in_buffer.find(terminator)
169 if index != -1:
170 # we found the terminator
171 if index > 0:
172 # don't bother reporting the empty string
173 # (source of subtle bugs)
174 self.collect_incoming_data(self.ac_in_buffer[:index])
175 self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
176 # This does the Right Thing if the terminator
177 # is changed here.
178 self.found_terminator()
179 else:
180 # check for a prefix of the terminator
181 index = find_prefix_at_end(self.ac_in_buffer, terminator)
182 if index:
183 if index != lb:
184 # we found a prefix, collect up to the prefix
185 self.collect_incoming_data(self.ac_in_buffer[:-index])
186 self.ac_in_buffer = self.ac_in_buffer[-index:]
187 break
188 else:
189 # no prefix, collect it all
190 self.collect_incoming_data(self.ac_in_buffer)
191 self.ac_in_buffer = b''
192
193 def handle_write(self):
194 self.initiate_send()
195
196 def handle_close(self):
197 self.close()
198
199 def push(self, data):
200 if not isinstance(data, (bytes, bytearray, memoryview)):
201 raise TypeError('data argument must be byte-ish (%r)',
202 type(data))
203 sabs = self.ac_out_buffer_size
204 if len(data) > sabs:
205 for i in range(0, len(data), sabs):
206 self.producer_fifo.append(data[i:i+sabs])
207 else:
208 self.producer_fifo.append(data)
209 self.initiate_send()
210
211 def push_with_producer(self, producer):
212 self.producer_fifo.append(producer)
213 self.initiate_send()
214
215 def readable(self):
216 "predicate for inclusion in the readable for select()"
217 # cannot use the old predicate, it violates the claim of the
218 # set_terminator method.
219
220 # return (len(self.ac_in_buffer) <= self.ac_in_buffer_size)
221 return 1
222
223 def writable(self):
224 "predicate for inclusion in the writable for select()"
225 return self.producer_fifo or (not self.connected)
226
227 def close_when_done(self):
228 "automatically close this channel once the outgoing queue is empty"
229 self.producer_fifo.append(None)
230
231 def initiate_send(self):
232 while self.producer_fifo and self.connected:
233 first = self.producer_fifo[0]
234 # handle empty string/buffer or None entry
235 if not first:
236 del self.producer_fifo[0]
237 if first is None:
238 self.handle_close()
239 return
240
241 # handle classic producer behavior
242 obs = self.ac_out_buffer_size
243 try:
244 data = first[:obs]
245 except TypeError:
246 data = first.more()
247 if data:
248 self.producer_fifo.appendleft(data)
249 else:
250 del self.producer_fifo[0]
251 continue
252
253 if isinstance(data, str) and self.use_encoding:
254 data = bytes(data, self.encoding)
255
256 # send the data
257 try:
258 num_sent = self.send(data)
259 except OSError:
260 self.handle_error()
261 return
262
263 if num_sent:
264 if num_sent < len(data) or obs < len(first):
265 self.producer_fifo[0] = first[num_sent:]
266 else:
267 del self.producer_fifo[0]
268 # we tried to send some actual data
269 return
270
271 def discard_buffers(self):
272 # Emergencies only!
273 self.ac_in_buffer = b''
274 del self.incoming[:]
275 self.producer_fifo.clear()
276
277
278 class ESC[4;38;5;81msimple_producer:
279
280 def __init__(self, data, buffer_size=512):
281 self.data = data
282 self.buffer_size = buffer_size
283
284 def more(self):
285 if len(self.data) > self.buffer_size:
286 result = self.data[:self.buffer_size]
287 self.data = self.data[self.buffer_size:]
288 return result
289 else:
290 result = self.data
291 self.data = b''
292 return result
293
294
295 # Given 'haystack', see if any prefix of 'needle' is at its end. This
296 # assumes an exact match has already been checked. Return the number of
297 # characters matched.
298 # for example:
299 # f_p_a_e("qwerty\r", "\r\n") => 1
300 # f_p_a_e("qwertydkjf", "\r\n") => 0
301 # f_p_a_e("qwerty\r\n", "\r\n") => <undefined>
302
303 # this could maybe be made faster with a computed regex?
304 # [answer: no; circa Python-2.0, Jan 2001]
305 # new python: 28961/s
306 # old python: 18307/s
307 # re: 12820/s
308 # regex: 14035/s
309
310 def find_prefix_at_end(haystack, needle):
311 l = len(needle) - 1
312 while l and not haystack.endswith(needle[:l]):
313 l -= 1
314 return l