scriptty 0.5.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitattributes +1 -0
- data/.gitignore +3 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/README.rdoc +31 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/scriptty-capture +5 -0
- data/bin/scriptty-dump-screens +4 -0
- data/bin/scriptty-replay +5 -0
- data/bin/scriptty-term-test +4 -0
- data/bin/scriptty-transcript-parse +4 -0
- data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
- data/examples/captures/xterm-vim-session.bin +262 -0
- data/examples/demo-capture.rb +19 -0
- data/examples/telnet-nego.rb +55 -0
- data/lib/scriptty/apps/capture_app/console.rb +104 -0
- data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
- data/lib/scriptty/apps/capture_app.rb +213 -0
- data/lib/scriptty/apps/dump_screens_app.rb +166 -0
- data/lib/scriptty/apps/replay_app.rb +229 -0
- data/lib/scriptty/apps/term_test_app.rb +124 -0
- data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
- data/lib/scriptty/cursor.rb +39 -0
- data/lib/scriptty/exception.rb +38 -0
- data/lib/scriptty/expect.rb +392 -0
- data/lib/scriptty/multiline_buffer.rb +192 -0
- data/lib/scriptty/net/event_loop.rb +610 -0
- data/lib/scriptty/screen_pattern/generator.rb +398 -0
- data/lib/scriptty/screen_pattern/parser.rb +558 -0
- data/lib/scriptty/screen_pattern.rb +104 -0
- data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
- data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
- data/lib/scriptty/term/dg410/parser.rb +162 -0
- data/lib/scriptty/term/dg410.rb +489 -0
- data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
- data/lib/scriptty/term/xterm.rb +661 -0
- data/lib/scriptty/term.rb +40 -0
- data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
- data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
- data/lib/scriptty/util/fsm.rb +177 -0
- data/lib/scriptty/util/transcript/reader.rb +96 -0
- data/lib/scriptty/util/transcript/writer.rb +111 -0
- data/test/apps/capture_app_test.rb +123 -0
- data/test/apps/transcript_parse_app_test.rb +118 -0
- data/test/cursor_test.rb +51 -0
- data/test/fsm_definition_parser_test.rb +220 -0
- data/test/fsm_test.rb +322 -0
- data/test/multiline_buffer_test.rb +275 -0
- data/test/net/event_loop_test.rb +402 -0
- data/test/screen_pattern/generator_test.rb +408 -0
- data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
- data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
- data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
- data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
- data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
- data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
- data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
- data/test/screen_pattern/parser_test.rb +266 -0
- data/test/term/dg410/parser_test.rb +139 -0
- data/test/term/xterm_test.rb +327 -0
- data/test/test_helper.rb +3 -0
- data/test/util/transcript/reader_test.rb +131 -0
- data/test/util/transcript/writer_test.rb +126 -0
- data/test.watchr +29 -0
- metadata +175 -0
@@ -0,0 +1,610 @@
|
|
1
|
+
# = Event loop driven by Java Non-blocking I/O
|
2
|
+
# Copyright (C) 2010 Infonium Inc.
|
3
|
+
#
|
4
|
+
# This file is part of ScripTTY.
|
5
|
+
#
|
6
|
+
# ScripTTY is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# ScripTTY is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
17
|
+
# along with ScripTTY. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
raise LoadError.new("This file only works in JRuby") unless defined?(Java)
|
20
|
+
|
21
|
+
require 'java'
|
22
|
+
require 'thread'
|
23
|
+
|
24
|
+
module ScripTTY
|
25
|
+
module Net
|
26
|
+
class EventLoop
|
27
|
+
# XXX - This complicated bit of code demonstrates that test-driven
|
28
|
+
# development is not a panacea. (A cleanup would be grand.)
|
29
|
+
|
30
|
+
# XXX - This code could use a cleanup. In particular, the interestOps
|
31
|
+
# logic should be simplified, because when it's done wrong, we can get
|
32
|
+
# into a state where the event loop won't process events from particular
|
33
|
+
# channels.
|
34
|
+
|
35
|
+
include_class 'java.net.InetSocketAddress'
|
36
|
+
include_class 'java.nio.ByteBuffer'
|
37
|
+
include_class 'java.nio.channels.SelectionKey'
|
38
|
+
include_class 'java.nio.channels.Selector'
|
39
|
+
include_class 'java.nio.channels.ServerSocketChannel'
|
40
|
+
include_class 'java.nio.channels.SocketChannel'
|
41
|
+
|
42
|
+
DEBUG = false
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@selector = Selector.open
|
46
|
+
@read_buffer = ByteBuffer.allocate(4096)
|
47
|
+
@exit_mutex = Mutex.new # protects
|
48
|
+
@exit_requested = false
|
49
|
+
@timer_queue = [] # sorted list of timers, in ascending order of expire_at time
|
50
|
+
@done = false
|
51
|
+
end
|
52
|
+
|
53
|
+
# Instruct the main loop to exit. Returns immediately.
|
54
|
+
#
|
55
|
+
# - This method may safely be called from any thread.
|
56
|
+
# - This method may safely be invoked multiple times.
|
57
|
+
def exit
|
58
|
+
@exit_mutex.synchronize { @exit_requested = true }
|
59
|
+
@selector.wakeup
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return true if the event loop is done executing.
|
64
|
+
def done?
|
65
|
+
@done
|
66
|
+
end
|
67
|
+
|
68
|
+
# Listen for TCP connections on the specified address (given as [host, port])
|
69
|
+
#
|
70
|
+
# If port 0 is specified, the operating system will choose a port.
|
71
|
+
#
|
72
|
+
# If a block is given, it will be passed the ListeningSocketWrapper
|
73
|
+
# object, and the result of the block will be returned. Otherwise, the
|
74
|
+
# ListeningSocketWrapper object is returned.
|
75
|
+
#
|
76
|
+
# Options:
|
77
|
+
# [:multiple]
|
78
|
+
# If true, then the parameter is a list of addresses, the block
|
79
|
+
# will be invoked for each one, and the return value will be an array
|
80
|
+
# of ListeningSocketWrapper objects.
|
81
|
+
def listen(address, options={}, &block)
|
82
|
+
if options[:multiple]
|
83
|
+
# address is actually a list of addresses
|
84
|
+
options = options.dup
|
85
|
+
options.delete(:multiple)
|
86
|
+
return address.map{ |addr| listen(addr, options, &block) }
|
87
|
+
end
|
88
|
+
bind_address = EventLoop.parse_address(address)
|
89
|
+
schan = ServerSocketChannel.open
|
90
|
+
schan.configureBlocking(false)
|
91
|
+
schan.socket.bind(bind_address)
|
92
|
+
lw = ListeningSocketWrapper.new(self, schan)
|
93
|
+
# We want OP_ACCEPT here (and later, OP_READ), so that we can tell
|
94
|
+
# when the connection is established/dropped, even if the user does
|
95
|
+
# not specifiy any on_accept or on_close/on_read_bytes callbacks.
|
96
|
+
schan.register(@selector, SelectionKey::OP_ACCEPT)
|
97
|
+
selection_key = schan.keyFor(@selector) # SelectionKey object
|
98
|
+
selection_key.attach({:listening_socket_wrapper => lw})
|
99
|
+
if block
|
100
|
+
block.call(lw)
|
101
|
+
else
|
102
|
+
lw
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Convenience method: Listen for TCP connections on a particular address
|
107
|
+
# (given as [host, port]), and invoke the given block when a connection
|
108
|
+
# is received.
|
109
|
+
#
|
110
|
+
# If port 0 is specified, the operating system will choose a port.
|
111
|
+
#
|
112
|
+
# Returns the ListeningSocketWrapper.
|
113
|
+
#
|
114
|
+
# Options:
|
115
|
+
# [:multiple]
|
116
|
+
# If true, then the parameter is a list of addresses, and
|
117
|
+
# multiple ListeningSocketWrapper objects will be returned as an array.
|
118
|
+
def on_accept(address, options={}, &callback)
|
119
|
+
raise ArgumentError.new("no block given") unless callback
|
120
|
+
listen(address, options) { |listener| listener.on_accept(&callback) }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Initiate a TCP connection to the specified address (given as [host, port])
|
124
|
+
#
|
125
|
+
# If a block is given, it will be passed the OutgointConnectionWrapper
|
126
|
+
# object, and the result of the block will be returned. Otherwise, the
|
127
|
+
# OutgoingConnectionWrapper object is returned.
|
128
|
+
def connect(address)
|
129
|
+
connect_address = EventLoop.parse_address(address)
|
130
|
+
chan = SocketChannel.open
|
131
|
+
chan.configureBlocking(false)
|
132
|
+
chan.socket.setOOBInline(true) # Receive TCP URGent data (but not the fact that it's urgent) in-band
|
133
|
+
chan.connect(connect_address)
|
134
|
+
cw = OutgoingConnectionWrapper.new(self, chan, address)
|
135
|
+
# We want OP_CONNECT here (and OP_READ after the connection is
|
136
|
+
# established) so that we can tell when the connection is
|
137
|
+
# established/dropped, even if the user does not specifiy any
|
138
|
+
# on_connect or on_close/on_read_bytes callbacks.
|
139
|
+
chan.register(@selector, SelectionKey::OP_CONNECT)
|
140
|
+
selection_key = chan.keyFor(@selector) # SelectionKey object
|
141
|
+
selection_key.attach({:connection_wrapper => cw})
|
142
|
+
if block_given?
|
143
|
+
yield cw
|
144
|
+
else
|
145
|
+
cw
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Convenience method: Initiate a TCP connection to the specified
|
150
|
+
# address (given as [host, port]) and invoke the given block when a
|
151
|
+
# connection is made.
|
152
|
+
#
|
153
|
+
# Returns the OutgoingConnectionWrapper that will be connected to.
|
154
|
+
def on_connect(address, &callback)
|
155
|
+
raise ArgumentError.new("no block given") unless callback
|
156
|
+
connect(address) { |conn| conn.on_connect(&callback) }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Invoke the specified callback after the specified number of seconds
|
160
|
+
# have elapsed.
|
161
|
+
#
|
162
|
+
# Return the ScripTTY::Net::EventLoop::Timer object for the timer.
|
163
|
+
def timer(delay, options={}, &callback)
|
164
|
+
raise ArgumentError.new("no block given") unless callback
|
165
|
+
new_timer = Timer.new(self, Time.now + delay, callback, options)
|
166
|
+
i = 0
|
167
|
+
while i < @timer_queue.length # Insert new timer in the correct sort order
|
168
|
+
break if @timer_queue[i].expire_at > new_timer.expire_at
|
169
|
+
i += 1
|
170
|
+
end
|
171
|
+
@timer_queue.insert(i, new_timer)
|
172
|
+
@selector.wakeup
|
173
|
+
new_timer
|
174
|
+
end
|
175
|
+
|
176
|
+
def main
|
177
|
+
raise ArgumentError.new("use the resume method when suspended") if @suspended
|
178
|
+
raise ArgumentError.new("Already done") if @done
|
179
|
+
loop do
|
180
|
+
# Exit if the "exit" method has been invoked.
|
181
|
+
break if @exit_mutex.synchronize{ @exit_requested }
|
182
|
+
# Exit if there are no active connections and no non-daemon timers
|
183
|
+
break if (@selector.keys.empty? and (@timer_queue.empty? or @timer_queue.map{|t| t.daemon?}.all?))
|
184
|
+
|
185
|
+
# If there are any timers, schedule a wake-up for when the first
|
186
|
+
# timer expires.
|
187
|
+
next_timer = @timer_queue.first
|
188
|
+
if next_timer
|
189
|
+
timeout_millis = (1000 * (next_timer.expire_at - Time.now)).to_i
|
190
|
+
timeout_millis = nil if timeout_millis <= 0
|
191
|
+
else
|
192
|
+
timeout_millis = 0 # sleep indefinitely
|
193
|
+
end
|
194
|
+
|
195
|
+
# select(), unless the timeout has already expired
|
196
|
+
puts "SELECT: to=#{timeout_millis.inspect} kk=#{@selector.keys.to_a.map{|k|k.attachment}.inspect} tt=#{@timer_queue.length}" if DEBUG
|
197
|
+
if timeout_millis
|
198
|
+
# Return when something happens, or after timeout (if timeout_millis is non-zero)
|
199
|
+
@selector.select(timeout_millis)
|
200
|
+
else
|
201
|
+
# Non-blocking select
|
202
|
+
@selector.selectNow
|
203
|
+
end
|
204
|
+
puts "DONE SELECT" if DEBUG
|
205
|
+
|
206
|
+
# Invoke the callbacks for any expired timers
|
207
|
+
now = Time.now
|
208
|
+
until @timer_queue.empty? or now < @timer_queue.first.expire_at
|
209
|
+
timer = @timer_queue.shift
|
210
|
+
timer.send(:callback).call
|
211
|
+
end
|
212
|
+
timer = nil
|
213
|
+
|
214
|
+
# Handle channels that are ready for I/O operations
|
215
|
+
@selector.selectedKeys.to_a.each do |k|
|
216
|
+
handle_selection_key(k)
|
217
|
+
@selector.selectedKeys.remove(k)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Break out of the loop if the suspend method has been invoked. # TODO - test me
|
221
|
+
return :suspended if @suspended
|
222
|
+
end
|
223
|
+
nil
|
224
|
+
ensure
|
225
|
+
unless @suspended or @done
|
226
|
+
@selector.keys.to_a.each { |k| k.channel.close } # Close any sockets opened by this EventLoop
|
227
|
+
@selector.close
|
228
|
+
@done = true
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Temporarily break out of the event loop.
|
233
|
+
#
|
234
|
+
# NOTE: Always use the resume method after using this method, since
|
235
|
+
# otherwise network connections will hang. This method is *NOT*
|
236
|
+
# thread-safe.
|
237
|
+
#
|
238
|
+
# To exit the event loop permanently, use the exit method.
|
239
|
+
def suspend
|
240
|
+
@suspended = true
|
241
|
+
@selector.wakeup
|
242
|
+
end
|
243
|
+
|
244
|
+
# Resume a
|
245
|
+
#
|
246
|
+
# Always use the resume method after using this method.
|
247
|
+
def resume
|
248
|
+
raise ArgumentError.new("not suspended") unless @suspended
|
249
|
+
@suspended = false
|
250
|
+
main
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
# Cancel the specified timer.
|
256
|
+
def cancel_timer(timer)
|
257
|
+
@timer_queue.delete(timer)
|
258
|
+
nil
|
259
|
+
end
|
260
|
+
|
261
|
+
def handle_selection_key(k)
|
262
|
+
att = k.attachment
|
263
|
+
case k.channel
|
264
|
+
when ServerSocketChannel
|
265
|
+
puts "SELECTED ServerSocketChannel: valid:#{k.valid?} connectable:#{k.connectable?} writable:#{k.writable?} readable:#{k.readable?} interestOps:#{k.interestOps.inspect} readyOps:#{k.readyOps}" if DEBUG
|
266
|
+
if k.valid? and k.acceptable?
|
267
|
+
lw = att[:listening_socket_wrapper]
|
268
|
+
accepted = false
|
269
|
+
begin
|
270
|
+
socket_channel = k.channel.accept
|
271
|
+
socket_channel.configureBlocking(false)
|
272
|
+
socket_channel.socket.setOOBInline(true) # Receive TCP URGent data (but not the fact that it's urgent) in-band
|
273
|
+
accepted = true
|
274
|
+
rescue => e
|
275
|
+
# Invoke the on_accept_error callback, if present.
|
276
|
+
begin
|
277
|
+
invoke_callback(k.channel, :on_accept_error, e)
|
278
|
+
ensure
|
279
|
+
close_channel(k.channel, true)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
if accepted
|
283
|
+
socket_channel.register(@selector, SelectionKey::OP_READ) # Register the channel with the selector
|
284
|
+
cw = ConnectionWrapper.new(self, socket_channel)
|
285
|
+
invoke_callback(k.channel, :on_accept, cw)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
when SocketChannel
|
290
|
+
puts "SELECTED SocketChannel: valid:#{k.valid?} connectable:#{k.connectable?} writable:#{k.writable?} readable:#{k.readable?} interestOps:#{k.interestOps.inspect} readyOps:#{k.readyOps}" if DEBUG
|
291
|
+
if k.valid? and (k.connectable? or !att[:connect_finished])
|
292
|
+
# WORKAROUND: On some platforms (Mac OS X 10.5, Java 1.5.0_22,
|
293
|
+
# JRuby 1.4.0) SelectionKey#isConnectable returns 0 when a
|
294
|
+
# connection error would occur, so we would never call
|
295
|
+
# finishConnect and therefore keep looping infinitely over the
|
296
|
+
# select() call. To work around this, we add this flag to the
|
297
|
+
# channel hash and check it, rather than relying on
|
298
|
+
# k.connectable? returning true.
|
299
|
+
att[:connect_finished] = true
|
300
|
+
cw = att[:connection_wrapper]
|
301
|
+
connected = false
|
302
|
+
begin
|
303
|
+
k.channel.finishConnect
|
304
|
+
connected = true
|
305
|
+
rescue => e
|
306
|
+
# Invoke the on_connect_error callback, if present.
|
307
|
+
begin
|
308
|
+
invoke_callback(k.channel, :on_connect_error, e)
|
309
|
+
ensure
|
310
|
+
close_channel(k.channel, true)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
if connected
|
314
|
+
k.interestOps(k.interestOps & ~SelectionKey::OP_CONNECT) # We no longer care about connection status
|
315
|
+
k.interestOps(k.interestOps | SelectionKey::OP_READ) # Now we care about incoming bytes (and disconnection)
|
316
|
+
invoke_callback(k.channel, :on_connect, cw)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
if k.valid? and k.writable?
|
320
|
+
puts "WRITABLE #{att.inspect}" if DEBUG
|
321
|
+
bufs = att[:write_buffers]
|
322
|
+
callbacks = att[:write_completion_callbacks]
|
323
|
+
if bufs and !bufs.empty?
|
324
|
+
puts "BUFS NOT EMPTY" if DEBUG
|
325
|
+
# Send as much as we can of the list of write buffers
|
326
|
+
w = k.channel.write(bufs.to_java(ByteBuffer))
|
327
|
+
|
328
|
+
# Remove the buffers that have been completely sent, and invoke
|
329
|
+
# any write-completion callbacks.
|
330
|
+
while !bufs.empty? and bufs[0].position == bufs[0].limit
|
331
|
+
bufs.shift
|
332
|
+
callback = callbacks.shift
|
333
|
+
invoke_callback(k.channel, callback)
|
334
|
+
end
|
335
|
+
elsif (k.interestOps & SelectionKey::OP_WRITE) != 0
|
336
|
+
# Socket is writable, but there's nothing here to write.
|
337
|
+
# Indicate that we're no longer interested in whether the socket is writable.
|
338
|
+
k.interestOps(k.interestOps & ~SelectionKey::OP_WRITE)
|
339
|
+
end
|
340
|
+
# At the end of a graceful close, (half-)close the output of the TCP connection
|
341
|
+
if att[:graceful_close] and (!bufs or bufs.empty?)
|
342
|
+
puts "SHUTTING DOWN OUTPUT" if DEBUG
|
343
|
+
shutdown_output_on_channel(k.channel)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
if k.valid? and k.readable?
|
347
|
+
@read_buffer.clear
|
348
|
+
length = k.channel.read(@read_buffer)
|
349
|
+
if length < 0 # connection shut down (or at least the input is)
|
350
|
+
puts "READ length < 0" if DEBUG
|
351
|
+
close_channel(k.channel)
|
352
|
+
elsif length == 0
|
353
|
+
puts "READ length == 0" if DEBUG
|
354
|
+
raise "BUG: unhandled length == 0" # I think this should never happen.
|
355
|
+
else
|
356
|
+
bytes = String.from_java_bytes(@read_buffer.array[0,length])
|
357
|
+
invoke_callback(k.channel, :on_receive_bytes, bytes)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def invoke_callback(channel, callback, *args)
|
364
|
+
if callback.is_a?(Symbol)
|
365
|
+
callback_proc = channel_callback_hash(channel)[callback]
|
366
|
+
else
|
367
|
+
callback_proc = callback
|
368
|
+
end
|
369
|
+
error_callback = channel_callback_hash(channel)[:on_callback_error]
|
370
|
+
begin
|
371
|
+
callback_proc.call(*args) if callback_proc
|
372
|
+
rescue => e
|
373
|
+
raise unless error_callback
|
374
|
+
error_callback.call(e)
|
375
|
+
end
|
376
|
+
nil
|
377
|
+
end
|
378
|
+
|
379
|
+
def set_callback_error_callback(channel, &callback) # :nodoc:
|
380
|
+
channel_callback_hash(channel)[:on_callback_error] = callback
|
381
|
+
nil
|
382
|
+
end
|
383
|
+
|
384
|
+
def set_on_accept_callback(channel, &callback) # :nodoc:
|
385
|
+
channel_callback_hash(channel)[:on_accept] = callback
|
386
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
387
|
+
k.interestOps(k.interestOps | SelectionKey::OP_ACCEPT)
|
388
|
+
nil
|
389
|
+
end
|
390
|
+
|
391
|
+
def set_on_accept_error_callback(channel, &callback) # :nodoc:
|
392
|
+
channel_callback_hash(channel)[:on_accept_error] = callback
|
393
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
394
|
+
k.interestOps(k.interestOps | SelectionKey::OP_ACCEPT)
|
395
|
+
nil
|
396
|
+
end
|
397
|
+
|
398
|
+
def set_on_connect_callback(channel, &callback) # :nodoc:
|
399
|
+
channel_callback_hash(channel)[:on_connect] = callback
|
400
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
401
|
+
#k.interestOps(k.interestOps | SelectionKey::OP_CONNECT) # we want to when the socket is connected or when there are connection errors
|
402
|
+
#k.interestOps(k.interestOps | SelectionKey::OP_CONNECT | SelectionKey::OP_READ) # we want to when the socket is connected or when there are connection errors DEBUG FIXME
|
403
|
+
nil
|
404
|
+
end
|
405
|
+
|
406
|
+
def set_on_connect_error_callback(channel, &callback) # :nodoc:
|
407
|
+
channel_callback_hash(channel)[:on_connect_error] = callback
|
408
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
409
|
+
k.interestOps(k.interestOps | SelectionKey::OP_CONNECT) # we want to when the socket is connected or when there are connection errors
|
410
|
+
nil
|
411
|
+
end
|
412
|
+
|
413
|
+
def set_on_receive_bytes_callback(channel, &callback) # :nodoc:
|
414
|
+
channel_callback_hash(channel)[:on_receive_bytes] = callback
|
415
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
416
|
+
k.interestOps(k.interestOps | SelectionKey::OP_READ) # we want to know when bytes are received
|
417
|
+
nil
|
418
|
+
end
|
419
|
+
|
420
|
+
def set_on_close_callback(channel, &callback) # :nodoc:
|
421
|
+
channel_callback_hash(channel)[:on_close] = callback
|
422
|
+
|
423
|
+
# we want to know when the connection closes
|
424
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
425
|
+
if channel.is_a?(ServerSocketChannel)
|
426
|
+
k.interestOps(k.interestOps | SelectionKey::OP_ACCEPT)
|
427
|
+
else # SocketChannel
|
428
|
+
k.interestOps(k.interestOps | SelectionKey::OP_READ)
|
429
|
+
end
|
430
|
+
nil
|
431
|
+
end
|
432
|
+
|
433
|
+
def add_to_write_buffer(channel, bytes, &completion_callback) # :nodoc:
|
434
|
+
h = channel_callback_hash(channel)
|
435
|
+
|
436
|
+
return if h[:graceful_close]
|
437
|
+
|
438
|
+
# Buffer the data to be written
|
439
|
+
h[:write_buffers] ||= []
|
440
|
+
h[:write_buffers] << ByteBuffer.wrap(bytes.to_java_bytes)
|
441
|
+
|
442
|
+
# Add a write-completion callback, if applicable.
|
443
|
+
h[:write_completion_callbacks] ||= []
|
444
|
+
h[:write_completion_callbacks] << completion_callback || nil
|
445
|
+
|
446
|
+
# indicate that we want to know when the channel is writable
|
447
|
+
k = channel.keyFor(@selector)
|
448
|
+
k.interestOps(k.interestOps | SelectionKey::OP_WRITE)
|
449
|
+
nil
|
450
|
+
end
|
451
|
+
|
452
|
+
def shutdown_output_on_channel(channel) # :nodoc:
|
453
|
+
@selector.wakeup
|
454
|
+
h = channel_callback_hash(channel)
|
455
|
+
return if h[:already_closed] or h[:already_shutdown_output]
|
456
|
+
channel.socket.shutdownOutput
|
457
|
+
h[:already_shutdown_output] = true
|
458
|
+
end
|
459
|
+
|
460
|
+
def close_channel(channel, error=false) # :nodoc:
|
461
|
+
puts "CLOSE_CHANNEL(hard) #{channel.java_class.simple_name}" if DEBUG
|
462
|
+
@selector.wakeup
|
463
|
+
h = channel_callback_hash(channel)
|
464
|
+
return if h[:already_closed]
|
465
|
+
channel.close
|
466
|
+
h[:already_closed] = true
|
467
|
+
invoke_callback(channel, :on_close) unless error
|
468
|
+
end
|
469
|
+
|
470
|
+
def close_channel_gracefully(channel)
|
471
|
+
puts "CLOSING GRACEFULLY" if DEBUG
|
472
|
+
@selector.wakeup
|
473
|
+
h = channel_callback_hash(channel)
|
474
|
+
return if h[:graceful_close]
|
475
|
+
h[:graceful_close] = true
|
476
|
+
|
477
|
+
# The graceful close code is in the channel-is-writable handler (above),
|
478
|
+
# so indicate that we care about whether the channel is # writable.
|
479
|
+
k = channel.keyFor(@selector)
|
480
|
+
k.interestOps(k.interestOps | SelectionKey::OP_WRITE)
|
481
|
+
end
|
482
|
+
|
483
|
+
def channel_callback_hash(channel) # :nodoc:
|
484
|
+
raise "BUG" unless channel.keyFor(@selector)
|
485
|
+
#channel.register(@selector, 0) unless channel.keyFor(@selector)
|
486
|
+
k = channel.keyFor(@selector) # SelectionKey object
|
487
|
+
k.attach({}) unless k.attachment
|
488
|
+
k.attachment
|
489
|
+
end
|
490
|
+
|
491
|
+
class SocketChannelWrapper
|
492
|
+
def initialize(master, channel) # :nodoc:
|
493
|
+
@master = master
|
494
|
+
@channel = channel
|
495
|
+
end
|
496
|
+
|
497
|
+
def close
|
498
|
+
@master.send(:close_channel, @channel)
|
499
|
+
nil
|
500
|
+
end
|
501
|
+
|
502
|
+
def on_close(&callback)
|
503
|
+
@master.send(:set_on_close_callback, @channel, &callback)
|
504
|
+
self
|
505
|
+
end
|
506
|
+
|
507
|
+
def local_address
|
508
|
+
EventLoop.unparse_address(@channel.socket.getLocalSocketAddress)
|
509
|
+
end
|
510
|
+
|
511
|
+
protected
|
512
|
+
end
|
513
|
+
|
514
|
+
class ListeningSocketWrapper < SocketChannelWrapper
|
515
|
+
def on_accept(&callback)
|
516
|
+
@master.send(:set_on_accept_callback, @channel, &callback)
|
517
|
+
self
|
518
|
+
end
|
519
|
+
|
520
|
+
def on_accept_error(&callback)
|
521
|
+
@master.send(:set_on_accept_error_callback, @channel, &callback)
|
522
|
+
self
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# Connection wrapper object.
|
527
|
+
#
|
528
|
+
# This object is passed to the block given to EventLoop#on_accept
|
529
|
+
class ConnectionWrapper < SocketChannelWrapper
|
530
|
+
attr_reader :master
|
531
|
+
# Yield to the given block when another callback raises an exception.
|
532
|
+
def on_callback_error(&callback)
|
533
|
+
@master.send(:set_callback_error_callback, @channel, &callback)
|
534
|
+
self
|
535
|
+
end
|
536
|
+
def on_receive_bytes(&callback)
|
537
|
+
@master.send(:set_on_receive_bytes_callback, @channel, &callback)
|
538
|
+
self
|
539
|
+
end
|
540
|
+
def write(bytes, &completion_callback)
|
541
|
+
@master.send(:add_to_write_buffer, @channel, bytes, &completion_callback)
|
542
|
+
self
|
543
|
+
end
|
544
|
+
def close(options={})
|
545
|
+
if options[:hard]
|
546
|
+
@master.send(:close_channel, @channel)
|
547
|
+
else
|
548
|
+
@master.send(:close_channel_gracefully, @channel)
|
549
|
+
end
|
550
|
+
end
|
551
|
+
def remote_address
|
552
|
+
EventLoop.unparse_address(@channel.socket.getRemoteSocketAddress)
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
class OutgoingConnectionWrapper < ConnectionWrapper
|
557
|
+
def initialize(master, channel, address)
|
558
|
+
@address = address
|
559
|
+
super(master, channel)
|
560
|
+
end
|
561
|
+
|
562
|
+
def on_connect(&callback)
|
563
|
+
@master.send(:set_on_connect_callback, @channel, &callback)
|
564
|
+
self
|
565
|
+
end
|
566
|
+
|
567
|
+
def on_connect_error(&callback)
|
568
|
+
@master.send(:set_on_connect_error_callback, @channel, &callback)
|
569
|
+
self
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
class Timer
|
574
|
+
attr_reader :expire_at
|
575
|
+
def initialize(master, expire_at, callback, options={})
|
576
|
+
@master = master
|
577
|
+
@expire_at = expire_at
|
578
|
+
@callback = callback
|
579
|
+
@daemon = !!options[:daemon] # if true, we won't hold up the main loop (just like a daemon thread)
|
580
|
+
end
|
581
|
+
|
582
|
+
def daemon?
|
583
|
+
@daemon
|
584
|
+
end
|
585
|
+
|
586
|
+
def cancel
|
587
|
+
@master.send(:cancel_timer, self)
|
588
|
+
end
|
589
|
+
|
590
|
+
private
|
591
|
+
attr_reader :callback
|
592
|
+
end
|
593
|
+
|
594
|
+
# Convert InetSocketAddress to [host, port]
|
595
|
+
def self.unparse_address(socket_address)
|
596
|
+
return nil unless socket_address
|
597
|
+
[socket_address.getAddress.getHostAddress, socket_address.getPort]
|
598
|
+
end
|
599
|
+
|
600
|
+
# Convert [host, port] to InetSocketAddress
|
601
|
+
def self.parse_address(address)
|
602
|
+
return nil unless address
|
603
|
+
raise TypeError.new("address must be [host, port], not #{address.inspect}") unless address.length == 2
|
604
|
+
host, port = address
|
605
|
+
raise TypeError.new("address must be [host, port], not #{address.inspect}") unless host.is_a? String and port.is_a? Integer
|
606
|
+
InetSocketAddress.new(host, port)
|
607
|
+
end
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|