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/log.rb ADDED
@@ -0,0 +1,207 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # =begin
5
+
6
+ # Copyright Nels Nelson 2016-2019 but freely usable (see license)
7
+
8
+ # =end
9
+
10
+ require 'java'
11
+ require 'logger'
12
+
13
+ require 'log4j-2'
14
+
15
+ # The LogInitialization module
16
+ module LogInitialization
17
+ LibDirPath = File.expand_path(__dir__) unless defined?(LibDirPath)
18
+ ProjectDirPath = File.expand_path(File.dirname(LibDirPath)) unless defined?(ProjectDirPath)
19
+ LogsDirPath = File.expand_path(File.join(ProjectDirPath, 'logs'))
20
+ Dir.mkdir(LogsDirPath) unless File.exist?(LogsDirPath)
21
+ ServerLogFile = File.join(LogsDirPath, 'server.log')
22
+ RollLogFileNameTemplate = 'server-%d{yyyy-MM-dd}.log.gz'.freeze
23
+ RollingLogFilePath = File.join(LogsDirPath, RollLogFileNameTemplate)
24
+ File.write(ServerLogFile, '') unless File.file? ServerLogFile
25
+ LoggerPatternTemplate = '%d{ABSOLUTE} %-5p [%c{1}] %m%n'.freeze
26
+
27
+ # rubocop: disable Metrics/AbcSize
28
+ # rubocop: disable Metrics/MethodLength
29
+ def self.init_log4j(log_level = org.apache.logging.log4j.Level::INFO)
30
+ java.lang::System.setProperty('log4j.shutdownHookEnabled', java.lang::Boolean.toString(false))
31
+ factory = org.apache.logging.log4j.core.config.builder.api::ConfigurationBuilderFactory
32
+ config = factory.newConfigurationBuilder()
33
+
34
+ log_level = org.apache.logging.log4j.Level.to_level(log_level.to_s.upcase) if log_level.is_a?(Symbol)
35
+ config.setStatusLevel(log_level)
36
+ config.setConfigurationName('websocket')
37
+
38
+ # create a console appender
39
+ target = org.apache.logging.log4j.core.appender::ConsoleAppender::Target::SYSTEM_OUT
40
+ layout = config.newLayout('PatternLayout')
41
+ layout = layout.addAttribute('pattern', LoggerPatternTemplate)
42
+ appender = config.newAppender('stdout', 'CONSOLE')
43
+ appender = appender.addAttribute('target', target)
44
+ appender = appender.add(layout)
45
+ config.add(appender)
46
+
47
+ # create a root logger
48
+ root_logger = config.newRootLogger(log_level)
49
+ root_logger = root_logger.add(config.newAppenderRef('stdout'))
50
+
51
+ # create a rolling file appender
52
+ cron = config.newComponent('CronTriggeringPolicy')
53
+ cron = cron.addAttribute('schedule', '0 0 0 * * ?')
54
+
55
+ size = config.newComponent('SizeBasedTriggeringPolicy')
56
+ size = size.addAttribute('size', '100M')
57
+
58
+ policies = config.newComponent('Policies')
59
+ policies = policies.addComponent(cron)
60
+ policies = policies.addComponent(size)
61
+
62
+ appender = config.newAppender('rolling_file', 'RollingFile')
63
+ appender = appender.addAttribute('fileName', ServerLogFile)
64
+ appender = appender.addAttribute('filePattern', RollingLogFilePath)
65
+ appender = appender.add(layout)
66
+ appender = appender.addComponent(policies)
67
+ config.add(appender)
68
+
69
+ root_logger = root_logger.addAttribute('additivity', false)
70
+ root_logger = root_logger.add(config.newAppenderRef('rolling_file'))
71
+ config.add(root_logger)
72
+
73
+ logging_configuration = config.build()
74
+ ctx = org.apache.logging.log4j.core.config::Configurator.initialize(logging_configuration)
75
+ ctx.updateLoggers()
76
+ end
77
+ # rubocop: enable Metrics/AbcSize
78
+ # rubocop: enable Metrics/MethodLength
79
+ # def init_log4j
80
+ end
81
+ # module LogInitialization
82
+
83
+ ::LogInitialization.init_log4j if defined? Java
84
+
85
+ # The Apache log4j Logger class
86
+ # rubocop: disable Style/ClassAndModuleChildren
87
+ class org.apache.logging.log4j.core::Logger
88
+ alias log4j_error error
89
+ def error(error_or_message, error = nil)
90
+ return extract_backtrace(error_or_message) if error.nil?
91
+ log4j_error(generate_message(error_or_message, error))
92
+ extract_backtrace(error)
93
+ end
94
+
95
+ def generate_message(error_or_message, error)
96
+ error_message = "#{error_or_message}: #{error.class.name}"
97
+ error_message << ": #{error.message}" if error.respond_to?(:message)
98
+ error_message
99
+ end
100
+
101
+ def extract_backtrace(error, default_result = nil)
102
+ log4j_error(error)
103
+ if error.respond_to?(:backtrace)
104
+ error.backtrace.each { |trace| log4j_error(trace) unless trace.nil? }
105
+ elsif error.respond_to?(:getStackTrace)
106
+ error.getStackTrace().each { |trace| log4j_error(trace) unless trace.nil? }
107
+ else
108
+ default_result
109
+ end
110
+ end
111
+ end
112
+ # rubocop: enable Style/ClassAndModuleChildren
113
+
114
+ # The Logging module
115
+ module Logging
116
+ # rubocop: disable Style/MutableConstant
117
+ Configuration = {
118
+ level: Logger::INFO
119
+ }
120
+ # rubocop: enable Style/MutableConstant
121
+
122
+ def self.log_level=(log_level)
123
+ Logging::Configuration[:level] = log_level
124
+ end
125
+
126
+ def self.log_level
127
+ Logging::Configuration[:level]
128
+ end
129
+
130
+ def log(level = Logging.log_level, log_name = nil)
131
+ @log ||= init_logger(level, log_name)
132
+ end
133
+ alias logger log
134
+
135
+ protected
136
+
137
+ def init_logger(level = Logging.log_level, logger_name = nil)
138
+ return init_java_logger(level, logger_name, caller[2]) if defined?(Java)
139
+ Logging.init_ruby_logger(level, logger_name)
140
+ end
141
+
142
+ def init_ruby_logger(level, logger_name = nil)
143
+ logger_instance = Logger.new(logger_name)
144
+ logger_instance.level = level
145
+ logger_instance
146
+ end
147
+
148
+ # rubocop: disable Metrics/AbcSize
149
+ def init_java_logger(level, logger_name = nil, source_location = nil)
150
+ logger_name = get_formatted_logger_name(logger_name)
151
+ logger_name = source_location.split(/\//).last if logger_name.empty?
152
+ level_name = symbolize_numeric_log_level(level).to_s.upcase
153
+ logger_instance = org.apache.logging.log4j.LogManager.getLogger(logger_name)
154
+ logger_instance.level = org.apache.logging.log4j.Level.to_level(level_name)
155
+ logger_instance
156
+ end
157
+ # rubocop: enable Metrics/AbcSize
158
+
159
+ def get_formatted_logger_name(logger_name = nil)
160
+ return logger_name.to_s[/\w+$/] unless logger_name.nil?
161
+ return name[/\w+$/] if is_a?(Class) || is_a?(Module)
162
+ self.class.name[/\w+$/]
163
+ end
164
+
165
+ # rubocop: disable Metrics/CyclomaticComplexity
166
+ # OFF: 0
167
+ # FATAL: 100
168
+ # ERROR: 200
169
+ # WARN: 300
170
+ # INFO: 400
171
+ # DEBUG: 500
172
+ # TRACE: 600
173
+ # ALL: 2147483647
174
+ # See: https://logging.apache.org/log4j/2.x/log4j-api/apidocs/org/apache/logging/log4j/Level.html
175
+ def symbolize_numeric_log_level(level)
176
+ case level
177
+ when 5..Float::INFINITY then :off
178
+ when 4 then :fatal
179
+ when 3 then :error
180
+ when 2 then :warn
181
+ when 1 then :info
182
+ when 0 then :debug
183
+ when -1 then :trace
184
+ when -2..-Float::INFINITY then :all
185
+ end
186
+ end
187
+ # rubocop: enable Metrics/CyclomaticComplexity
188
+ end
189
+ # module Logging
190
+
191
+ # The Module class
192
+ class Module
193
+ # Universally include Logging
194
+ include ::Logging
195
+ end
196
+
197
+ # The Class class
198
+ class Class
199
+ # Universally include Logging
200
+ include ::Logging
201
+ end
202
+
203
+ # The Object class
204
+ class Object
205
+ # Universally include Logging
206
+ include ::Logging
207
+ end
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require 'optparse'
14
+
15
+ require_relative 'config'
16
+
17
+ # The Server module
18
+ module Server
19
+ # The ArgumentsParser class
20
+ class ArgumentsParser
21
+ Flags = %i[
22
+ banner port ssl idle_reading idle_writing log_requests log_level help
23
+ version
24
+ ].freeze
25
+ attr_reader :parser, :options
26
+
27
+ def initialize(option_parser = OptionParser.new)
28
+ @parser = option_parser
29
+ @options = ::Server::Config::DEFAULTS.dup
30
+ Flags.each { |method_name| method(method_name).call }
31
+ end
32
+
33
+ def banner
34
+ @parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [port] [options]"
35
+ @parser.separator ''
36
+ @parser.separator 'Options:'
37
+ end
38
+
39
+ IntegerPattern = /^\d+$/.freeze
40
+
41
+ def validated_port(val)
42
+ raise "Invalid port: #{v}" unless IntegerPattern.match?(val.to_s) && val.positive? && val < 65_536
43
+ val
44
+ end
45
+
46
+ def port
47
+ description = "Port on which to listen for connections; default: #{@options[:port]}"
48
+ @parser.on('-p', '--port=<port>', Integer, description) do |v|
49
+ @options[:port] = validated_port(v).to_i
50
+ end
51
+ end
52
+
53
+ def ssl
54
+ @parser.on('-s', '--ssl', "Enable SSL socket server; default: #{@options[:ssl]}") do
55
+ @options[:ssl] = true
56
+ end
57
+ end
58
+
59
+ def idle_reading
60
+ @parser.on('--idle-reading=seconds', 'Amount of time channel can idle without incoming data') do |v|
61
+ @options[:idle_reading] = v.to_i
62
+ end
63
+ end
64
+
65
+ def idle_writing
66
+ @parser.on('--idle-writing=seconds', 'Amount of time channel can idle without outgoing data') do |v|
67
+ @options[:idle_writing] = v.to_i
68
+ end
69
+ end
70
+
71
+ def log_requests
72
+ @parser.on('-r', '--log-requests', 'Include individual request info in log output') do
73
+ @options[:log_requests] = true
74
+ end
75
+ end
76
+
77
+ def log_level
78
+ @parser.on_tail('-v', '--verbose', 'Increase verbosity') do
79
+ @options[:log_level] -= 1
80
+ end
81
+ end
82
+
83
+ def help
84
+ @parser.on_tail('-?', '--help', 'Show this message') do
85
+ puts @parser
86
+ exit
87
+ end
88
+ end
89
+
90
+ def version
91
+ @parser.on_tail('--version', 'Show version') do
92
+ puts "#{File.basename($PROGRAM_NAME)} version #{::Server::VERSION}"
93
+ exit
94
+ end
95
+ end
96
+ end
97
+ # class ArgumentsParser
98
+
99
+ def parse_arguments(arguments_parser = ::Server::ArgumentsParser.new)
100
+ arguments_parser.parser.parse!(ARGV)
101
+ arguments_parser.options
102
+ rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
103
+ abort e.message
104
+ end
105
+ end
106
+ # module Server
@@ -0,0 +1,124 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require 'java'
14
+ require 'netty'
15
+
16
+ require_relative 'message_handler'
17
+ require_relative 'modular_handler'
18
+
19
+ # The Server module
20
+ module Server
21
+ java_import Java::io.netty.handler.codec.DelimiterBasedFrameDecoder
22
+ java_import Java::io.netty.handler.codec.Delimiters
23
+ java_import Java::io.netty.handler.codec.string.StringDecoder
24
+ java_import Java::io.netty.handler.codec.string.StringEncoder
25
+ java_import Java::io.netty.handler.ssl.SslContextBuilder
26
+ java_import Java::io.netty.handler.ssl.SslHandler
27
+ java_import Java::io.netty.handler.ssl.util.InsecureTrustManagerFactory
28
+ java_import Java::io.netty.handler.ssl.util.SelfSignedCertificate
29
+ java_import Java::io.netty.util.concurrent.FutureListener
30
+
31
+ # The ChannelInitializer class
32
+ class ChannelInitializer < Java::io.netty.channel.ChannelInitializer
33
+ DefaultHandler = ModularHandler.new
34
+ FrameDecoderBufferBytesSize = 8192
35
+ # The encoder and decoder are sharable. If they were not, then
36
+ # constant definitions could not be used.
37
+ Decoder = StringDecoder.new
38
+ Encoder = StringEncoder.new
39
+ attr_accessor :handlers
40
+ attr_reader :options
41
+
42
+ def initialize(options = {})
43
+ super()
44
+ @options = options
45
+ @handlers = []
46
+ end
47
+
48
+ def <<(handler)
49
+ @handlers << handler
50
+ end
51
+
52
+ def initChannel(channel)
53
+ pipeline = channel.pipeline
54
+ pipeline.addLast(ssl_handler(channel)) if @options[:ssl]
55
+ pipeline.addLast(
56
+ DelimiterBasedFrameDecoder.new(FrameDecoderBufferBytesSize, Delimiters.lineDelimiter),
57
+ Decoder,
58
+ Encoder
59
+ )
60
+ add_user_handlers(pipeline)
61
+ pipeline.addLast(DefaultHandler)
62
+ end
63
+
64
+ protected
65
+
66
+ def add_user_handlers(pipeline)
67
+ @handlers.each do |handler|
68
+ case handler
69
+ when Class then pipeline.addLast(handler.new)
70
+ when Proc then pipeline.addLast(Server::MessageHandler.new(&handler))
71
+ else pipeline.addLast(handler)
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def ssl_context
79
+ return @ssl_ctx unless @ssl_ctx.nil?
80
+ log.debug 'Initializing SSL context'
81
+ require 'bouncycastle'
82
+ ssc = SelfSignedCertificate.new
83
+ builder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
84
+ @ssl_ctx = builder.trustManager(InsecureTrustManagerFactory::INSTANCE).build()
85
+ end
86
+
87
+ def ssl_handler_instance(channel, ssl_ctx = ssl_context)
88
+ return ssl_ctx.newHandler(channel.alloc()) if ssl_ctx.respond_to?(:newHandler)
89
+ log.warn 'The SSL context did not provide a handler initializer'
90
+ log.warn 'Creating handler with SSL engine of the context'
91
+ SslHandler.new(ssl_engine(ssl_ctx))
92
+ end
93
+
94
+ def ssl_handler(channel)
95
+ handler = ssl_handler_instance(channel)
96
+ handler.handshake_future.addListener(SslHandshakeFutureListener.new)
97
+ handler
98
+ end
99
+
100
+ # The SslHandshakeFutureListener class
101
+ class SslHandshakeFutureListener
102
+ include FutureListener
103
+ # @Override
104
+ #
105
+ # public void operationComplete(Future<Channel> future) throws Exception
106
+ def operationComplete(future)
107
+ raise future.cause unless future.success?
108
+ session = future.now.pipeline.get('SslHandler#0')&.engine&.session
109
+ ::Server.log.info "Channel protocol: #{session.protocol}, cipher suite: #{session.cipher_suite}"
110
+ rescue StandardError => e
111
+ ::Server.log.warn e.message
112
+ end
113
+ end
114
+
115
+ def ssl_engine(ssl_ctx)
116
+ ssl_engine = ssl_ctx.createSSLEngine()
117
+ ssl_engine.setUseClientMode(false) # Server mode
118
+ ssl_engine.setNeedClientAuth(false)
119
+ ssl_engine
120
+ end
121
+ end
122
+ # class ChannelInitializer
123
+ end
124
+ # module Server
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require 'logger'
14
+
15
+ # The Server module
16
+ module Server
17
+ # The Config module
18
+ module Config
19
+ DEFAULTS = {
20
+ host: '0.0.0.0',
21
+ port: 8080,
22
+ ssl: false,
23
+ idle_reading: 5 * 60, # seconds
24
+ idle_writing: 30, # seconds
25
+ log_requests: false,
26
+ log_level: Logger::INFO,
27
+ quit_commands: %i[bye quit]
28
+ }.freeze
29
+ end
30
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require 'java'
14
+ require 'netty'
15
+
16
+ require_relative 'channel_initializer'
17
+ require_relative 'shutdown_hook'
18
+
19
+ # The Server module
20
+ module Server
21
+ java_import Java::io.netty.bootstrap.ServerBootstrap
22
+ java_import Java::io.netty.channel.ChannelOption
23
+ java_import Java::io.netty.channel.nio.NioEventLoopGroup
24
+ java_import Java::io.netty.handler.logging.LogLevel
25
+ java_import Java::io.netty.handler.logging.LoggingHandler
26
+
27
+ # The InstanceMethods module
28
+ module InstanceMethods
29
+ def configure_handlers(&block)
30
+ ::Server::ChannelInitializer::DefaultHandler.add_listener(self)
31
+ channel_initializer << block if block_given?
32
+ end
33
+
34
+ def bootstrap
35
+ @bootstrap = ServerBootstrap.new
36
+ @bootstrap.group(boss_group, worker_group)
37
+ @bootstrap.channel(::Server::CHANNEL_TYPE)
38
+ @bootstrap.option(ChannelOption::SO_BACKLOG, 100.to_java(java.lang.Integer))
39
+ @bootstrap.handler(logging_handler) if options[:log_requests]
40
+ @bootstrap.childHandler(channel_initializer)
41
+ end
42
+
43
+ def channel_initializer
44
+ @channel_initializer ||= ::Server::ChannelInitializer.new(@options)
45
+ end
46
+
47
+ def boss_group
48
+ @boss_group ||= NioEventLoopGroup.new(2)
49
+ end
50
+
51
+ def worker_group
52
+ @worker_group ||= NioEventLoopGroup.new
53
+ end
54
+
55
+ def logging_handler
56
+ @logging_handler ||= LoggingHandler.new(LogLevel::INFO)
57
+ end
58
+
59
+ # rubocop: disable Metrics/AbcSize
60
+ # rubocop: disable Metrics/MethodLength
61
+ def run(port = @options[:port])
62
+ channel = bootstrap.bind(port).sync().channel()
63
+ ::Server::Channels.add(channel)
64
+ ::Server::ShutdownHook.new(self)
65
+ log.info "Listening on #{channel.local_address}"
66
+ channel.closeFuture().sync()
67
+ rescue java.lang.IllegalArgumentException => e
68
+ raise "Invalid argument: #{e.message}"
69
+ rescue java.net.BindException => e
70
+ raise "Bind error: #{e.message}: #{options[:host]}:#{port}"
71
+ rescue java.net.SocketException => e
72
+ raise "Socket error: #{e.message}: #{options[:host]}:#{port}"
73
+ ensure
74
+ boss_group&.shutdownGracefully()
75
+ worker_group&.shutdownGracefully()
76
+ end
77
+ # rubocop: enable Metrics/AbcSize
78
+ # rubocop: enable Metrics/MethodLength
79
+
80
+ def shutdown
81
+ ::Server::Channels.disconnect().awaitUninterruptibly()
82
+ ::Server::Channels.close().awaitUninterruptibly()
83
+ end
84
+
85
+ def <<(handler)
86
+ channel_initializer << handler
87
+ end
88
+ end
89
+ # module ServerInstanceMethods
90
+ end
91
+ # module Server
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require 'java'
14
+
15
+ # The Server module
16
+ module Server
17
+ # The Listenable module
18
+ module Listenable
19
+ def listeners
20
+ @listeners ||= java.util.concurrent.CopyOnWriteArrayList.new
21
+ end
22
+
23
+ def add_listener(listener)
24
+ listeners << listener
25
+ end
26
+
27
+ def remove_listener(listener)
28
+ listeners.delete(listener)
29
+ end
30
+
31
+ def notify(message, *args)
32
+ return if listeners.empty?
33
+ log.trace "Notifying listeners (#{listeners}) of message: #{message}"
34
+ listeners.each do |listener|
35
+ listener.send(message.to_sym, *args) if listener.respond_to?(message.to_sym)
36
+ end
37
+ end
38
+ end
39
+ # module Listenable
40
+ end
41
+ # module Server
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require 'java'
14
+ require 'netty'
15
+
16
+ # The Server module
17
+ module Server
18
+ java_import Java::io.netty.channel.ChannelFutureListener
19
+ java_import Java::io.netty.channel.ChannelHandler
20
+ java_import Java::io.netty.channel.SimpleChannelInboundHandler
21
+
22
+ # The MessageHandler class
23
+ class MessageHandler < SimpleChannelInboundHandler
24
+ include ChannelHandler
25
+ include ChannelFutureListener
26
+
27
+ def initialize(&handler)
28
+ super()
29
+ @handler = handler
30
+ end
31
+
32
+ # Please keep in mind that this method will be renamed to
33
+ # messageReceived(ChannelHandlerContext, I) in 5.0.
34
+ #
35
+ # java_signature 'protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception'
36
+ def channelRead0(ctx, msg)
37
+ log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
38
+ messageReceived(ctx, msg)
39
+ end
40
+
41
+ def messageReceived(ctx, msg)
42
+ log.trace "##{__method} channel: #{ctx.channel}, message: #{msg.inspect}"
43
+ msg&.chomp!
44
+ log.info "Received message: #{msg}"
45
+ return super(ctx, msg) unless respond_to?(:handle_message) && @handler.arity == 2
46
+ handle_message(ctx, msg)
47
+ end
48
+
49
+ def handle_message(ctx, message)
50
+ request = message.to_s.strip
51
+ response = @handler.call(ctx.channel, request).to_s.chomp
52
+ log.debug "response: #{response}"
53
+ ctx.channel.writeAndFlush("#{response}\n")
54
+ end
55
+ end
56
+ end