tcp-server 1.0.1-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/lib/client.rb ADDED
@@ -0,0 +1,578 @@
1
+ #! /usr/bin/env jruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # =begin
10
+ #
11
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
12
+ #
13
+ # =end
14
+
15
+ require 'optparse'
16
+
17
+ require 'java'
18
+ require 'netty'
19
+
20
+ require 'log'
21
+
22
+ # The Client module
23
+ module Client
24
+ VERSION = '1.0.1'.freeze unless defined?(VERSION)
25
+ end
26
+
27
+ # The Client module
28
+ module Client
29
+ # The Config module
30
+ module Config
31
+ DEFAULTS = {
32
+ port: 8080,
33
+ host: 'localhost',
34
+ ssl: false,
35
+ quit_commands: %i[bye quit],
36
+ log_level: Logger::INFO
37
+ }.freeze
38
+ end
39
+ end
40
+
41
+ # The Client module
42
+ module Client
43
+ java_import Java::io.netty.bootstrap.Bootstrap
44
+ java_import Java::io.netty.channel.ChannelOption
45
+ java_import Java::io.netty.channel.nio.NioEventLoopGroup
46
+ java_import Java::io.netty.handler.ssl.SslContextBuilder
47
+ java_import Java::io.netty.handler.ssl.util.InsecureTrustManagerFactory
48
+
49
+ # The InitializationMethods module
50
+ module InitializationMethods
51
+ def init(options)
52
+ @options = options
53
+ @host = @options[:host]
54
+ @port = @options[:port]
55
+ @queue = java.util.concurrent.LinkedBlockingQueue.new
56
+ end
57
+
58
+ def bootstrap
59
+ @bootstrap = Bootstrap.new
60
+ @bootstrap.group(client_group)
61
+ @bootstrap.channel(::TCP::CHANNEL_TYPE)
62
+ @bootstrap.option(ChannelOption::TCP_NODELAY, true)
63
+ @bootstrap.handler(logging_handler) if @options[:log_requests]
64
+ @bootstrap.handler(channel_initializer)
65
+ end
66
+
67
+ def client_group
68
+ @client_group ||= NioEventLoopGroup.new
69
+ end
70
+
71
+ def channel_initializer
72
+ @channel_initializer ||= ::Client::ChannelInitializer.new(@options)
73
+ end
74
+
75
+ def logging_handler
76
+ @logging_handler ||= LoggingHandler.new(LogLevel::INFO)
77
+ end
78
+
79
+ def configure_handlers(*handlers, &block)
80
+ ::Client::ChannelInitializer::DefaultHandler.add_listener(self)
81
+ listeners.addAll(handlers)
82
+ @user_app = block
83
+ @application_handler = lambda do |ctx, msg|
84
+ if @user_app.nil? || @user_app.arity == 1
85
+ @queue.add(msg.chomp)
86
+ else
87
+ @user_app.call(ctx, msg)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ # module InitializationMethods
93
+ end
94
+ # module Client
95
+
96
+ # The Client module
97
+ module Client
98
+ java_import Java::io.netty.channel.AbstractChannel
99
+
100
+ # The InstanceMethods module
101
+ module InstanceMethods
102
+ def puts(msg)
103
+ sleep 0.1 until @channel.isActive()
104
+ msg.chomp!
105
+ log.trace "#puts msg: #{msg.inspect}"
106
+ raise 'Message is empty!' if msg.nil? || msg.empty?
107
+ @last_write_future = @channel.writeAndFlush("#{msg}\n")
108
+ end
109
+
110
+ def gets
111
+ log.debug 'Waiting for response from server'
112
+ @queue.take
113
+ rescue StandardError => e
114
+ warn "Unexpected error waiting for message: #{e.message}"
115
+ nil
116
+ end
117
+
118
+ def connect(host = @options[:host], port = @options[:port])
119
+ return unless @channel.nil?
120
+ # Start the connection attempt.
121
+ @channel = bootstrap.connect(host, port).sync().channel()
122
+ rescue AbstractChannel::AnnotatedConnectException => e
123
+ raise e.message
124
+ rescue StandardError => e
125
+ raise "Connection failure: #{e.message}"
126
+ end
127
+
128
+ def close(channel = @channel)
129
+ log.debug "Closing client channel: #{channel}"
130
+ channel.closeFuture().sync()
131
+ # Wait until all messages are flushed before closing the channel.
132
+ @last_write_future&.sync()
133
+ ensure
134
+ shutdown
135
+ end
136
+
137
+ def shutdown
138
+ log.debug 'Shutting down gracefully'
139
+ @client_group&.shutdownGracefully()
140
+ ensure
141
+ client_has_shut_down
142
+ end
143
+
144
+ def session
145
+ when_client_has_shut_down(@client_group) do |group|
146
+ log.debug "Channel group has shut down: #{group.inspect}"
147
+ end
148
+ @user_app.nil? ? read_user_commands : invoke_user_app
149
+ end
150
+
151
+ def invoke_user_app
152
+ @user_app&.call(self)
153
+ ensure
154
+ close
155
+ end
156
+
157
+ def shut_down_callbacks
158
+ @shut_down_callbacks ||= []
159
+ end
160
+
161
+ def when_client_has_shut_down(*args, &block)
162
+ shut_down_callbacks << {
163
+ block: block,
164
+ args: args
165
+ }
166
+ self
167
+ end
168
+
169
+ def client_has_shut_down
170
+ shut_down_callbacks.take_while do |callback|
171
+ callback[:block]&.call(*callback.fetch(:args, []))
172
+ end
173
+ rescue StandardError => e
174
+ log.error e.message
175
+ end
176
+
177
+ def channel_unregistered(ctx)
178
+ log.trace "##{__method__} channel: #{ctx.channel}"
179
+ shutdown
180
+ end
181
+
182
+ def message_received(ctx, message)
183
+ notify :message_received, ctx, message
184
+ if @application_handler.nil?
185
+ $stdout.puts message.chomp unless message.nil?
186
+ else
187
+ @application_handler.call(ctx, message)
188
+ end
189
+ end
190
+
191
+ def execute_command(str, client = self)
192
+ return if str.empty?
193
+ client.puts str
194
+ close if @options[:quit_commands].include?(str.downcase.to_sym)
195
+ end
196
+
197
+ def read_user_commands
198
+ log.trace 'Reading user commands'
199
+ loop do
200
+ input = $stdin.gets
201
+ raise 'Poll failure from stdin' if input.nil?
202
+ break unless @channel.active?
203
+ break if execute_command(input).is_a?(AbstractChannel::CloseFuture)
204
+ end
205
+ end
206
+
207
+ IdentiferTemplate = '#<%<class>s:0x%<id>s @options=%<opts>s>'.freeze
208
+
209
+ def to_s
210
+ format(IdentiferTemplate, class: self.class.name, id: object_id.to_s(16), opts: @options.to_s)
211
+ end
212
+ end
213
+ # module InstanceMethods
214
+ end
215
+ # module Client
216
+
217
+ # The Client module
218
+ module Client
219
+ # The Listenable module
220
+ module Listenable
221
+ def listeners
222
+ @listeners ||= java.util.concurrent.CopyOnWriteArrayList.new
223
+ end
224
+
225
+ def add_listener(listener)
226
+ listeners << listener
227
+ end
228
+
229
+ def remove_listener(listener)
230
+ listeners.delete(listener)
231
+ end
232
+
233
+ def notify(message, *args)
234
+ return if listeners.empty?
235
+ log.trace "Notifying listeners (#{listeners}) of message: #{message}"
236
+ listeners.each do |listener|
237
+ listener.send(message.to_sym, *args) if listener.respond_to?(message.to_sym)
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ # The Client module
244
+ module Client
245
+ # java_import Java::io.netty.channel.ChannelInboundHandlerAdapter
246
+ java_import Java::io.netty.channel.SimpleChannelInboundHandler
247
+
248
+ # The ModularHandler class
249
+ # class ModularHandler < ChannelInboundHandlerAdapter
250
+ class ModularHandler < SimpleChannelInboundHandler
251
+ include ::Client::Listenable
252
+
253
+ def isSharable
254
+ true
255
+ end
256
+
257
+ def channelRegistered(ctx)
258
+ log.trace "##{__method__} channel: #{ctx.channel}"
259
+ notify :channel_registered, ctx
260
+ super(ctx)
261
+ end
262
+
263
+ def channelUnregistered(ctx)
264
+ log.trace "##{__method__} channel: #{ctx.channel}"
265
+ notify :channel_unregistered, ctx
266
+ super(ctx)
267
+ end
268
+
269
+ def channelActive(ctx)
270
+ ::Client.log.info "Channel active #{ctx.channel}"
271
+ notify :channel_active, ctx
272
+ super(ctx)
273
+ end
274
+
275
+ def channelInactive(ctx)
276
+ log.trace "##{__method__} channel: #{ctx.channel}"
277
+ notify :channel_inactive, ctx
278
+ super(ctx)
279
+ end
280
+
281
+ def messageReceived(ctx, msg)
282
+ log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
283
+ notify :message_received, ctx, msg
284
+ end
285
+
286
+ def channelRead(ctx, msg)
287
+ log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
288
+ notify :channel_read, ctx, msg
289
+ super(ctx, msg)
290
+ end
291
+
292
+ # Please keep in mind that this method will be renamed to
293
+ # messageReceived(ChannelHandlerContext, I) in 5.0.
294
+ #
295
+ # java_signature 'protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception'
296
+ def channelRead0(ctx, msg)
297
+ log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
298
+ messageReceived(ctx, msg)
299
+ end
300
+
301
+ def channelReadComplete(ctx)
302
+ log.trace "##{__method__} channel: #{ctx.channel}"
303
+ notify :channel_read_complete, ctx
304
+ super(ctx)
305
+ end
306
+
307
+ def channelWritabilityChanged(ctx)
308
+ log.trace "##{__method__} channel: #{ctx.channel}"
309
+ notify :channel_writability_changed, ctx
310
+ super(ctx)
311
+ end
312
+
313
+ def userEventTriggered(ctx, evt)
314
+ log.trace "##{__method__} channel: #{ctx.channel}, event: #{evt}"
315
+ notify :user_event_triggered, ctx, evt
316
+ super(ctx, evt)
317
+ end
318
+
319
+ def exceptionCaught(ctx, cause)
320
+ ::Client.log.warn "##{__method__} channel: #{ctx.channel}, cause: #{cause.message}"
321
+ listeners = notify :exception_caught, ctx, cause
322
+ super(ctx, cause) if listeners.empty?
323
+ end
324
+
325
+ IdentiferTemplate = '#<%<class>s:0x%<id>s>'.freeze
326
+
327
+ def to_s
328
+ format(IdentiferTemplate, class: self.class.name, id: object_id.to_s(16))
329
+ end
330
+ alias inspect to_s
331
+ end
332
+ # class ModularHandler
333
+ end
334
+ # module Client
335
+
336
+ # The Client module
337
+ module Client
338
+ java_import Java::io.netty.handler.codec.DelimiterBasedFrameDecoder
339
+ java_import Java::io.netty.handler.codec.Delimiters
340
+ java_import Java::io.netty.handler.codec.string.StringDecoder
341
+ java_import Java::io.netty.handler.codec.string.StringEncoder
342
+
343
+ # The ChannelInitializer class
344
+ class ChannelInitializer < Java::io.netty.channel.ChannelInitializer
345
+ DefaultHandler = ::Client::ModularHandler.new
346
+ FrameDecoderBufferSize = 8192 # bytes
347
+ # The encoder and decoder are sharable. If they were not, then
348
+ # constant definitions could not be used.
349
+ Decoder = StringDecoder.new
350
+ Encoder = StringEncoder.new
351
+ attr_accessor :handlers
352
+
353
+ def initialize(options = {})
354
+ super()
355
+ @options = options
356
+ @host = @options[:host]
357
+ @port = @options[:port]
358
+ @handlers = []
359
+ end
360
+
361
+ def <<(handler)
362
+ @handlers << handler
363
+ end
364
+
365
+ def initChannel(channel)
366
+ pipeline = channel.pipeline
367
+ pipeline.addLast(ssl_handler(channel)) if @options[:ssl]
368
+ pipeline.addLast(
369
+ DelimiterBasedFrameDecoder.new(FrameDecoderBufferSize, Delimiters.lineDelimiter()),
370
+ Decoder,
371
+ Encoder
372
+ )
373
+ add_user_handlers(pipeline)
374
+ pipeline.addLast(DefaultHandler)
375
+ end
376
+
377
+ protected
378
+
379
+ def add_user_handlers(pipeline)
380
+ @handlers.each do |handler|
381
+ case handler
382
+ when Class then pipeline.addLast(handler.new)
383
+ when Proc then pipeline.addLast(Server::MessageHandler.new(&handler))
384
+ else pipeline.addLast(handler)
385
+ end
386
+ end
387
+ end
388
+
389
+ private
390
+
391
+ def ssl_context
392
+ return @ssl_ctx unless @ssl_ctx.nil?
393
+ log.debug 'Initializing SSL context'
394
+ builder = SslContextBuilder.forClient()
395
+ @ssl_ctx = builder.trustManager(InsecureTrustManagerFactory::INSTANCE).build()
396
+ end
397
+
398
+ def ssl_handler(channel, ssl_ctx = ssl_context)
399
+ return ssl_ctx.newHandler(channel.alloc(), @host, @port) if ssl_ctx.respond_to?(:newHandler)
400
+ log.warn 'The SSL context did not provide a handler initializer'
401
+ log.warn 'Creating handler with SSL engine of the context'
402
+ SslHandler.new(ssl_engine(ssl_ctx))
403
+ end
404
+
405
+ def ssl_engine(ssl_ctx)
406
+ ssl_engine = ssl_ctx.createSSLEngine()
407
+ ssl_engine.setUseClientMode(true) # Client mode
408
+ ssl_engine.setNeedClientAuth(false)
409
+ ssl_engine
410
+ end
411
+ end
412
+ # class ChannelInitializer
413
+ end
414
+ # module Client
415
+
416
+ # The TCP module
417
+ module TCP
418
+ CHANNEL_TYPE = Java::io.netty.channel.socket.nio.NioSocketChannel.java_class
419
+
420
+ # The Client class
421
+ class Client
422
+ include ::Client::InitializationMethods
423
+ include ::Client::InstanceMethods
424
+ include ::Client::Listenable
425
+
426
+ def initialize(options = {}, *handlers, &block)
427
+ init(::Client::Config::DEFAULTS.merge(options))
428
+ configure_handlers(*handlers, &block)
429
+ connect
430
+ session
431
+ end
432
+
433
+ IdentiferTemplate = '#<%<class>s:0x%<id>s>'.freeze
434
+
435
+ def to_s
436
+ format(IdentiferTemplate, class: self.class.name, id: object_id.to_s(16))
437
+ end
438
+ alias inspect to_s
439
+ end
440
+ end
441
+ # module Client
442
+
443
+ # The Client module
444
+ module Client
445
+ # The ArgumentsParser class
446
+ class ArgumentsParser
447
+ Flags = %i[banner port ssl log_level help version].freeze
448
+ attr_reader :parser, :options
449
+
450
+ def initialize(parser = OptionParser.new, options = ::Client::Config::DEFAULTS.dup)
451
+ @parser = parser
452
+ @options = options
453
+ Flags.each { |method_name| method(method_name).call }
454
+ end
455
+
456
+ def banner
457
+ @parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [host] [port] [options]"
458
+ @parser.separator ''
459
+ @parser.separator 'Options:'
460
+ end
461
+
462
+ def host
463
+ description = "Connect to server at this host; default: #{@options[:host]}"
464
+ @parser.on('-h', '--host=<host>s', description) do |v|
465
+ @options[:host] = v
466
+ end
467
+ end
468
+
469
+ IntegerPattern = /^\d+$/.freeze
470
+
471
+ def validated_port(val)
472
+ raise "Invalid port: #{v}" unless IntegerPattern.match?(val.to_s) && val.positive? && val < 65_536
473
+ val
474
+ end
475
+
476
+ def port
477
+ description = "Connect to server at this port; default: #{@options[:port]}"
478
+ @parser.on('-p', '--port=<port>', Integer, description) do |v|
479
+ @options[:port] = validated_port(v).to_i
480
+ end
481
+ end
482
+
483
+ def ssl
484
+ @parser.on('--ssl', "Secure connection with TLSv1.3; default: #{@options[:ssl]}") do
485
+ @options[:ssl] = true
486
+ end
487
+ end
488
+
489
+ def log_level
490
+ @parser.on_tail('-v', '--verbose', 'Increase verbosity') do
491
+ @options[:log_level] -= 1
492
+ end
493
+ end
494
+
495
+ def help
496
+ @parser.on_tail('-?', '--help', 'Show this message') do
497
+ puts @parser
498
+ exit
499
+ end
500
+ end
501
+
502
+ def version
503
+ @parser.on_tail('--version', 'Show version') do
504
+ puts "#{$PROGRAM_NAME} version #{Version}"
505
+ exit
506
+ end
507
+ end
508
+ end
509
+ # class ArgumentsParser
510
+
511
+ # rubocop: disable Metrics/AbcSize
512
+ def parse_arguments(arguments_parser = ArgumentsParser.new)
513
+ arguments_parser.parser.parse!(ARGV)
514
+ arguments_parser.options[:host] = v.to_s unless (v = ARGV.shift).nil?
515
+ arguments_parser.options[:port] = validated_port(v).to_i unless (v = ARGV.shift).nil?
516
+ arguments_parser.options
517
+ rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
518
+ abort e.message
519
+ end
520
+ # rubocop: enable Metrics/AbcSize
521
+ end
522
+ # module Client
523
+
524
+ # The Client module
525
+ module Client
526
+ # The Monitor class
527
+ class Monitor
528
+ def exception_caught(ctx, cause)
529
+ log.warn "Unexpected exception from downstream channel #{ctx.channel}: #{cause.message}"
530
+ end
531
+
532
+ def channel_registered(ctx)
533
+ log.info "Channel registered: #{ctx.channel}"
534
+ end
535
+ end
536
+ end
537
+ # module Client
538
+
539
+ # The Client module
540
+ module Client
541
+ # The ConsoleHandler class
542
+ class ConsoleHandler
543
+ def message_received(ctx, message)
544
+ log.trace "##{__method__} channel: #{ctx.channel}, message: #{message}"
545
+ log.debug "Received message: #{message}"
546
+ puts message.chomp unless message.nil?
547
+ end
548
+ end
549
+ end
550
+ # module Client
551
+
552
+ # The Client module
553
+ module Client
554
+ InterruptTemplate = "\r%<class>s".freeze
555
+
556
+ # rubocop: disable Metrics/AbcSize
557
+ # rubocop: disable Metrics/MethodLength
558
+ def main(args = parse_arguments)
559
+ Logging.log_level = args[:log_level]
560
+ handlers = [::Client::Monitor.new, ::Client::ConsoleHandler.new]
561
+ ::TCP::Client.new(args, *handlers)
562
+ rescue Interrupt => e
563
+ warn format(InterruptTemplate, class: e.class)
564
+ exit
565
+ rescue RuntimeError => e
566
+ ::Client.log.fatal e.message
567
+ exit 1
568
+ rescue StandardError => e
569
+ ::Client.log.fatal "Unexpected error: #{e.class}: #{e.message}"
570
+ e.backtrace.each { |t| log.debug t } if Logging.log_level == Logger::DEBUG
571
+ exit 1
572
+ end
573
+ # rubocop: enable Metrics/AbcSize
574
+ # rubocop: enable Metrics/MethodLength
575
+ end
576
+ # module Client
577
+
578
+ Object.new.extend(Client).main if $PROGRAM_NAME == __FILE__