-module(test). -export([listen/1, handle_client/1, maintain_clients/1, start/1, stop/0, controller/1]). -author("jerith"). -define(TCP_OPTIONS,[list, {packet, 0}, {active, false}, {reuseaddr, true}]). -record(player, {name=none, socket, mode}). %% To allow incoming connections, we need to listen on a TCP port. %% This is also the entry point for our server as a whole, so it %% starts the client_manager process and gives it a name so the rest %% of the code can get to it easily. listen(Port) -> {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS), register(client_manager, spawn(?MODULE, maintain_clients, [[]])), do_accept(LSocket). %% Accepting a connection gives us a connection socket with the %% newly-connected client on the other end. Since we want to accept %% more than one client, we spawn a new process for each and then wait %% for another connection on our listening socket. do_accept(LSocket) -> case gen_tcp:accept(LSocket) of {ok, Socket} -> spawn(?MODULE, handle_client, [Socket]), client_manager ! {connect, Socket}; {error, Reason} -> io:format("Socket accept error: ~s~n", [Reason]) end, do_accept(LSocket). %% All the client-socket process needs to do is wait for data and %% forward it to the client_manager process which decides what to do %% with it. If the client disconnects, we let client_manager know and %% then quietly go away. handle_client(Socket) -> case gen_tcp:recv(Socket, 0) of {ok, Data} -> client_manager ! {data, Socket, Data}, handle_client(Socket); {error, closed} -> client_manager ! {disconnect, Socket} end. %% This is the main loop of the client_manager process. It maintains %% the list of "players" and calls the handler for client input. maintain_clients(Players) -> io:format("Players:~n", []), lists:foreach(fun(P) -> io:format(">>> ~w~n", [P]) end, Players), receive {connect, Socket} -> Player = #player{socket=Socket, mode=connect}, send_prompt(Player), io:format("client connected: ~w~n", [Player]), NewPlayers = [Player | Players]; {disconnect, Socket} -> Player = find_player(Socket, Players), io:format("client disconnected: ~w~n", [Player]), NewPlayers = lists:delete(Player, Players); {data, Socket, Data} -> Player = find_player(Socket, Players), NewPlayers = parse_data(Player, Players, Data), NewPlayer = find_player(Socket, NewPlayers), send_prompt(NewPlayer) end, maintain_clients(NewPlayers). %% find_player is a utility function to get a player record associated %% with a particular socket out of the player list. find_player(Socket, Players) -> {value, Player} = lists:keysearch(Socket, #player.socket, Players), Player. %% delete_player returns the player list without the given player. It %% deletes the player from the list based on the socket rather than %% the whole record because the list might hold a different version. delete_player(Player, Players) -> lists:keydelete(Player#player.socket, #player.socket, Players). %% Sends an appropriate prompt to the player. Currently the only %% prompt we send is the initial "Name: " when the player connects. send_prompt(Player) -> case Player#player.mode of connect -> gen_tcp:send(Player#player.socket, "Name: "); active -> ok end. %% Sends the given data to all players in active mode. send_to_active(Prefix, Players, Data) -> ActivePlayers = lists:filter(fun(P) -> P#player.mode == active end, Players), lists:foreach(fun(P) -> gen_tcp:send(P#player.socket, Prefix ++ Data) end, ActivePlayers), ok. %% We don't really do much parsing, but that will probably change as %% more features are added. Currently this handles naming the player %% when he first connects and treats everything else as a message to %% send. parse_data(Player, Players, Data) -> case Player#player.mode of active -> send_to_active(Player#player.name ++ ": ", delete_player(Player, Players), Data), Players; connect -> UPlayer = Player#player{name=bogostrip(Data), mode=active}, [UPlayer | delete_player(Player, Players)] end. %% Utility methods to clean up the name before we apply it. Called %% bogostrip rather than strip because it returns the first continuous %% block of non-matching characters rather stripping matching %% characters off the front and back. bogostrip(String) -> bogostrip(String, "\r\n\t "). bogostrip(String, Chars) -> LStripped = string:substr(String, string:span(String, Chars)+1), string:substr(LStripped, 1, string:cspan(LStripped, Chars)). %% Here we have some extra code to test other bits of pygments' Erlang %% lexer. get_timestamp() -> {{Year,Month,Day},{Hour,Min,Sec}} = erlang:universaltime(), lists:flatten(io_lib:format( "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ", [Year, Month, Day, Hour, Min, Sec])). a_binary() -> << 100:16/integer, 16#7f >>. a_list_comprehension() -> [X*2 || X <- [1,2,3]]. a_map() -> M0 = #{ a => 1, b => 2 }, M1 = M0#{ b := 200 }. escape_sequences() -> [ "\b\d\e\f\n\r\s\t\v\'\"\\" , "\1\12\123" % octal , "\x01" % short hex , "\x{fff}" % long hex , "\^a\^A" % control characters ]. map(Fun, [H|T]) -> [Fun(H) | map(Fun, T)]; map(Fun, []) -> []. %% pmap, just because it's cool. pmap(F, L) -> Parent = self(), [receive {Pid, Result} -> Result end || Pid <- [spawn(fun() -> Parent ! {self(), F(X)} end) || X <- L]].