1
|
|
|
# -*- Mode: Python; tab-width: 4 -*- |
2
|
|
|
# $Id$ |
3
|
|
|
# Author: Sam Rushing <[email protected]> |
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
|
|
|
"""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
|
|
|
import string |
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 set_terminator (self, term): |
69
|
|
|
"Set the input delimiter. Can be a fixed string of any length, an integer, or None" |
70
|
|
|
self.terminator = term |
71
|
|
|
|
72
|
|
|
def get_terminator (self): |
73
|
|
|
return self.terminator |
74
|
|
|
|
75
|
|
|
# grab some more data from the socket, |
76
|
|
|
# throw it to the collector method, |
77
|
|
|
# check for the terminator, |
78
|
|
|
# if found, transition to the next state. |
79
|
|
|
|
80
|
|
|
def handle_read (self): |
81
|
|
|
|
82
|
|
|
try: |
83
|
|
|
data = self.recv (self.ac_in_buffer_size) |
84
|
|
|
except socket.error, why: |
85
|
|
|
self.handle_error() |
86
|
|
|
return |
87
|
|
|
|
88
|
|
|
self.ac_in_buffer = self.ac_in_buffer + data |
89
|
|
|
|
90
|
|
|
# Continue to search for self.terminator in self.ac_in_buffer, |
91
|
|
|
# while calling self.collect_incoming_data. The while loop |
92
|
|
|
# is necessary because we might read several data+terminator |
93
|
|
|
# combos with a single recv(1024). |
94
|
|
|
|
95
|
|
|
while self.ac_in_buffer: |
96
|
|
|
lb = len(self.ac_in_buffer) |
97
|
|
|
terminator = self.get_terminator() |
98
|
|
|
if terminator is None: |
99
|
|
|
# no terminator, collect it all |
100
|
|
|
self.collect_incoming_data (self.ac_in_buffer) |
101
|
|
|
self.ac_in_buffer = '' |
102
|
|
|
elif type(terminator) == type(0): |
103
|
|
|
# numeric terminator |
104
|
|
|
n = terminator |
105
|
|
|
if lb < n: |
106
|
|
|
self.collect_incoming_data (self.ac_in_buffer) |
107
|
|
|
self.ac_in_buffer = '' |
108
|
|
|
self.terminator = self.terminator - lb |
109
|
|
|
else: |
110
|
|
|
self.collect_incoming_data (self.ac_in_buffer[:n]) |
111
|
|
|
self.ac_in_buffer = self.ac_in_buffer[n:] |
112
|
|
|
self.terminator = 0 |
113
|
|
|
self.found_terminator() |
114
|
|
|
else: |
115
|
|
|
# 3 cases: |
116
|
|
|
# 1) end of buffer matches terminator exactly: |
117
|
|
|
# collect data, transition |
118
|
|
|
# 2) end of buffer matches some prefix: |
119
|
|
|
# collect data to the prefix |
120
|
|
|
# 3) end of buffer does not match any prefix: |
121
|
|
|
# collect data |
122
|
|
|
terminator_len = len(terminator) |
123
|
|
|
index = string.find (self.ac_in_buffer, terminator) |
124
|
|
|
if index != -1: |
125
|
|
|
# we found the terminator |
126
|
|
|
if index > 0: |
127
|
|
|
# don't bother reporting the empty string (source of subtle bugs) |
128
|
|
|
self.collect_incoming_data (self.ac_in_buffer[:index]) |
129
|
|
|
self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:] |
130
|
|
|
# This does the Right Thing if the terminator is changed here. |
131
|
|
|
self.found_terminator() |
132
|
|
|
else: |
133
|
|
|
# check for a prefix of the terminator |
134
|
|
|
index = find_prefix_at_end (self.ac_in_buffer, terminator) |
135
|
|
|
if index: |
136
|
|
|
if index != lb: |
137
|
|
|
# we found a prefix, collect up to the prefix |
138
|
|
|
self.collect_incoming_data (self.ac_in_buffer[:-index]) |
139
|
|
|
self.ac_in_buffer = self.ac_in_buffer[-index:] |
140
|
|
|
break |
141
|
|
|
else: |
142
|
|
|
# no prefix, collect it all |
143
|
|
|
self.collect_incoming_data (self.ac_in_buffer) |
144
|
|
|
self.ac_in_buffer = '' |
145
|
|
|
|
146
|
|
|
def handle_write (self): |
147
|
|
|
self.initiate_send () |
148
|
|
|
|
149
|
|
|
def handle_close (self): |
150
|
|
|
self.close() |
151
|
|
|
|
152
|
|
|
def push (self, data): |
153
|
|
|
self.producer_fifo.push (simple_producer (data)) |
154
|
|
|
self.initiate_send() |
155
|
|
|
|
156
|
|
|
def push_with_producer (self, producer): |
157
|
|
|
self.producer_fifo.push (producer) |
158
|
|
|
self.initiate_send() |
159
|
|
|
|
160
|
|
|
def readable (self): |
161
|
|
|
"predicate for inclusion in the readable for select()" |
162
|
|
|
return (len(self.ac_in_buffer) <= self.ac_in_buffer_size) |
163
|
|
|
|
164
|
|
|
def writable (self): |
165
|
|
|
"predicate for inclusion in the writable for select()" |
166
|
|
|
# return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected) |
167
|
|
|
# this is about twice as fast, though not as clear. |
168
|
|
|
return not ( |
169
|
|
|
(self.ac_out_buffer is '') and |
170
|
|
|
self.producer_fifo.is_empty() and |
171
|
|
|
self.connected |
172
|
|
|
) |
173
|
|
|
|
174
|
|
|
def close_when_done (self): |
175
|
|
|
"automatically close this channel once the outgoing queue is empty" |
176
|
|
|
self.producer_fifo.push (None) |
177
|
|
|
|
178
|
|
|
# refill the outgoing buffer by calling the more() method |
179
|
|
|
# of the first producer in the queue |
180
|
|
|
def refill_buffer (self): |
181
|
|
|
_string_type = type('') |
182
|
|
|
while 1: |
183
|
|
|
if len(self.producer_fifo): |
184
|
|
|
p = self.producer_fifo.first() |
185
|
|
|
# a 'None' in the producer fifo is a sentinel, |
186
|
|
|
# telling us to close the channel. |
187
|
|
|
if p is None: |
188
|
|
|
if not self.ac_out_buffer: |
189
|
|
|
self.producer_fifo.pop() |
190
|
|
|
self.close() |
191
|
|
|
return |
192
|
|
|
elif type(p) is _string_type: |
193
|
|
|
self.producer_fifo.pop() |
194
|
|
|
self.ac_out_buffer = self.ac_out_buffer + p |
195
|
|
|
return |
196
|
|
|
data = p.more() |
197
|
|
|
if data: |
198
|
|
|
self.ac_out_buffer = self.ac_out_buffer + data |
199
|
|
|
return |
200
|
|
|
else: |
201
|
|
|
self.producer_fifo.pop() |
202
|
|
|
else: |
203
|
|
|
return |
204
|
|
|
|
205
|
|
|
def initiate_send (self): |
206
|
|
|
obs = self.ac_out_buffer_size |
207
|
|
|
# try to refill the buffer |
208
|
|
|
if (len (self.ac_out_buffer) < obs): |
209
|
|
|
self.refill_buffer() |
210
|
|
|
|
211
|
|
|
if self.ac_out_buffer and self.connected: |
212
|
|
|
# try to send the buffer |
213
|
|
|
try: |
214
|
|
|
num_sent = self.send (self.ac_out_buffer[:obs]) |
215
|
|
|
if num_sent: |
216
|
|
|
self.ac_out_buffer = self.ac_out_buffer[num_sent:] |
217
|
|
|
|
218
|
|
|
except socket.error, why: |
219
|
|
|
self.handle_error() |
220
|
|
|
return |
221
|
|
|
|
222
|
|
|
def discard_buffers (self): |
223
|
|
|
# Emergencies only! |
224
|
|
|
self.ac_in_buffer = '' |
225
|
|
|
self.ac_out_buffer = '' |
226
|
|
|
while self.producer_fifo: |
227
|
|
|
self.producer_fifo.pop() |
228
|
|
|
|
229
|
|
|
|
230
|
|
View Code Duplication |
class simple_producer: |
|
|
|
|
231
|
|
|
|
232
|
|
|
def __init__ (self, data, buffer_size=512): |
233
|
|
|
self.data = data |
234
|
|
|
self.buffer_size = buffer_size |
235
|
|
|
|
236
|
|
|
def more (self): |
237
|
|
|
if len (self.data) > self.buffer_size: |
238
|
|
|
result = self.data[:self.buffer_size] |
239
|
|
|
self.data = self.data[self.buffer_size:] |
240
|
|
|
return result |
241
|
|
|
else: |
242
|
|
|
result = self.data |
243
|
|
|
self.data = '' |
244
|
|
|
return result |
245
|
|
|
|
246
|
|
|
class fifo: |
247
|
|
|
def __init__ (self, list=None): |
248
|
|
|
if not list: |
249
|
|
|
self.list = [] |
250
|
|
|
else: |
251
|
|
|
self.list = list |
252
|
|
|
|
253
|
|
|
def __len__ (self): |
254
|
|
|
return len(self.list) |
255
|
|
|
|
256
|
|
|
def is_empty (self): |
257
|
|
|
return self.list == [] |
258
|
|
|
|
259
|
|
|
def first (self): |
260
|
|
|
return self.list[0] |
261
|
|
|
|
262
|
|
|
def push (self, data): |
263
|
|
|
self.list.append (data) |
264
|
|
|
|
265
|
|
|
def pop (self): |
266
|
|
|
if self.list: |
267
|
|
|
result = self.list[0] |
268
|
|
|
del self.list[0] |
269
|
|
|
return (1, result) |
270
|
|
|
else: |
271
|
|
|
return (0, None) |
272
|
|
|
|
273
|
|
|
# Given 'haystack', see if any prefix of 'needle' is at its end. This |
274
|
|
|
# assumes an exact match has already been checked. Return the number of |
275
|
|
|
# characters matched. |
276
|
|
|
# for example: |
277
|
|
|
# f_p_a_e ("qwerty\r", "\r\n") => 1 |
278
|
|
|
# f_p_a_e ("qwertydkjf", "\r\n") => 0 |
279
|
|
|
# f_p_a_e ("qwerty\r\n", "\r\n") => <undefined> |
280
|
|
|
|
281
|
|
|
# this could maybe be made faster with a computed regex? |
282
|
|
|
# [answer: no; circa Python-2.0, Jan 2001] |
283
|
|
|
# new python: 28961/s |
284
|
|
|
# old python: 18307/s |
285
|
|
|
# re: 12820/s |
286
|
|
|
# regex: 14035/s |
287
|
|
|
|
288
|
|
|
def find_prefix_at_end (haystack, needle): |
289
|
|
|
l = len(needle) - 1 |
290
|
|
|
while l and not haystack.endswith(needle[:l]): |
291
|
|
|
l -= 1 |
292
|
|
|
return l |
293
|
|
|
|