require "http/web_socket" require "option_parser" require "ipc" require "baguette-crystal-base" require "./utils" require "./lib_modifications.cr" # Websocketc # - is the application allowing libipc communications through Web Socket # - is designed to connect to a Websocketd server, ask for a service, then exchange data # - is WIP # Currently, the exchange format is JSON, encapsulated in JSON: # type: Int32 # type of the message # payload: JSON::Any? # encapsulated payload # # Websocketd deserialize the type and the payload before sending it to the service. module Websocketc class Exception < ::Exception end class Context class_property uri = "ws://localhost:1234/pong" class_property rounds = 1 end end class Baguette::Configuration class Websocketc < Base property verbosity : Int32 = 2 property print_ipc_timer : Bool = false property ipc_timer : Int32 = 30_000 def initialize end end end require "./network.cr" class Websocketc::Service < IPC::Server property connections : Hash(Int32, WebSocket) = {} of Int32 => WebSocket getter all_fd : Array(Int32) = Array(Int32).new property config : Baguette::Configuration::Websocketc def initialize(@config : Baguette::Configuration::Websocketc) super "websocketc" end def handle_request(event : IPC::Event::MessageReceived) request_start = Time.utc # TODO: The message can come from an already linked fd. request = Websocketc.requests.parse_ipc_json event.message if request.nil? raise "unknown request type" end request_name = request.class.name.sub /^Websocketc::Request::/, "" Baguette::Log.debug "<< #{request_name}" response = begin request.handle self, event rescue e : Websocketc::Exception Baguette::Log.error "Websocketc::Exception: #{request_name} => #{e}" Websocketc::Response::Error.new "generic error" rescue e Baguette::Log.error "#{request_name} generic error #{e}" Websocketc::Response::Error.new "unknown error" end # If clients sent requests with an “id” field, it is copied # in the responses. Allows identifying responses easily. response.id = request.id send event.fd, response duration = Time.utc - request_start response_name = response.class.name.sub /^Websocketc::Response::/, "" if response.is_a? Websocketc::Response::Error Baguette::Log.warning ">> #{response_name} (#{response.reason})" else Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})" end end def run Baguette::Log.title "Starting websocketc" self.loop do |event| begin case event when IPC::Event::Timer Baguette::Log.debug "Timer" if @config.print_ipc_timer when IPC::Event::Connection Baguette::Log.debug "connection from #{event.fd}" @all_fd << event.fd when IPC::Event::Disconnection Baguette::Log.debug "disconnection from #{event.fd}" @all_fd.select! &.!=(event.fd) connections.delete event.fd when IPC::Event::MessageSent Baguette::Log.debug "message sent to #{event.fd}" when IPC::Event::MessageReceived Baguette::Log.debug "message received from #{event.fd}" handle_request event when IPC::Exception Baguette::Log.warning "IPC::Exception: #{event.message}" else Baguette::Log.warning "unhandled IPC event: #{event.class}" end rescue e Baguette::Log.error "exception: #{typeof(e)} - #{e.message}" end end end def self.from_cli # First option parsing. simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser # Websocketc configuration. configuration = if no_configuration Baguette::Log.info "do not load a configuration file." Baguette::Configuration::Websocketc.new else # In case there is a configuration file helping with the parameters. Baguette::Configuration::Websocketc.get(configuration_file) || Baguette::Configuration::Websocketc.new end OptionParser.parse do |parser| parser.on "-u uri", "--uri uri", "URI" do |opturi| Websocketc::Context.uri = opturi end parser.on "-r rounds", "--rounds nb-messages", "Nb messages to send." do |opt| Websocketc::Context.rounds = opt.to_i end parser.on "-h", "--help", "Show this help" do puts parser exit 0 end end if simulation pp! configuration exit 0 end ::Websocketc::Service.new configuration end end Websocketc::Service.from_cli.run