tcp-server 1.0.1-java

Sign up to get free protection for your applications and to get access to all the features.
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__