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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +123 -0
- data/Rakefile +56 -0
- data/exe/tcp_server +17 -0
- data/lib/client.rb +578 -0
- data/lib/log.rb +207 -0
- data/lib/server/argument_parser.rb +106 -0
- data/lib/server/channel_initializer.rb +124 -0
- data/lib/server/config.rb +30 -0
- data/lib/server/instance_methods.rb +91 -0
- data/lib/server/listenable.rb +41 -0
- data/lib/server/message_handler.rb +56 -0
- data/lib/server/modular_handler.rb +119 -0
- data/lib/server/server.rb +51 -0
- data/lib/server/shutdown_hook.rb +35 -0
- data/lib/server/version.rb +16 -0
- data/lib/server.rb +59 -0
- metadata +148 -0
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__
|