scriptty 0.5.0-java
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/.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
|