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.
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