scriptty 0.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.gitattributes +1 -0
  2. data/.gitignore +3 -0
  3. data/COPYING +674 -0
  4. data/COPYING.LESSER +165 -0
  5. data/README.rdoc +31 -0
  6. data/Rakefile +49 -0
  7. data/VERSION +1 -0
  8. data/bin/scriptty-capture +5 -0
  9. data/bin/scriptty-dump-screens +4 -0
  10. data/bin/scriptty-replay +5 -0
  11. data/bin/scriptty-term-test +4 -0
  12. data/bin/scriptty-transcript-parse +4 -0
  13. data/examples/captures/xterm-overlong-line-prompt.bin +9 -0
  14. data/examples/captures/xterm-vim-session.bin +262 -0
  15. data/examples/demo-capture.rb +19 -0
  16. data/examples/telnet-nego.rb +55 -0
  17. data/lib/scriptty/apps/capture_app/console.rb +104 -0
  18. data/lib/scriptty/apps/capture_app/password_prompt.rb +65 -0
  19. data/lib/scriptty/apps/capture_app.rb +213 -0
  20. data/lib/scriptty/apps/dump_screens_app.rb +166 -0
  21. data/lib/scriptty/apps/replay_app.rb +229 -0
  22. data/lib/scriptty/apps/term_test_app.rb +124 -0
  23. data/lib/scriptty/apps/transcript_parse_app.rb +143 -0
  24. data/lib/scriptty/cursor.rb +39 -0
  25. data/lib/scriptty/exception.rb +38 -0
  26. data/lib/scriptty/expect.rb +392 -0
  27. data/lib/scriptty/multiline_buffer.rb +192 -0
  28. data/lib/scriptty/net/event_loop.rb +610 -0
  29. data/lib/scriptty/screen_pattern/generator.rb +398 -0
  30. data/lib/scriptty/screen_pattern/parser.rb +558 -0
  31. data/lib/scriptty/screen_pattern.rb +104 -0
  32. data/lib/scriptty/term/dg410/dg410-client-escapes.txt +37 -0
  33. data/lib/scriptty/term/dg410/dg410-escapes.txt +82 -0
  34. data/lib/scriptty/term/dg410/parser.rb +162 -0
  35. data/lib/scriptty/term/dg410.rb +489 -0
  36. data/lib/scriptty/term/xterm/xterm-escapes.txt +73 -0
  37. data/lib/scriptty/term/xterm.rb +661 -0
  38. data/lib/scriptty/term.rb +40 -0
  39. data/lib/scriptty/util/fsm/definition_parser.rb +111 -0
  40. data/lib/scriptty/util/fsm/scriptty_fsm_definition.treetop +189 -0
  41. data/lib/scriptty/util/fsm.rb +177 -0
  42. data/lib/scriptty/util/transcript/reader.rb +96 -0
  43. data/lib/scriptty/util/transcript/writer.rb +111 -0
  44. data/test/apps/capture_app_test.rb +123 -0
  45. data/test/apps/transcript_parse_app_test.rb +118 -0
  46. data/test/cursor_test.rb +51 -0
  47. data/test/fsm_definition_parser_test.rb +220 -0
  48. data/test/fsm_test.rb +322 -0
  49. data/test/multiline_buffer_test.rb +275 -0
  50. data/test/net/event_loop_test.rb +402 -0
  51. data/test/screen_pattern/generator_test.rb +408 -0
  52. data/test/screen_pattern/parser_test/explicit_cursor_pattern.txt +14 -0
  53. data/test/screen_pattern/parser_test/explicit_fields.txt +22 -0
  54. data/test/screen_pattern/parser_test/multiple_patterns.txt +42 -0
  55. data/test/screen_pattern/parser_test/simple_pattern.txt +14 -0
  56. data/test/screen_pattern/parser_test/truncated_heredoc.txt +12 -0
  57. data/test/screen_pattern/parser_test/utf16bebom_pattern.bin +0 -0
  58. data/test/screen_pattern/parser_test/utf16lebom_pattern.bin +0 -0
  59. data/test/screen_pattern/parser_test/utf8_pattern.bin +14 -0
  60. data/test/screen_pattern/parser_test/utf8_unix_pattern.bin +14 -0
  61. data/test/screen_pattern/parser_test/utf8bom_pattern.bin +14 -0
  62. data/test/screen_pattern/parser_test.rb +266 -0
  63. data/test/term/dg410/parser_test.rb +139 -0
  64. data/test/term/xterm_test.rb +327 -0
  65. data/test/test_helper.rb +3 -0
  66. data/test/util/transcript/reader_test.rb +131 -0
  67. data/test/util/transcript/writer_test.rb +126 -0
  68. data/test.watchr +29 -0
  69. 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