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