lib/whatup/server/server.rb in whatup-0.2.3 vs lib/whatup/server/server.rb in whatup-0.2.4

- old
+ new

@@ -1,68 +1,183 @@ # frozen_string_literal: true require 'socket' require 'fileutils' +require 'securerandom' +require 'active_support/core_ext/object/blank' + require 'whatup/server/client' +require 'whatup/server/room' +require 'whatup/cli/commands/interactive/interactive' module Whatup module Server class Server include Thor::Shell Client = Whatup::Server::Client + # Used by the interactive client cli + attr_reader *%i[ip port address clients max_id pid pid_file rooms] + def initialize port: @ip = 'localhost' @port = port @address = "#{@ip}:#{@port}" @clients = [] + @rooms = [] @max_id = 1 @pid = Process.pid @pid_file = "#{Dir.home}/.whatup.pid" end + # 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 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 do |client| - handle_client client + Thread.new(@socket.accept) do |client| + case handle_client client + when :exit + client.puts 'bye!' + Thread.kill Thread.current + end end end - rescue SignalException + rescue SignalException # In case of ^c kill end + def find_client_by name: + @clients.select { |c| c.name == name }&.first + end + + def new_room! clients: [], name: + room = Room.new name: name, clients: clients + @rooms << room + room + end + private + # Receives a new client, then continuously gets input from that client + # + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def handle_client client + client = create_new_client_if_not_existing! client + + # Loop forever to maintain the connection + loop do + handle_chatting(client) if client.chatting? + + # Wait until we get a valid command. This takes as long as the client + # takes. + msg = client.input! unless Whatup::CLI::Interactive.command?(msg) + + puts "#{client.name}> #{msg}" + + # 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. + cmds, opts = Whatup::CLI::Interactive.parse_input msg + cli = Whatup::CLI::Interactive.new(cmds, opts).tap do |c| + c.server = self + c.current_user = client + end + + begin + # Send the output to the client + redirect stdin: client.socket, stdout: client.socket do + # Invoke the cli using the provided commands and options. + + # This _should_ achieve the same effect as + # `Whatup::CLI::Interactive.start(args)`, but allows us to set + # instance variables on the cli class. + cli.invoke cli.args.first, cli.args[1..cli.args.size - 1] + 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 # rubocop:disable Lint/UselessAssignment + end + end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + + def handle_chatting client + loop do + input = client.input! + room = client.room + puts "#{client.name}> #{input}" + if input == '.exit' + client.leave_room! + break + end + room.broadcast except: client do + "#{client.name}> #{input}" + end + end + end + + # Receives a username from a client, then creates a new client unless a + # client with that username already exists. + # + # If no username is provided (i.e, blank), it assigns a random, anonymous + # username in the format `ANON-xxx`, where `xxx` is a random number upto + # 100, left-padded with zeros. + def create_new_client_if_not_existing! client + name = client.gets.chomp + 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.exit! + end + @clients << client = Client.new( - id: @max_id += 1, - name: client.gets.chomp, + id: new_client_id, + name: name, socket: client ) puts "#{client.name} just showed up!" client.puts "Hello, #{client.name}!" + client + end - loop do - msg = client.gets&.chomp - puts "#{client.name}> #{msg}" unless msg.nil? || msg == '' + # @return A new, unique client identification number + def new_client_id + @max_id += 1 + end - @clients.reject { |c| c.id == client.id }.each do |c| - c.puts "\n#{client.name}> #{msg}" unless msg.nil? || msg == '' - end - end + # Reroutes stdin and stdout inside a block + def redirect stdin: $stdin, stdout: $stdout + original_stdin = $stdin + original_stdout = $stdout + $stdin = stdin + $stdout = stdout + yield + ensure + $stdin = original_stdin + $stdout = original_stdout end def exit_if_pid_exists! return unless running? @@ -90,9 +205,10 @@ end def kill say "Killing the server with PID:#{Process.pid} ...", :red FileUtils.rm_rf @pid_file + exit end end end end