quartz_torrent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/quartztorrent_download +127 -0
- data/bin/quartztorrent_download_curses +841 -0
- data/bin/quartztorrent_magnet_from_torrent +32 -0
- data/bin/quartztorrent_show_info +62 -0
- data/lib/quartz_torrent.rb +2 -0
- data/lib/quartz_torrent/bitfield.rb +314 -0
- data/lib/quartz_torrent/blockstate.rb +354 -0
- data/lib/quartz_torrent/classifiedpeers.rb +95 -0
- data/lib/quartz_torrent/extension.rb +37 -0
- data/lib/quartz_torrent/filemanager.rb +543 -0
- data/lib/quartz_torrent/formatter.rb +92 -0
- data/lib/quartz_torrent/httptrackerclient.rb +121 -0
- data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
- data/lib/quartz_torrent/log.rb +132 -0
- data/lib/quartz_torrent/magnet.rb +92 -0
- data/lib/quartz_torrent/memprofiler.rb +27 -0
- data/lib/quartz_torrent/metainfo.rb +221 -0
- data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
- data/lib/quartz_torrent/peer.rb +145 -0
- data/lib/quartz_torrent/peerclient.rb +1627 -0
- data/lib/quartz_torrent/peerholder.rb +123 -0
- data/lib/quartz_torrent/peermanager.rb +170 -0
- data/lib/quartz_torrent/peermsg.rb +502 -0
- data/lib/quartz_torrent/peermsgserialization.rb +102 -0
- data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
- data/lib/quartz_torrent/rate.rb +58 -0
- data/lib/quartz_torrent/ratelimit.rb +48 -0
- data/lib/quartz_torrent/reactor.rb +949 -0
- data/lib/quartz_torrent/regionmap.rb +124 -0
- data/lib/quartz_torrent/semaphore.rb +43 -0
- data/lib/quartz_torrent/trackerclient.rb +271 -0
- data/lib/quartz_torrent/udptrackerclient.rb +70 -0
- data/lib/quartz_torrent/udptrackermsg.rb +250 -0
- data/lib/quartz_torrent/util.rb +100 -0
- metadata +195 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'bencode'
|
2
|
+
require 'quartz_torrent/peermsg'
|
3
|
+
require 'quartz_torrent/extension'
|
4
|
+
|
5
|
+
module QuartzTorrent
|
6
|
+
class PeerWireMessageSerializer
|
7
|
+
@@classForMessage = nil
|
8
|
+
# The mapping of our extended message ids to extensions. This is different than @extendedMessageIdToClass which is
|
9
|
+
# the mapping of peer message ids to extensions, which is different for every peer.
|
10
|
+
@@classForExtendedMessage = nil
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
# extendedMessageIdToClass is the mapping of extended message ids that the peer has sent to extensions.
|
14
|
+
@extendedMessageIdToClass = [ExtendedHandshake]
|
15
|
+
@logger = LogManager.getLogger("peermsg_serializer")
|
16
|
+
end
|
17
|
+
|
18
|
+
def unserializeFrom(io)
|
19
|
+
packedLength = io.read(4)
|
20
|
+
raise EOFError.new if packedLength.length == 0
|
21
|
+
|
22
|
+
length = packedLength.unpack("N")[0]
|
23
|
+
@logger.debug "unserializeFrom: read that length of message is #{length}"
|
24
|
+
raise "Received peer message with length #{length}. All messages must have length >= 0" if length < 0
|
25
|
+
return KeepAlive.new if length == 0
|
26
|
+
|
27
|
+
id = io.read(1).unpack("C")[0]
|
28
|
+
@logger.debug "unserializeFrom: read message id #{id}"
|
29
|
+
payload = io.read(length-1)
|
30
|
+
|
31
|
+
#raise "Unsupported peer message id #{id}" if id >= self.classForMessage.length
|
32
|
+
clazz = classForMessage(id, payload)
|
33
|
+
raise "Unsupported peer message id #{id}" if ! clazz
|
34
|
+
|
35
|
+
result = clazz.new
|
36
|
+
result.unserialize(payload)
|
37
|
+
updateExtendedMessageIdsFromHandshake(result)
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def serializeTo(msg, io)
|
42
|
+
if msg.is_a?(Extended)
|
43
|
+
# Set the extended message id
|
44
|
+
extendedMsgId = @extendedMessageIdToClass.index msg.class
|
45
|
+
raise "Unsupported extended peer message #{msg.class}" if ! extendedMsgId
|
46
|
+
msg.extendedMessageId = extendedMsgId
|
47
|
+
end
|
48
|
+
msg.serializeTo(io)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
# Determine the class associated with the message type passed.
|
53
|
+
def classForMessage(id, payload)
|
54
|
+
if @@classForMessage.nil?
|
55
|
+
@@classForMessage = [Choke, Unchoke, Interested, Uninterested, Have, BitfieldMessage, Request, Piece, Cancel]
|
56
|
+
@@classForMessage[20] = Extended
|
57
|
+
end
|
58
|
+
|
59
|
+
if @@classForExtendedMessage.nil?
|
60
|
+
@@classForExtendedMessage = []
|
61
|
+
@@classForExtendedMessage[Extension::MetadataExtensionId] = ExtendedMetaInfo
|
62
|
+
end
|
63
|
+
|
64
|
+
result = @@classForMessage[id]
|
65
|
+
|
66
|
+
if result == Extended && payload
|
67
|
+
# Extended messages have further subtypes.
|
68
|
+
extendedMsgId = payload.unpack("C")[0]
|
69
|
+
if extendedMsgId == 0
|
70
|
+
result = ExtendedHandshake
|
71
|
+
else
|
72
|
+
# In this case the extended message number is the one we told our peers to use, not the one the peer told us.
|
73
|
+
result = @@classForExtendedMessage[extendedMsgId]
|
74
|
+
raise "Unsupported extended peer message id '#{extendedMsgId}'" if ! result
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
def updateExtendedMessageIdsFromHandshake(msg)
|
83
|
+
if msg.is_a?(ExtendedHandshake)
|
84
|
+
if msg.dict && msg.dict["m"]
|
85
|
+
msg.dict["m"].each do |extName, extId|
|
86
|
+
# Update the list here.
|
87
|
+
clazz = Extension.peerMsgClassForExtensionName(extName)
|
88
|
+
if clazz
|
89
|
+
@logger.debug "Peer supports extension #{extName} using id '#{extId}'."
|
90
|
+
@extendedMessageIdToClass[extId] = clazz
|
91
|
+
else
|
92
|
+
@logger.warn "Peer supports extension #{extName} using id '#{extId}', but I don't know what class to use for that extension."
|
93
|
+
end
|
94
|
+
end
|
95
|
+
else
|
96
|
+
@logger.warn "Peer sent extended handshake without the 'm' key."
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
|
3
|
+
# Metadata associated with outstanding requests to the PieceManager (asynchronous IO management).
|
4
|
+
class PieceManagerRequestMetadata
|
5
|
+
def initialize(type, data)
|
6
|
+
@type = type
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
attr_accessor :type
|
10
|
+
attr_accessor :data
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
# Class that keeps track of a rate, for example a download or upload rate.
|
3
|
+
# The class is used by calling 'update' with samples (numbers) representing
|
4
|
+
# the amount of units being measured accrued since the last call, and value
|
5
|
+
# returns the rate in units/second.
|
6
|
+
#
|
7
|
+
# This is implemented as an exponential moving average. The weight of the
|
8
|
+
# current sample is based on an exponention function of the time since the
|
9
|
+
# last sample. To reduce CPU usage, when update is called with a new sample
|
10
|
+
# the new average is not calculated immediately, but instead the samples are
|
11
|
+
# summed until 1 second has elapsed before recalculating the average.
|
12
|
+
class Rate
|
13
|
+
# Create a new Rate that measures the rate using samples.
|
14
|
+
# avgPeriod specifies the duration over which the samples are averaged.
|
15
|
+
def initialize(avgPeriod = 4.0)
|
16
|
+
reset
|
17
|
+
@avgPeriod = avgPeriod.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the current rate. If there are too few samples, 0 is returned.
|
21
|
+
def value
|
22
|
+
update 0
|
23
|
+
@value ? @value : 0.0
|
24
|
+
end
|
25
|
+
|
26
|
+
# Update the rate by passing another sample (number) representing units accrued since the last
|
27
|
+
# call to update.
|
28
|
+
def update(sample)
|
29
|
+
now = Time.new
|
30
|
+
elapsed = now - @time
|
31
|
+
@sum += sample
|
32
|
+
if elapsed > 1.0
|
33
|
+
@value = newValue elapsed, @sum/elapsed
|
34
|
+
@time = now
|
35
|
+
@sum = 0.0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Reset the rate to empty.
|
40
|
+
def reset
|
41
|
+
@value = nil
|
42
|
+
@time = Time.new
|
43
|
+
@sum = 0.0
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def newValue(elapsed, sample)
|
48
|
+
return sample if ! @value
|
49
|
+
a = alpha elapsed
|
50
|
+
a*sample + (1-a)*@value
|
51
|
+
end
|
52
|
+
|
53
|
+
# See http://en.wikipedia.org/wiki/Moving_average#Application_to_measuring_computer_performance
|
54
|
+
def alpha(elapsed)
|
55
|
+
1 - Math.exp(-elapsed/@avgPeriod)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
|
3
|
+
# This class can be used to limit the rate at which work is done.
|
4
|
+
class RateLimit
|
5
|
+
# unitsPerSecond: Each second this many units are added to a pool. At any time
|
6
|
+
# up to the number of units in the pool may be withdrawn.
|
7
|
+
# upperLimit: The maximum size of the pool. This controls how bursty the rate is.
|
8
|
+
# For example, if the rate is 1/s and the limit is 5, then if there was no withdrawals
|
9
|
+
# for 5 seconds, then the max pool size is reached, and the next withdrawal may be 5, meaning
|
10
|
+
# 5 units could be used instantaneously. However the average for the last 5 seconds is still 1/s.
|
11
|
+
# initialValue: Initial size of the pool.
|
12
|
+
def initialize(unitsPerSecond, upperLimit, initialValue)
|
13
|
+
@unitsPerSecond = unitsPerSecond.to_f
|
14
|
+
@upperLimit = upperLimit.to_f
|
15
|
+
@initialValue = initialValue.to_f
|
16
|
+
@pool = @initialValue
|
17
|
+
@time = Time.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return the limit in units per second
|
21
|
+
attr_reader :unitsPerSecond
|
22
|
+
|
23
|
+
# Set the limit in units per second.
|
24
|
+
def unitsPerSecond=(v)
|
25
|
+
@unitsPerSecond = v.to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
# How much is in the pool.
|
29
|
+
def avail
|
30
|
+
updatePool
|
31
|
+
@pool
|
32
|
+
end
|
33
|
+
|
34
|
+
# Withdraw this much from the pool.
|
35
|
+
def withdraw(n)
|
36
|
+
updatePool
|
37
|
+
@pool -= n
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def updatePool
|
42
|
+
now = Time.new
|
43
|
+
@pool = @pool + (now - @time)*@unitsPerSecond
|
44
|
+
@pool = @upperLimit if @pool > @upperLimit
|
45
|
+
@time = now
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,949 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'pqueue'
|
3
|
+
require 'fiber'
|
4
|
+
require 'thread'
|
5
|
+
require 'quartz_torrent/ratelimit'
|
6
|
+
include Socket::Constants
|
7
|
+
|
8
|
+
module QuartzTorrent
|
9
|
+
|
10
|
+
# Callers must subclass this class to use the reactor. The event handler methods should be
|
11
|
+
# overridden. For data-oriented event handler methods, the functions write and read are available
|
12
|
+
# to access the current io, as well as the method currentIo. Close can be called from
|
13
|
+
# event handlers to close the current io.
|
14
|
+
class Handler
|
15
|
+
# Event handler. An IO object has been successfully initialized.
|
16
|
+
# For example, a connect call has completed
|
17
|
+
def clientInit(metainfo)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Event handler. A peer has connected to the listening socket
|
21
|
+
def serverInit(metainfo, addr, port)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Event handler. The current io is ready for reading
|
25
|
+
# If you will write to the same io from both this handler and the timerExpired handler,
|
26
|
+
# you must make sure to perform all writing at once in this handler. If not then
|
27
|
+
# the writes from the timer handler may be interleaved.
|
28
|
+
#
|
29
|
+
# For example if the recvData handler performs:
|
30
|
+
#
|
31
|
+
# 1. read 5 bytes
|
32
|
+
# 2. write 5 bytes
|
33
|
+
# 3. read 5 bytes
|
34
|
+
# 4. write 5 bytes
|
35
|
+
#
|
36
|
+
# and the writes in 2 and 4 are meant to be one message (say mesage 2 is the length, and message 4 is the body)
|
37
|
+
# then this can occur:
|
38
|
+
# recvData reads 5 bytes, writes 5 bytes, tries to read 5 more bytes and is blocked
|
39
|
+
# timerExpired writes 5 bytes
|
40
|
+
# recvData continues; reads the 5 bytes and writes 5 bytes.
|
41
|
+
#
|
42
|
+
# Now the timerExpired data was written interleaved.
|
43
|
+
def recvData(metainfo)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Event handler. A timer has expired
|
47
|
+
def timerExpired(metainfo)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Event handler. Error during read, or write. Connection errors are reported separately in connectError
|
51
|
+
def error(metainfo, details)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Event handler. Error during connection, or connection timed out.
|
55
|
+
def connectError(metainfo, details)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Event handler. This is called for events added using addUserEvent to the reactor.
|
59
|
+
def userEvent(event)
|
60
|
+
end
|
61
|
+
|
62
|
+
### Methods not meant to be overridden
|
63
|
+
attr_accessor :reactor
|
64
|
+
|
65
|
+
# Schedule a timer.
|
66
|
+
def scheduleTimer(duration, metainfo = nil, recurring = true, immed = false)
|
67
|
+
@reactor.scheduleTimer(duration, metainfo, recurring, immed) if @reactor
|
68
|
+
end
|
69
|
+
|
70
|
+
# Cancel a timer scheduled with scheduleTimer.
|
71
|
+
def cancelTimer(timerInfo)
|
72
|
+
return if ! timerInfo
|
73
|
+
@reactor.cancelTimer(timerInfo) if @reactor
|
74
|
+
end
|
75
|
+
|
76
|
+
def connect(addr, port, metainfo, timeout = nil)
|
77
|
+
@reactor.connect(addr, port, metainfo, timeout) if @reactor
|
78
|
+
end
|
79
|
+
|
80
|
+
# Write data to the current io
|
81
|
+
def write(data)
|
82
|
+
@reactor.write(data) if @reactor
|
83
|
+
end
|
84
|
+
|
85
|
+
# Read len bytes from the current io
|
86
|
+
def read(len)
|
87
|
+
result = ''
|
88
|
+
result = @reactor.read(len) if @reactor
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
# Shutdown the reactor
|
93
|
+
def stopReactor
|
94
|
+
@reactor.stop if @reactor
|
95
|
+
end
|
96
|
+
|
97
|
+
# Check if stop has been called on the reactor
|
98
|
+
def stopped?
|
99
|
+
@stopped
|
100
|
+
end
|
101
|
+
|
102
|
+
# Close the current io
|
103
|
+
def close(io = nil)
|
104
|
+
@reactor.close(io) if @reactor
|
105
|
+
end
|
106
|
+
|
107
|
+
def currentIo
|
108
|
+
result = nil
|
109
|
+
result = @reactor.currentIo if @reactor
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
# Find an io by metainfo
|
114
|
+
def findIoByMetainfo(metainfo)
|
115
|
+
@reactor.findIoByMetainfo metainfo if metainfo && @reactor
|
116
|
+
end
|
117
|
+
|
118
|
+
def setMetaInfo(metainfo)
|
119
|
+
@reactor.setMetaInfo metainfo if @reactor
|
120
|
+
end
|
121
|
+
|
122
|
+
# Set the max rate at which the current IO can be read. The parameter should be a RateLimit object.
|
123
|
+
def setReadRateLimit(rateLimit)
|
124
|
+
@reactor.setReadRateLimit rateLimit if @reactor
|
125
|
+
end
|
126
|
+
|
127
|
+
# Set the max rate at which the current IO can be written to. The parameter should be a RateLimit object.
|
128
|
+
def setWriteRateLimit(rateLimit)
|
129
|
+
@reactor.setWriteRateLimit rateLimit if @reactor
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class OutputBuffer
|
134
|
+
# Create a new OutputBuffer for the specified IO. The parameter seekable should be
|
135
|
+
# true or false. If true, then this output buffer will support seek
|
136
|
+
# at the cost of performance.
|
137
|
+
def initialize(io, seekable = false)
|
138
|
+
@io = io
|
139
|
+
@seekable = seekable
|
140
|
+
if seekable
|
141
|
+
@buffer = []
|
142
|
+
else
|
143
|
+
@buffer = ''
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def empty?
|
148
|
+
@buffer.length == 0
|
149
|
+
end
|
150
|
+
|
151
|
+
def size
|
152
|
+
@buffer.length
|
153
|
+
end
|
154
|
+
|
155
|
+
def append(data)
|
156
|
+
if ! @seekable
|
157
|
+
@buffer << data
|
158
|
+
else
|
159
|
+
@buffer.push [@io.tell, data]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Write the contents of the output buffer to the io. This method throws all of the exceptions that write would throw
|
164
|
+
# (EAGAIN, EWOULDBLOCK, etc)
|
165
|
+
# If max is specified and this is a non-seekable io, at most that many bytes are written.
|
166
|
+
def flush(max = nil)
|
167
|
+
if ! @seekable
|
168
|
+
toWrite = @buffer.length
|
169
|
+
toWrite = max if max && max < @buffer.length
|
170
|
+
numWritten = 0
|
171
|
+
while toWrite > 0
|
172
|
+
numWritten = @io.write_nonblock(@buffer[0,toWrite])
|
173
|
+
raise Errno::EAGAIN if numWritten == 0
|
174
|
+
@buffer = @buffer[numWritten,@buffer.length]
|
175
|
+
toWrite -= numWritten
|
176
|
+
end
|
177
|
+
else
|
178
|
+
while @buffer.length > 0
|
179
|
+
@io.seek @buffer.first[0], IO::SEEK_SET
|
180
|
+
while @buffer.first[1].length > 0
|
181
|
+
numWritten = @io.write_nonblock(@buffer.first[1])
|
182
|
+
raise Errno::EAGAIN if numWritten == 0
|
183
|
+
@buffer.first[1] = @buffer.first[1][numWritten,@buffer.first[1].length]
|
184
|
+
end
|
185
|
+
# This chunk has been fully written. Remove it
|
186
|
+
@buffer.shift
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# This class provides a facade for an IO object. The read method
|
193
|
+
# on this object acts as a blocking read. Internally it reads
|
194
|
+
# nonblockingly and passes processing to other ready sockets
|
195
|
+
# if this socket would block.
|
196
|
+
class IoFacade
|
197
|
+
def initialize(ioInfo, logger = nil)
|
198
|
+
@ioInfo = ioInfo
|
199
|
+
@io = ioInfo.io
|
200
|
+
@logger = logger
|
201
|
+
end
|
202
|
+
|
203
|
+
attr_accessor :logger
|
204
|
+
|
205
|
+
# Method needed for disposeIo to work without breaking encapsulation of
|
206
|
+
# WriteOnlyIoFacade.
|
207
|
+
def removeFromIOHash(hash)
|
208
|
+
hash.delete @io
|
209
|
+
end
|
210
|
+
|
211
|
+
def read(length)
|
212
|
+
data = ''
|
213
|
+
while data.length < length
|
214
|
+
begin
|
215
|
+
toRead = length-data.length
|
216
|
+
rateLimited = false
|
217
|
+
if @ioInfo.readRateLimit
|
218
|
+
avail = @ioInfo.readRateLimit.avail.to_i
|
219
|
+
if avail < toRead
|
220
|
+
toRead = avail
|
221
|
+
rateLimited = true
|
222
|
+
end
|
223
|
+
@ioInfo.readRateLimit.withdraw toRead
|
224
|
+
end
|
225
|
+
@logger.debug "IoFacade: must read: #{length} have read: #{data.length}. Reading #{toRead} bytes now" if @logger
|
226
|
+
data << @io.read_nonblock(toRead) if toRead > 0
|
227
|
+
# If we tried to read more than we are allowed to by rate limiting, yield.
|
228
|
+
Fiber.yield if rateLimited
|
229
|
+
rescue Errno::EWOULDBLOCK
|
230
|
+
# Wait for more data.
|
231
|
+
@logger.debug "IoFacade: read would block" if @logger
|
232
|
+
Fiber.yield
|
233
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
234
|
+
# Wait for more data.
|
235
|
+
@logger.debug "IoFacade: read was interrupted" if @logger
|
236
|
+
Fiber.yield
|
237
|
+
rescue
|
238
|
+
@logger.debug "IoFacade: read error: #{$!}" if @logger
|
239
|
+
# Read failure occurred
|
240
|
+
@ioInfo.lastReadError = $!
|
241
|
+
if @ioInfo.useErrorhandler
|
242
|
+
@ioInfo.state = :error
|
243
|
+
Fiber.yield
|
244
|
+
else
|
245
|
+
raise $!
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
data
|
250
|
+
end
|
251
|
+
|
252
|
+
def write(data)
|
253
|
+
# Issue: what about write, read, read on files opened for read/write? Write should happen at offset X, but reads moved to offset N. Since writes
|
254
|
+
# are buffered, write may happen after read which means write will happen at the wrong offset N. Can fix by always seeking (if needed) before writes to the
|
255
|
+
# position where the write was queued, but this should only be done for files since other fds don't support seek. This is satisfactory for files opened
|
256
|
+
# for write only since the file position will be where we expect so we won't need to seek. Same for read only.
|
257
|
+
@ioInfo.outputBuffer.append data
|
258
|
+
@logger.debug "wrote #{data.length} bytes to the output buffer of IO metainfo=#{@ioInfo.metainfo}" if @logger
|
259
|
+
data.length
|
260
|
+
end
|
261
|
+
|
262
|
+
def seek(amount, whence)
|
263
|
+
@io.seek amount, whence if @ioInfo.seekable?
|
264
|
+
end
|
265
|
+
|
266
|
+
def flush
|
267
|
+
@io.flush
|
268
|
+
end
|
269
|
+
|
270
|
+
def close
|
271
|
+
@io.close
|
272
|
+
end
|
273
|
+
|
274
|
+
def closed?
|
275
|
+
@io.closed?
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# An IoFacade that doesn't allow reading.
|
280
|
+
class WriteOnlyIoFacade < IoFacade
|
281
|
+
def initialize(ioInfo, logger = nil, readError = "Reading is not allowed for this IO")
|
282
|
+
super(ioInfo, logger)
|
283
|
+
@readError = readError
|
284
|
+
end
|
285
|
+
|
286
|
+
def read(length)
|
287
|
+
raise @readError
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# An IO and associated meta-information used by the Reactor.
|
292
|
+
class IOInfo
|
293
|
+
def initialize(io, metainfo, seekable = false)
|
294
|
+
@io = io
|
295
|
+
@metainfo = metainfo
|
296
|
+
@readFiber = nil
|
297
|
+
@readFiberIoFacade = IoFacade.new(self)
|
298
|
+
@lastReadError = nil
|
299
|
+
@connectTimer = nil
|
300
|
+
@seekable = seekable
|
301
|
+
@outputBuffer = OutputBuffer.new(@io, seekable)
|
302
|
+
@useErrorhandler = true
|
303
|
+
@readRateLimit = nil
|
304
|
+
@writeRateLimit = nil
|
305
|
+
end
|
306
|
+
attr_accessor :io
|
307
|
+
attr_accessor :metainfo
|
308
|
+
attr_accessor :state
|
309
|
+
attr_accessor :lastReadError
|
310
|
+
attr_accessor :connectTimeout
|
311
|
+
attr_accessor :outputBuffer
|
312
|
+
attr_accessor :readFiber
|
313
|
+
attr_accessor :readFiberIoFacade
|
314
|
+
attr_accessor :connectTimer
|
315
|
+
attr_accessor :useErrorhandler
|
316
|
+
attr_accessor :readRateLimit
|
317
|
+
attr_accessor :writeRateLimit
|
318
|
+
def seekable?
|
319
|
+
@seekable
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Class used to manage timers.
|
324
|
+
class TimerManager
|
325
|
+
class TimerInfo
|
326
|
+
def initialize(duration, recurring, metainfo)
|
327
|
+
@duration = duration
|
328
|
+
@recurring = recurring
|
329
|
+
@metainfo = metainfo
|
330
|
+
@cancelled = false
|
331
|
+
refresh
|
332
|
+
end
|
333
|
+
attr_accessor :recurring
|
334
|
+
attr_accessor :duration
|
335
|
+
attr_accessor :expiry
|
336
|
+
attr_accessor :metainfo
|
337
|
+
# Since pqueue doesn't allow removal of anything but the head
|
338
|
+
# we flag deleted items so they are deleted when they are pulled
|
339
|
+
attr_accessor :cancelled
|
340
|
+
|
341
|
+
def secondsUntilExpiry
|
342
|
+
@expiry - Time.new
|
343
|
+
end
|
344
|
+
|
345
|
+
def refresh
|
346
|
+
@expiry = Time.new + @duration
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def initialize(logger = nil)
|
351
|
+
@queue = PQueue.new { |a,b| b.expiry <=> a.expiry }
|
352
|
+
@mutex = Mutex.new
|
353
|
+
@logger = logger
|
354
|
+
end
|
355
|
+
|
356
|
+
# Add a timer. Parameter 'duration' specifies the timer duration in seconds,
|
357
|
+
# 'metainfo' is caller information passed to the handler when the timer expires,
|
358
|
+
# 'recurring' should be true if the timer will repeat, or false if it will only
|
359
|
+
# expire once, and 'immed' when true specifies that the timer should expire immediately
|
360
|
+
# (and again each duration if recurring) while false specifies that the timer will only
|
361
|
+
# expire the first time after it's duration elapses.
|
362
|
+
def add(duration, metainfo = nil, recurring = true, immed = false)
|
363
|
+
raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
|
364
|
+
info = TimerInfo.new(duration, recurring, metainfo)
|
365
|
+
info.expiry = Time.new if immed
|
366
|
+
@mutex.synchronize{ @queue.push info }
|
367
|
+
info
|
368
|
+
end
|
369
|
+
|
370
|
+
# Cancel a timer.
|
371
|
+
def cancel(timerInfo)
|
372
|
+
timerInfo.cancelled = true
|
373
|
+
end
|
374
|
+
|
375
|
+
# Return the next timer event from the queue, but don't remove it from the queue.
|
376
|
+
def peek
|
377
|
+
clearCancelled
|
378
|
+
@queue.top
|
379
|
+
end
|
380
|
+
|
381
|
+
# Remove the next timer event from the queue and return it as a TimerHandler::TimerInfo object.
|
382
|
+
def next
|
383
|
+
clearCancelled
|
384
|
+
result = nil
|
385
|
+
@mutex.synchronize{ result = @queue.pop }
|
386
|
+
if result && result.recurring
|
387
|
+
result.refresh
|
388
|
+
@mutex.synchronize{ @queue.push result }
|
389
|
+
end
|
390
|
+
result
|
391
|
+
end
|
392
|
+
|
393
|
+
private
|
394
|
+
|
395
|
+
def clearCancelled
|
396
|
+
while @queue.top && @queue.top.cancelled
|
397
|
+
info = @queue.pop
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# This class implements the Reactor pattern. The Reactor listens for activity on IO objects and calls methods on
|
403
|
+
# an associated Handler object when activity is detected. Callers can use listen, connect or open to register IO
|
404
|
+
# objects with the reactor.
|
405
|
+
#
|
406
|
+
# This Reactor is implemented using Fibers in such a way that when activity is defected on an IO, the handler
|
407
|
+
# can perform reads of N bytes without blocking and without needing to buffer. For example, the handler
|
408
|
+
# may call:
|
409
|
+
#
|
410
|
+
# msg = io.read(300)
|
411
|
+
#
|
412
|
+
# when it knows it must read 300 bytes. If only 100 are available, the handler is cooperatively preempted and
|
413
|
+
# later resumed when more bytes are available, so that the read seems atomic while also not blocking.
|
414
|
+
class Reactor
|
415
|
+
class InternalTimerInfo
|
416
|
+
def initialize(type, data)
|
417
|
+
@type = type
|
418
|
+
@data = data
|
419
|
+
end
|
420
|
+
attr_accessor :type
|
421
|
+
attr_accessor :data
|
422
|
+
end
|
423
|
+
|
424
|
+
# Create a new reactor that uses the passed hander.
|
425
|
+
def initialize(handler, logger = nil)
|
426
|
+
raise "Reactor.new called with nil handler. Handler can't be nil" if handler.nil?
|
427
|
+
|
428
|
+
@stopped = false
|
429
|
+
@handler = handler
|
430
|
+
@handler.reactor = self
|
431
|
+
|
432
|
+
# Hash of IOInfo objects, keyed by io.
|
433
|
+
@ioInfo = {}
|
434
|
+
@timerManager = TimerManager.new(logger)
|
435
|
+
@currentIoInfo = nil
|
436
|
+
@logger = logger
|
437
|
+
@listenBacklog = 10
|
438
|
+
@eventRead, @eventWrite = IO.pipe
|
439
|
+
@currentEventPipeChars = 0
|
440
|
+
@currentHandlerCallback = nil
|
441
|
+
@userEvents = []
|
442
|
+
end
|
443
|
+
|
444
|
+
attr_accessor :listenBacklog
|
445
|
+
|
446
|
+
# Returns true if the reactor is stopped
|
447
|
+
def stopped?
|
448
|
+
@stopped
|
449
|
+
end
|
450
|
+
|
451
|
+
# Create a TCP connection to the specified host
|
452
|
+
def connect(addr, port, metainfo, timeout = nil)
|
453
|
+
ioInfo = startConnection(port, addr, metainfo)
|
454
|
+
@ioInfo[ioInfo.io] = ioInfo
|
455
|
+
if timeout && ioInfo.state == :connecting
|
456
|
+
ioInfo.connectTimeout = timeout
|
457
|
+
ioInfo.connectTimer = scheduleTimer(timeout, InternalTimerInfo.new(:connect_timeout, ioInfo), false)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Create a TCP server that listens for connections on the specified
|
462
|
+
# port
|
463
|
+
def listen(addr, port, metainfo)
|
464
|
+
listener = Socket.new( AF_INET, SOCK_STREAM, 0 )
|
465
|
+
sockaddr = Socket.pack_sockaddr_in( port, "0.0.0.0" )
|
466
|
+
listener.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
|
467
|
+
listener.bind( sockaddr )
|
468
|
+
@logger.debug "listening on port #{port}" if @logger
|
469
|
+
listener.listen( @listenBacklog )
|
470
|
+
|
471
|
+
info = IOInfo.new(listener, metainfo)
|
472
|
+
info.readFiberIoFacade.logger = @logger if @logger
|
473
|
+
info.state = :listening
|
474
|
+
@ioInfo[info.io] = info
|
475
|
+
end
|
476
|
+
|
477
|
+
# Open the specified file for the specified mode.
|
478
|
+
def open(path, mode, metainfo, useErrorhandler = true)
|
479
|
+
file = File.open(path, mode)
|
480
|
+
|
481
|
+
info = IOInfo.new(file, metainfo, true)
|
482
|
+
info.useErrorhandler = useErrorhandler
|
483
|
+
info.readFiberIoFacade.logger = @logger if @logger
|
484
|
+
info.state = :connected
|
485
|
+
@ioInfo[info.io] = info
|
486
|
+
end
|
487
|
+
|
488
|
+
# Add a generic event. This event will be processed the next pass through the
|
489
|
+
# event loop
|
490
|
+
def addUserEvent(event)
|
491
|
+
@userEvents.push event
|
492
|
+
end
|
493
|
+
|
494
|
+
# Run event loop
|
495
|
+
def start
|
496
|
+
while true
|
497
|
+
begin
|
498
|
+
break if eventLoopBody == :halt
|
499
|
+
rescue
|
500
|
+
@logger.error "Unexpected exception in reactor event loop: #{$!}" if @logger
|
501
|
+
@logger.error $!.backtrace.join "\n" if @logger
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
@logger.info "Reactor shutting down" if @logger
|
506
|
+
|
507
|
+
# Event loop finished
|
508
|
+
@ioInfo.each do |k,v|
|
509
|
+
k.close
|
510
|
+
end
|
511
|
+
|
512
|
+
end
|
513
|
+
|
514
|
+
# Stop the event loop.
|
515
|
+
def stop
|
516
|
+
@stopped = true
|
517
|
+
return if @currentEventPipeChars > 0
|
518
|
+
@eventWrite.write 'x'
|
519
|
+
@currentEventPipeChars += 1
|
520
|
+
@eventWrite.flush
|
521
|
+
end
|
522
|
+
|
523
|
+
# Schedule a timer. Parameter 'duration' specifies the timer duration in seconds,
|
524
|
+
# 'metainfo' is caller information passed to the handler when the timer expires,
|
525
|
+
# 'recurring' should be true if the timer will repeat, or false if it will only
|
526
|
+
# expire once, and 'immed' when true specifies that the timer should expire immediately
|
527
|
+
# (and again each duration if recurring) while false specifies that the timer will only
|
528
|
+
# expire the first time after it's duration elapses.
|
529
|
+
def scheduleTimer(duration, metainfo = nil, recurring = true, immed = false)
|
530
|
+
timerInfo = @timerManager.add(duration, metainfo, recurring, immed)
|
531
|
+
# This timer may expire sooner than the current sleep we are doing in select(). To make
|
532
|
+
# sure we will write to the event pipe to break out of select().
|
533
|
+
if @currentEventPipeChars == 0
|
534
|
+
@eventWrite.write 'x'
|
535
|
+
@currentEventPipeChars += 1
|
536
|
+
@eventWrite.flush
|
537
|
+
end
|
538
|
+
timerInfo
|
539
|
+
end
|
540
|
+
|
541
|
+
# Meant to be called from the handler. Cancel the timer scheduled with scheduleTimer
|
542
|
+
def cancelTimer(timerInfo)
|
543
|
+
@timerManager.cancel timerInfo
|
544
|
+
end
|
545
|
+
|
546
|
+
# Meant to be called from the handler. Adds the specified data to the outgoing queue for the current io
|
547
|
+
def write(data)
|
548
|
+
if @currentIoInfo
|
549
|
+
# This is meant to be called from inside a fiber. Should add a check to confirm that here.
|
550
|
+
@currentIoInfo.readFiberIoFacade.write(data)
|
551
|
+
else
|
552
|
+
raise "Reactor.write called with no current io. Was it called from a timer handler?"
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
# Meant to be called from the handler. Read 'len' bytes from the current IO.
|
557
|
+
def read(len)
|
558
|
+
if @currentIoInfo
|
559
|
+
# This is meant to be called from inside a fiber. Should add a check to confirm that here.
|
560
|
+
@currentIoInfo.readFiberIoFacade.read(len)
|
561
|
+
else
|
562
|
+
raise "Reactor.read called with no current io. Was it called from a timer handler?"
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
# Meant to be called from the handler. Closes the passed io, or if it's nil, closes the current io
|
567
|
+
def close(io = nil)
|
568
|
+
if ! io
|
569
|
+
disposeIo @currentIoInfo if @currentIoInfo
|
570
|
+
else
|
571
|
+
disposeIo io
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
# Meant to be called from the handler. Returns the current io
|
576
|
+
def currentIo
|
577
|
+
@currentIoInfo.readFiberIoFacade
|
578
|
+
end
|
579
|
+
|
580
|
+
# Meant to be called from the handler. Sets the meta info for the current io
|
581
|
+
def setMetaInfo(metainfo)
|
582
|
+
@currentIoInfo.metainfo = metainfo
|
583
|
+
end
|
584
|
+
|
585
|
+
# Meant to be called from the handler. Sets the max rate at which the current io can read.
|
586
|
+
def setReadRateLimit(rateLimit)
|
587
|
+
@currentIoInfo.readRateLimit = rateLimit
|
588
|
+
end
|
589
|
+
|
590
|
+
# Meant to be called from the handler. Sets the max rate at which the current io can be written to.
|
591
|
+
def setWriteRateLimit(rateLimit)
|
592
|
+
@currentIoInfo.writeRateLimit = rateLimit
|
593
|
+
end
|
594
|
+
|
595
|
+
# Meant to be called from the handler. Find an IO by metainfo. The == operator is used to
|
596
|
+
# match the metainfo.
|
597
|
+
def findIoByMetainfo(metainfo)
|
598
|
+
@ioInfo.each_value do |info|
|
599
|
+
if info.metainfo == metainfo
|
600
|
+
io = info.readFiberIoFacade
|
601
|
+
# Don't allow read calls from timer handlers. This is to prevent a complex situation.
|
602
|
+
# See the processTimer call in eventLoopBody for more info
|
603
|
+
io = WriteOnlyIoFacade.new(info) if @currentHandlerCallback == :timer
|
604
|
+
return io
|
605
|
+
end
|
606
|
+
end
|
607
|
+
nil
|
608
|
+
end
|
609
|
+
|
610
|
+
private
|
611
|
+
|
612
|
+
# Returns :continue or :halt to the caller, specifying whether to continue the event loop or halt.
|
613
|
+
def eventLoopBody
|
614
|
+
# 1. Check timers
|
615
|
+
timer, selectTimeout = processTimers
|
616
|
+
|
617
|
+
readset = []
|
618
|
+
writeset = []
|
619
|
+
outputBufferNotEmptyCount = 0
|
620
|
+
ioToRemove = []
|
621
|
+
@ioInfo.each do |k,v|
|
622
|
+
if k.closed?
|
623
|
+
ioToRemove.push k
|
624
|
+
next
|
625
|
+
end
|
626
|
+
readset.push k if v.state != :connecting && ! @stopped && (v.readRateLimit.nil? || v.readRateLimit.avail >= 1.0)
|
627
|
+
@logger.debug "eventloop: IO metainfo=#{v.metainfo} added to read set" if @logger
|
628
|
+
writeset.push k if (!v.outputBuffer.empty? || v.state == :connecting) && v.state != :listening && (v.writeRateLimit.nil? || v.writeRateLimit.avail >= 1.0)
|
629
|
+
@logger.debug "eventloop: IO metainfo=#{v.metainfo} added to write set" if @logger
|
630
|
+
outputBufferNotEmptyCount += 1 if !v.outputBuffer.empty?
|
631
|
+
end
|
632
|
+
readset.push @eventRead
|
633
|
+
|
634
|
+
# Only exit the event loop once we've written all pending data.
|
635
|
+
return :halt if @stopped && outputBufferNotEmptyCount == 0
|
636
|
+
|
637
|
+
# 2. Check user events
|
638
|
+
@userEvents.each{ |event| @handler.userEvent event } if ! @stopped
|
639
|
+
|
640
|
+
# 3. Call Select. Ignore exception set: apparently this is for OOB data, or terminal things.
|
641
|
+
selectResult = nil
|
642
|
+
while true
|
643
|
+
begin
|
644
|
+
if readset.length > 1024 || writeset.length > 1024
|
645
|
+
@logger.error "Too many file descriptors to pass to select! Trimming them. Some fds may starve!" if @logger
|
646
|
+
readset = readset.first(1024)
|
647
|
+
writeset = writeset.first(1024)
|
648
|
+
end
|
649
|
+
@logger.debug "eventloop: Calling select" if @logger
|
650
|
+
selectResult = IO.select(readset, writeset, nil, selectTimeout)
|
651
|
+
@logger.debug "eventloop: select done. result: #{selectResult.inspect}" if @logger
|
652
|
+
break
|
653
|
+
rescue
|
654
|
+
# Exception occurred. Probably EINTR.
|
655
|
+
@logger.warn "Select raised exception; will retry. Reason: #{$!}" if @logger
|
656
|
+
@logger.warn "IOs at time of failure:"
|
657
|
+
@logger.warn "Readset:"
|
658
|
+
readset.each do |io|
|
659
|
+
ioInfo = @ioInfo[io]
|
660
|
+
puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
|
661
|
+
end
|
662
|
+
@logger.warn "Writeset:"
|
663
|
+
writeset.each do |io|
|
664
|
+
ioInfo = @ioInfo[io]
|
665
|
+
puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
|
666
|
+
end
|
667
|
+
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
if selectResult.nil?
|
672
|
+
# Process timer
|
673
|
+
@logger.debug "eventloop: processing timer" if @logger
|
674
|
+
# Calling processTimer in withReadFiber here is not correct. What if at this point the fiber was already paused in a read, and we
|
675
|
+
# want to process a timer? In that case we will resume the read and it will possibly finish, but we'll never
|
676
|
+
# call the timer handler. For this reason we must prevent read calls in timerHandlers.
|
677
|
+
processTimer(timer) if timer
|
678
|
+
else
|
679
|
+
readable, writeable = selectResult
|
680
|
+
|
681
|
+
# If we are stopped, then ignore reads; we only care about completing our writes that were pending when we were stopped.
|
682
|
+
readable = [] if @stopped
|
683
|
+
|
684
|
+
readable.each do |io|
|
685
|
+
# This could be the eventRead pipe, which we use to signal shutdown or to reloop.
|
686
|
+
if io == @eventRead
|
687
|
+
@logger.debug "Event received on the eventRead pipe." if @logger
|
688
|
+
@eventRead.read 1
|
689
|
+
@currentEventPipeChars -= 1
|
690
|
+
next
|
691
|
+
end
|
692
|
+
|
693
|
+
@currentIoInfo = @ioInfo[io]
|
694
|
+
if @currentIoInfo.state == :listening
|
695
|
+
@logger.debug "eventloop: calling handleAccept for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
|
696
|
+
# Replace the currentIoInfo with the accepted socket
|
697
|
+
listenerMetainfo = @currentIoInfo.metainfo
|
698
|
+
@currentIoInfo, addr, port = handleAccept(@currentIoInfo)
|
699
|
+
withReadFiber(@currentIoInfo) do
|
700
|
+
@currentHandlerCallback = :serverinit
|
701
|
+
@handler.serverInit(listenerMetainfo, addr, port)
|
702
|
+
end
|
703
|
+
else
|
704
|
+
@logger.debug "eventloop: calling handleRead for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
|
705
|
+
#handleRead(@currentIoInfo)
|
706
|
+
withReadFiber(@currentIoInfo) do
|
707
|
+
@currentHandlerCallback = :recv_data
|
708
|
+
@handler.recvData @currentIoInfo.metainfo
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
writeable.each do |io|
|
714
|
+
@currentIoInfo = @ioInfo[io]
|
715
|
+
# Check if there is still ioInfo for this io. This can happen if this io was also ready for read, and
|
716
|
+
# the read had an error (for example connection failure) and the ioinfo was removed when handling the error.
|
717
|
+
next if ! @currentIoInfo
|
718
|
+
if @currentIoInfo.state == :connecting
|
719
|
+
@logger.debug "eventloop: calling finishConnection for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
|
720
|
+
finishConnection(@currentIoInfo)
|
721
|
+
else
|
722
|
+
@logger.debug "eventloop: calling writeOutputBuffer for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
|
723
|
+
writeOutputBuffer(@currentIoInfo)
|
724
|
+
end
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
ioToRemove.each do |io|
|
729
|
+
ioInfo = @ioInfo.delete io
|
730
|
+
@logger.warn "Detected an IO that was closed but still in the list of selectable IO. Metadata = #{ioInfo.metainfo}"
|
731
|
+
end
|
732
|
+
|
733
|
+
:continue
|
734
|
+
end
|
735
|
+
|
736
|
+
def processTimers
|
737
|
+
selectTimeout = nil
|
738
|
+
timer = nil
|
739
|
+
while true && ! @stopped
|
740
|
+
timer = @timerManager.peek
|
741
|
+
break if ! timer
|
742
|
+
secondsUntilExpiry = timer.secondsUntilExpiry
|
743
|
+
if secondsUntilExpiry > 0
|
744
|
+
selectTimeout = secondsUntilExpiry
|
745
|
+
break
|
746
|
+
end
|
747
|
+
# Process timer now; it's firing time has already passed.
|
748
|
+
processTimer(timer)
|
749
|
+
end
|
750
|
+
[timer, selectTimeout]
|
751
|
+
end
|
752
|
+
|
753
|
+
def startConnection(port, addr, metainfo)
|
754
|
+
socket = Socket.new(AF_INET, SOCK_STREAM, 0)
|
755
|
+
addr = Socket.pack_sockaddr_in(port, addr)
|
756
|
+
|
757
|
+
info = IOInfo.new(socket, metainfo)
|
758
|
+
info.readFiberIoFacade.logger = @logger if @logger
|
759
|
+
|
760
|
+
begin
|
761
|
+
socket.connect_nonblock(addr)
|
762
|
+
info.state = :connected
|
763
|
+
@currentHandlerCallback = :client_init
|
764
|
+
@handler.clientInit(ioInfo.metainfo)
|
765
|
+
rescue Errno::EINPROGRESS
|
766
|
+
# Connection is ongoing.
|
767
|
+
info.state = :connecting
|
768
|
+
end
|
769
|
+
|
770
|
+
info
|
771
|
+
end
|
772
|
+
|
773
|
+
def finishConnection(ioInfo)
|
774
|
+
# Socket was connecting and is now writable. Check if there was a connection failure
|
775
|
+
# This uses the getpeername method. See http://cr.yp.to/docs/connect.html
|
776
|
+
begin
|
777
|
+
ioInfo.io.getpeername
|
778
|
+
ioInfo.state = :connected
|
779
|
+
if ioInfo.connectTimer
|
780
|
+
@logger.debug "cancelling connect timer for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
|
781
|
+
@timerManager.cancel ioInfo.connectTimer
|
782
|
+
end
|
783
|
+
@currentHandlerCallback = :client_init
|
784
|
+
@handler.clientInit(ioInfo.metainfo)
|
785
|
+
rescue
|
786
|
+
# Connection failed.
|
787
|
+
@logger.debug "connection failed for IO metainfo=#{@currentIoInfo.metainfo}: #{$!}" if @logger
|
788
|
+
@currentHandlerCallback = :connect_error
|
789
|
+
@handler.connectError(ioInfo.metainfo, $!)
|
790
|
+
disposeIo(ioInfo)
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
def processTimer(timer)
|
795
|
+
begin
|
796
|
+
# Check for internal timers first.
|
797
|
+
if timer.metainfo && timer.metainfo.is_a?(InternalTimerInfo)
|
798
|
+
if timer.metainfo.type == :connect_timeout
|
799
|
+
@currentHandlerCallback = :error
|
800
|
+
@handler.error(timer.metainfo.data.metainfo, "Connection timed out")
|
801
|
+
disposeIo(timer.metainfo.data)
|
802
|
+
end
|
803
|
+
else
|
804
|
+
@currentHandlerCallback = :timer
|
805
|
+
@handler.timerExpired(timer.metainfo)
|
806
|
+
end
|
807
|
+
rescue
|
808
|
+
@logger.error "Exception in timer event handler: #{$!}" if @logger
|
809
|
+
@logger.error $!.backtrace.join "\n" if @logger
|
810
|
+
end
|
811
|
+
@timerManager.next
|
812
|
+
end
|
813
|
+
|
814
|
+
def disposeIo(io)
|
815
|
+
# Inner function in disposeIo.
|
816
|
+
def closeIo(io)
|
817
|
+
begin
|
818
|
+
io.close if !io.closed?
|
819
|
+
rescue
|
820
|
+
@logger.warn "Closing IO failed with exception #{$!}"
|
821
|
+
@logger.debug $!.backtrace.join("\n")
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
if io.is_a?(IOInfo)
|
826
|
+
# Flush any output
|
827
|
+
begin
|
828
|
+
@logger.debug "disposeIo: flushing data" if @logger
|
829
|
+
io.outputBuffer.flush
|
830
|
+
rescue
|
831
|
+
end
|
832
|
+
|
833
|
+
closeIo(io.io)
|
834
|
+
@ioInfo.delete io.io
|
835
|
+
elsif io.is_a?(IoFacade)
|
836
|
+
closeIo(io)
|
837
|
+
io.removeFromIOHash(@ioInfo)
|
838
|
+
else
|
839
|
+
closeIo(io)
|
840
|
+
@ioInfo.delete io
|
841
|
+
end
|
842
|
+
end
|
843
|
+
|
844
|
+
# Given the ioInfo for a listening socket, call accept and return the new ioInfo for the
|
845
|
+
# client's socket
|
846
|
+
def handleAccept(ioInfo)
|
847
|
+
socket, clientAddr = ioInfo.io.accept
|
848
|
+
info = IOInfo.new(socket, ioInfo.metainfo)
|
849
|
+
info.readFiberIoFacade.logger = @logger if @logger
|
850
|
+
info.state = :connected
|
851
|
+
@ioInfo[info.io] = info
|
852
|
+
if @logger
|
853
|
+
port, addr = Socket.unpack_sockaddr_in(clientAddr)
|
854
|
+
@logger.debug "Accepted connection from #{addr}:#{port}" if @logger
|
855
|
+
end
|
856
|
+
|
857
|
+
[info, addr, port]
|
858
|
+
end
|
859
|
+
|
860
|
+
def handleRead(ioInfo)
|
861
|
+
if ioInfo.readFiber.nil? || ! ioInfo.readFiber.alive?
|
862
|
+
ioInfo.readFiber = Fiber.new do |ioInfo|
|
863
|
+
@currentHandlerCallback = :recv_data
|
864
|
+
@handler.recvData ioInfo.metainfo
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
# Allow handler to read some data.
|
869
|
+
# This call will return either if:
|
870
|
+
# 1. the handler needs more data but it isn't available yet,
|
871
|
+
# 2. if it's read all the data it wanted to read for the current message it's building
|
872
|
+
# 3. if a read error occurred.
|
873
|
+
#
|
874
|
+
# In case 2 the latter case the fiber will be dead. In cases 1 and 2, we should select on the socket
|
875
|
+
# until data is ready. For case 3, the state of the ioInfo is set to error and the io should be
|
876
|
+
# removed.
|
877
|
+
ioInfo.readFiber.resume(ioInfo)
|
878
|
+
if ioInfo.state == :error
|
879
|
+
@currentHandlerCallback = :error
|
880
|
+
@handler.error(ioInfo.metainfo, ioInfo.lastReadError)
|
881
|
+
disposeIo(ioInfo)
|
882
|
+
end
|
883
|
+
end
|
884
|
+
|
885
|
+
# Call the passed block in the context of the read Fiber. Basically the
|
886
|
+
# passed block is run as normal, but if the block performs a read from an io and that
|
887
|
+
# read would block, the block is paused, and withReadFiber returns. The next time withReadFiber
|
888
|
+
# is called the block will be resumed at the point of the read.
|
889
|
+
def withReadFiber(ioInfo)
|
890
|
+
if ioInfo.readFiber.nil? || ! ioInfo.readFiber.alive?
|
891
|
+
ioInfo.readFiber = Fiber.new do |ioInfo|
|
892
|
+
yield ioInfo.readFiberIoFacade
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
# Allow handler to read some data.
|
897
|
+
# This call will return either if:
|
898
|
+
# 1. the handler needs more data but it isn't available yet,
|
899
|
+
# 2. if it's read all the data it wanted to read for the current message it's building
|
900
|
+
# 3. if a read error occurred.
|
901
|
+
#
|
902
|
+
# In case 2 the latter case the fiber will be dead. In cases 1 and 2, we should select on the socket
|
903
|
+
# until data is ready. For case 3, the state of the ioInfo is set to error and the io should be
|
904
|
+
# removed.
|
905
|
+
ioInfo.readFiber.resume(ioInfo)
|
906
|
+
if ioInfo.state == :error
|
907
|
+
@currentHandlerCallback = :error
|
908
|
+
@handler.error(ioInfo.metainfo, ioInfo.lastReadError)
|
909
|
+
disposeIo(ioInfo)
|
910
|
+
end
|
911
|
+
|
912
|
+
end
|
913
|
+
|
914
|
+
def writeOutputBuffer(ioInfo)
|
915
|
+
begin
|
916
|
+
@logger.debug "writeOutputBuffer: flushing data" if @logger
|
917
|
+
if !ioInfo.writeRateLimit
|
918
|
+
ioInfo.outputBuffer.flush
|
919
|
+
else
|
920
|
+
avail = ioInfo.writeRateLimit.avail
|
921
|
+
if avail < ioInfo.outputBuffer.size
|
922
|
+
if avail > 0
|
923
|
+
ioInfo.writeRateLimit.withdraw avail
|
924
|
+
ioInfo.outputBuffer.flush avail
|
925
|
+
end
|
926
|
+
else
|
927
|
+
ioInfo.writeRateLimit.withdraw ioInfo.outputBuffer.size
|
928
|
+
ioInfo.outputBuffer.flush
|
929
|
+
end
|
930
|
+
end
|
931
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
|
932
|
+
# Need to wait to write more.
|
933
|
+
@logger.debug "writeOutputBuffer: write failed with retryable exception #{$!}" if @logger
|
934
|
+
rescue
|
935
|
+
# Write failure occurred
|
936
|
+
@logger.debug "writeOutputBuffer: write failed with unexpected exception #{$!}" if @logger
|
937
|
+
if ioInfo.useErrorhandler
|
938
|
+
@currentHandlerCallback = :error
|
939
|
+
@handler.error(ioInfo.metainfo, "Write error: #{$!}")
|
940
|
+
else
|
941
|
+
raise $!
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
end
|
946
|
+
|
947
|
+
end
|
948
|
+
|
949
|
+
end
|