require 'rbczmq' module Softwear module Library class LightServer def self.test!(file_path) Object.class_eval do def start_server!(*args) Thread.new do Softwear::Library::LightServer.new.start_server!(*args) end end end load file_path end def split(string, limit = nil) string.split(/\s+/, limit) end def report_error(rep, whole_command, error) $stderr.puts "=== ERROR WHILE PROCESSING THE COMMAND \"#{whole_command}\" ===\n"\ "#{error.class.name}: #{error.message}\n#{error.backtrace.join("\n")}" begin rep.send "sorry" if rep rescue StandardError => e $stderr.puts "(could not send 'sorry' message: \"#{e.class} #{e.message}\")" end end def dev_log(*a) $stdout.puts(*a) #if Rails.env.development? end def log(*a) $stdout.puts(*a) #unless Rails.env.test? end # ==== Send Format: ======= # One line strings: "#{command} #{arg1} #{arg2} #{etc}" # The last argument, depending on the command, may contain spaces (but usually does not need to) # ========================= # === Receive Format: ===== # Usually one string, like "yes", or "no". # Returns "denied" if an unauthorized command was attempted. # Returns "invalid" if an invalid command was attempted. # Returns "sorry" if an error was raised while processing the command. # Can be a json argument, often following "yes ". # ========================= def start_server!(*args) log "Connecting...!" if args.size > 1 port = args.first else port = ENV['port'] || ENV['PORT'] || 2900 end address = ENV['SOCKET_ADDR'] || "tcp://*" if address =~ /:$/ socket_address = address else socket_address = "#{address}:#{port}" end rep = zmq.bind(:REP, socket_address) log "Ready! Using \"#{ActiveRecord::Base.connection.try(:current_database) || 'in-memory'}\" database" commands = args.last loop do begin loop do line_in = rep.recv raise "Got nil response (ZMQ REP/REQ out of sync?)" if line_in.nil? command, rest_of_command = split(line_in, 2) before = Time.now begin command = commands[command.downcase.to_sym] if command.nil? log "Received invalid command: \"#{line_in}\"" else dev_log "<== #{line_in}" ActiveRecord::Base.connection_pool.with_connection do # The ZMQ socket requires that a reply be send after every # message -- so we pass a lambda for the client code to # call to send a message and make sure it only happens # once. replied = false reply = lambda do |msg| if replied raise "Reply sent twice" else rep.send(msg) replied = true end end instance_exec(reply, rest_of_command, &command) if !replied rep.send "noreply" end end end rescue StandardError => e report_error(rep, line_in, e) rescue Exception => e report_error(rep, line_in, e) break end after = Time.now ms = (after - before) * 1000 dev_log %[(#{'%.2f' % ms}ms)] dev_log "" end rescue StandardError => error $stderr.puts "=== ERROR -- RESTARTING SERVER ===\n"\ "#{error.class.name}: #{error.message}\n#{error.backtrace.join("\n")}" rep.destroy log "Reconnecting...!" rep = zmq.bind(:REP, socket_address) end end end def zmq $zmq_context ||= ZMQ::Context.new end end end end def start_server!(*args) Softwear::Library::LightServer.new.start_server!(*args) end