Hana 9e216da9ef go.mod: add go.mod and move pygments to third_party
After go1.16, go will use module mode by default,
even when the repository is checked out under GOPATH
or in a one-off directory. Add go.mod, go.sum to keep
this repo buildable without opting out of the module
mode.

> go mod init github.com/mmcgrana/gobyexample
> go mod tidy
> go mod vendor

In module mode, the 'vendor' directory is special
and its contents will be actively maintained by the
go command. pygments aren't the dependency the go will
know about, so it will delete the contents from vendor
directory. Move it to `third_party` directory now.

And, vendor the blackfriday package.

Note: the tutorial contents are not affected by the
change in go1.16 because all the examples in this
tutorial ask users to run the go command with the
explicit list of files to be compiled (e.g.
`go run hello-world.go` or `go build command-line-arguments.go`).
When the source list is provided, the go command does
not have to compute the build list and whether it's
running in GOPATH mode or module mode becomes irrelevant.
2021-02-15 16:45:26 -05:00

170 lines
5.6 KiB
Erlang

-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]].
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]].