lib/whatup/server/server.rb in whatup-0.3.4 vs lib/whatup/server/server.rb in whatup-0.3.5

- old
+ new

@@ -7,22 +7,23 @@ require 'sqlite3' require 'active_record' require 'active_support/core_ext/object/blank' require 'whatup/server/db_init' +require 'whatup/server/logger' require 'whatup/server/redirection' require 'whatup/server/models/client' require 'whatup/server/models/message' require 'whatup/server/models/room' require 'whatup/cli/commands/interactive/interactive' module Whatup module Server class Server # rubocop:disable Metrics/ClassLength - include Thor::Shell include DbInit include Redirection + include WhatupLogger Client = Whatup::Server::Client attr_reader *%i[ip port address clients pid pid_file rooms] @@ -47,19 +48,22 @@ # Starts the server. # # The server continuously loops, and handle each new client in a separate # thread. def start - say "Starting a server with PID:#{@pid} @ #{@address} ... \n", :green + log.info { "Starting a server with PID:#{@pid} @ #{@address} ... \n" } exit_if_pid_exists! connect_to_socket! write_pid! # Listen for connections, then accept each in a separate thread loop do - Thread.new(@socket.accept) { |client| handle_client client } + Thread.new(@socket.accept) do |client| + log.info { "Accepted new client: #{client.inspect}" } + handle_client client + end end rescue SignalException # In case of ^c kill end @@ -74,83 +78,95 @@ # @param clients [Array<Whatup::Server::Client>] Room's inital clients # @param name [String] The room's name # # @return [Whatup::Server::Room] The created room def new_room! clients: [], name: - room = Room.create! name: name, clients: clients - @rooms << room - room + Room.create!(name: name, clients: clients).tap do |room| + @rooms << room + end end private # Receives a new client, then continuously gets input from that client # # @param client [Whatup::Server::Client] The client # - # rubocop:disable Metrics/MethodLength - def handle_client client + def handle_client client # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength client = create_new_client_if_not_existing! client # Loop forever to maintain the connection loop do @clients.reject! &:deleted - Thread.current.exit if client.deleted + if client.deleted + log.debug do + <<~OUT + Client `#{client.name}` has been deleted. + #{'Killing'.colorize :red} this thread." + OUT + end + Thread.current.exit + end if client.composing_dm? handle_dm client elsif client.chatting? handle_chatting client end # Wait until we get a valid command. This takes as long as the client # takes. msg = client.input! unless Whatup::CLI::Interactive.command?(msg) + log.info { "#{client.name.colorize :light_blue}> #{msg}" } - puts "#{client.name}> #{msg}" - - begin - # Send the output to the client - redirect stdin: client.socket, stdout: client.socket do + # Send the output to the client + redirect stdin: client.socket, stdout: client.socket do + begin # Invoke the cli using the provided commands and options. run_thor_command! client: client, msg: msg + rescue RuntimeError, + ArgumentError, + Thor::InvocationError, + Thor::UndefinedCommandError => e + log.info do + "#{client.name.colorize :red}> #{e.class}: #{e.message}" + end + client.puts case e.class.to_s + when 'RuntimeError' + 'Invalid input or unknown command' + else + e.message + end end - rescue RuntimeError, - Thor::InvocationError, - Thor::UndefinedCommandError => e - puts e.message - client.puts 'Invalid input or unknown command' - rescue ArgumentError => e - puts e.message - client.puts e.message end msg = nil end end - # rubocop:enable Metrics/MethodLength # Handles inputing direct messages # # @param client [Whatup::Server::Client] `client` is the sender of # the message, and `client.composing_dm` is the recipient. def handle_dm client msg = StringIO.new loop do input = client.input! - puts "#{client.name}> #{input}" + log.info { "#{client.name.colorize :light_blue}> #{input}" } msg.puts input if input == '.exit' client.puts "Finished dm to `#{client.composing_dm.name}`." break end end - client.composing_dm - .received_messages << Message.new( - sender: client, - content: msg.string - ) + Message.create!(sender: client, content: msg.string).tap do |m| + client.composing_dm.received_messages << m + log.debug do + "Created new message (id = #{m.id}) from `#{client}` to" \ + "`#{client.composing_dm}`" + end + end client.composing_dm = nil end # Handles chatting. # @@ -161,11 +177,11 @@ input = client.input! audience = @clients.reject { |c| c.id == client.id } .select do |c| client.room.clients.pluck(:id).include? c.id end - puts "#{client.name}> #{input}" + log.info { "#{client.name.colorize :light_blue}> #{input}" } if input == '.exit' client.puts "Exited `#{client.room.name}`." audience.each { |c| c.puts "#{client.name}> LEFT" } client.leave_room! break @@ -183,35 +199,51 @@ # # @param client [TCPSocket] The client connection # # @return [Whatup::Server::Client] The created client def create_new_client_if_not_existing! client - name = client.gets.chomp + log.debug { 'Creating new client' } + + name = client.gets&.chomp + + if name.nil? + log.debug do + 'New client (currently unknown) has left. ' \ + "#{'Killing'.colorize :red} this thread." + end + Thread.current.exit + end + rand_num = SecureRandom.random_number(100).to_s.rjust 3, '0' name = name == '' ? "ANON-#{rand_num}" : name if @clients.any? { |c| c.name == name } client.puts 'That name is taken! Goodbye.' client.puts 'END' client.close + log.debug do + "Existing name `#{name}` entered. " \ + "#{'Killing'.colorize :red} this thread" + end Thread.current.exit end @clients << client = Client.create!( name: name, socket: client ) + log.info { "Created new client `#{name}` ..." } - puts "#{client.name} just showed up!" - client.puts <<~MSG - Hello, #{client.name}! + client.tap do |c| + c.puts <<~MSG + Hello, #{client.name}! - Welcome to whatup. + Welcome to whatup. - To get started, type `help`. - MSG - client + To get started, type `help`. + MSG + end end # Initialize a new cli class using the initial command and options, # and then set any instance variables, since Thor will create a new # class instance when it's invoked. @@ -231,42 +263,52 @@ cli.invoke cli.args.first, cli.args.drop(1) end # Kills the server if a PID for this app exists def exit_if_pid_exists! + log.debug { "Checking if `#{@pid}` exists ..." } + return unless running? - say <<~EXIT, :cyan + log.info <<~EXIT A server appears to already be running! Check `#{@pid_file}`. EXIT kill end # Connect a new socket for this server to start listening on the specified # address and port. def connect_to_socket! + log.info do + "#{'Opening'.colorize :blue} TCP socket at `#{@ip}:#{@port}`" + end @socket = TCPServer.open @ip, @port rescue Errno::EADDRINUSE - puts 'Address already in use!' + log.error "Address `#{@ip}:#{@port}` is already in use!" kill end # Write this process's PID to the PID file def write_pid! + log.debug { "Writing PID to `#{@pid}`" } File.open(@pid_file, 'w') { |f| f.puts Process.pid } end # @return [Bool] Whether or not a PID for this app exists def running? File.file? @pid_file end # Kills the server and removes the PID file def kill - say "Killing the server with PID:#{Process.pid} ...", :red + log.info do + "#{'Killing'.colorize :red} the server with " \ + "PID:#{Process.pid} ..." + end FileUtils.rm_rf @pid_file + log.debug { "Removed `#{@pid_file}`." } exit end end end end