echo daemon works well enough

This commit is contained in:
Peter Harpending
2026-09-01 23:01:34 -07:00
parent 997c3d1da3
commit 7aa6f4b863
12 changed files with 352 additions and 272 deletions
+12
View File
@@ -7,3 +7,15 @@
- client connects to server - client connects to server
- server writes back "hello" - server writes back "hello"
- server closes connection - server closes connection
- x01-noisy-hello:
- was going to be hello world over noise
- abandoned
- `x02-_`:
- skipped because the `2` key on my laptop doesn't work
- x03-echo:
- telnet echo server
- budget version of zx template "traditional erlang service
application"
-7
View File
@@ -1,7 +0,0 @@
% @doc echo server client
-module(echoc).
-export([main/0]).
main() ->
io:format("you can't help people who won't help themselves.~n", []).
-265
View File
@@ -1,265 +0,0 @@
%% @doc
%% super simple telnet echo server daemon
%%
%% no noise yet
%%
%% pimps and hos model (from trash talk template):
%%
%% ```
%% t_sup
%% |
%% t_clients
%% |
%% +--- t_client_man pimp
%% |
%% +--- t_client_sup bottom bitch
%% |
%% +--- <0.1.0> (t_client) ho
%% +--- <0.2.0> (t_client) ho
%% +--- <0.3.0> (t_client) ho
%% '''
%%
%% each ho:
%%
%% <ul>
%% <li>Takes listen socket as arg</li>
%% <li>spawns next ho</li>
%% </ul>
%%
%% ```
%% start(ListenSocket) ->
%% t_client_sup:start_acceptor(ListenSocket).
%%
%% start_link(ListenSocket) ->
%% proc_lib:start_link(?MODULE, init, [self(), ListenSocket]).
%%
%% init(Parent, ListenSocket) ->
%% ok = io:format("~p Listening.~n", [self()]),
%% Debug = sys:debug_options([]),
%% ok = proc_lib:init_ack(Parent, {ok, self()}),
%% listen(Parent, Debug, ListenSocket).
%%
%% listen(Parent, Debug, ListenSocket) ->
%% case gen_tcp:accept(ListenSocket) of
%% {ok, Socket} ->
%% {ok, _} = start(ListenSocket),
%% {ok, Peer} = inet:peername(Socket),
%% ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]),
%% ok = t_client_man:enroll(),
%% State = #s{socket = Socket},
%% loop(Parent, Debug, State);
%% {error, closed} ->
%% ok = io:format("~p Retiring: Listen socket closed.~n", [self()]),
%% exit(normal)
%% end.
%% '''
%%
%% @end
-module(echod).
-include("./ansi.hrl").
-export([main/0]).
%% pimp state
-record(ps,
{lsock = none :: gen_tcp:socket(),
hos = [] :: [{pid(), reference()}],
ho_names = init_ho_names() :: [string()]}).
-type pimp_state() :: #ps{}.
%% ho state
-record(hs,
{name = none :: string(),
daddy = none :: pid(),
vagene = none :: gen_tcp:socket()}).
-type ho_state() :: #hs{}.
-spec main() -> no_return().
%% @doc
%% run the echo server daemon
main() ->
%% show the splash screen nigga
ok = splash(),
case gen_tcp:listen(6969, [binary, {packet, line}, {active, once}]) of
{ok, LSock} ->
%% we out here
%% we not wuz sockitz nigga
%% we **be** sockets
%% nigga
ok = pimp_logfln("Butters iz in the hizzouse: ~tp", [self()]),
pimp_loop(#ps{lsock = LSock});
{error, closed} ->
%% dont be pimp
io:format("PIMP ERROR: ~tp~n", [Error])
end.
-spec splash() -> ok.
%% @private
%% show splash screen
splash() ->
SplashStr = """
RBX example 2 telnet echo server daemon
"one" part of the many-to-one
""",
io:format("~s~n", [SplashStr]).
-spec pimp_loop(PimpState) -> NoReturn when
PimpState :: pimp_state(),
NoReturn :: no_return().
%% @private
%% runs in context of main
pimp_loop(PS = #ps{lsock = LSock, hos = Hos, ho_names = Names}) ->
%% blocks until client connects
case gen_tcp:accept(LSock) of
{ok, ASock} ->
{HoName, NewNames} = recycle_ho_names(Names),
InitHoState = #hs{name = HoName,
daddy = self(),
vagene = ASock},
SpawnHo = fun() -> do_be_ho(InitHoState) end,
NewHo = {_Pid, _Ref} = erlang:spawn_monitor(SpawnHo),
NewPimpState = PS#ps{hos = [NewHo | Hos],
ho_names = NewNames},
pimp_loop(NewPimpState);
{echo, From, Data} ->
echo_except(From, Data, Hos),
pimp_loop(PS);
{'DOWN', Ref, process, Pid, Info} ->
ok = pimp_logfln("ho down: ~tp; reason: ~tw", [Pid, Info]),
NewHos = lists:delete({Pid, Ref}, Hos),
NewPS = PS#ps{hos = NewHos},
pimp_loop(NewPS);
Bad ->
pimp_logfln("bad msg: ~tw", [Bad]),
pimp_logln("liquidating the hos"),
liquidate_hos(Hos),
gen_tcp:close(LSock)
end.
liquidate_hos([{HoPid, _HoRef} | Hos]) ->
pimp_logfln("liquidating ~tp", [HoPid]),
exit(HoPid, kill),
liquidate_hos(Hos);
liquidate_hos([]) ->
pimp_logln("it's done").
% skip if equal
echo_except(SrcPid, Data, [{SrcPid, _Ref} | Rest]) ->
echo_except(SrcPid, Data, Rest);
echo_except(SrcPid, Data, [{Pid, _Ref} | Rest]) ->
Pid ! {echo, Data},
echo_except(SrcPid, Data, Rest);
echo_except(_, _, []) ->
ok.
-spec spawn_ho(HoState) ->
-spec do_be_ho(HoState) -> NoReturn when
HoState :: ho_state(),
NoReturn :: no_return().
%% @doc
%% ain't no rest for the wicked
do_be_ho(HS = #hs{name = Name, daddy = Daddy, vagene = ASock}) ->
Msg = io:format("HI! I'm ~ts. What's your name?\r\n", [Name]),
ok = ho_send(ASock, Msg),
receive
{tcp, ASock, Data} ->
ho_rcvd(Data),
Daddy ! {echo, self(), Data},
do_be_ho(HS);
{echo, Data} ->
ho_send(ASock, Data),
do_be_ho(HS);
{tcp_closed, ASock} ->
exit(its_fine)
end.
pimp_logfln(MsgStr, Args) ->
pimp_logln(io_lib:format(MsgStr, Args)).
pimp_logln(MsgStr) ->
PimpPfx = io_lib:format("pimp ~tp :: ", [self()]),
Msg = [?ANSI_BOLD([PimpPfx, MsgStr, "\n"])],
ok = io:put_chars(Msg),
ok.
ho_rcvd(MsgStr) ->
ok = io:format("ho ~tp << ~tw~n", [self(), MsgStr]),
ok.
ho_send(Socket, Msg) ->
ok = io:format("ho ~tp >> ~tw~n", [self(), Msg]),
ok = gen_tcp:send(Socket, Msg),
ok.
init_ho_names() ->
["Crystal",
"Tiffany",
"Amber",
"Brandy",
"Lola",
"Angel",
"Ginger",
"Candy",
"Charity",
"Anastasia",
"Cherry",
"Kitty",
"Jade",
"Destiny",
"Devon",
"Chastity",
"Raven",
"Scarlett",
"Bambi",
"Star",
"Paris",
"Dallas",
"Diamond",
"Skye",
"Trinity",
"Tawny",
"Layla",
"Lexie",
"Roxy",
"Porsche",
"Nevaeh",
"Ashlynn",
"Aspen",
"Chyna",
"Lexus",
"Unique",
"Chardonnay",
"Houston",
"London",
"Coco",
"Luscious",
"Delight",
"Capri",
"Trixie",
"Cinnamon"].
recycle_ho_names([N | Ns]) ->
{N, Ns ++ [N]}.
+20
View File
@@ -0,0 +1,20 @@
% @doc echo server client
-module(echoc).
-export([main/0]).
main() ->
{ok, Socket} = gen_tcp:connect("localhost", 6969, [binary, {packet, 0}]),
loop(Socket).
loop(Socket) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, Bytes} ->
ok = io:format("< ~tp~n", [Bytes]),
loop(Socket);
{tcp_closed, Socket} ->
ok = io:format("connection terminated~n", []),
exit(normal)
end.
+249
View File
@@ -0,0 +1,249 @@
%% @doc
%% super simple telnet echo server daemon
%%
%% no noise yet
%%
%% pimps and hos model (from trash talk template):
%%
%% ```
%% t_sup
%% |
%% t_clients
%% |
%% +--- t_client_man pimp
%% |
%% +--- t_client_sup bottom bitch
%% |
%% +--- <0.1.0> (t_client) ho
%% +--- <0.2.0> (t_client) ho
%% +--- <0.3.0> (t_client) ho
%% '''
%%
%% each ho:
%%
%% <ul>
%% <li>Takes listen socket as arg</li>
%% <li>spawns next ho</li>
%% </ul>
%%
%% ```
%% start(ListenSocket) ->
%% t_client_sup:start_acceptor(ListenSocket).
%%
%% start_link(ListenSocket) ->
%% proc_lib:start_link(?MODULE, init, [self(), ListenSocket]).
%%
%% init(Parent, ListenSocket) ->
%% ok = io:format("~p Listening.~n", [self()]),
%% Debug = sys:debug_options([]),
%% ok = proc_lib:init_ack(Parent, {ok, self()}),
%% listen(Parent, Debug, ListenSocket).
%%
%% listen(Parent, Debug, ListenSocket) ->
%% case gen_tcp:accept(ListenSocket) of
%% {ok, Socket} ->
%% {ok, _} = start(ListenSocket),
%% {ok, Peer} = inet:peername(Socket),
%% ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]),
%% ok = t_client_man:enroll(),
%% State = #s{socket = Socket},
%% loop(Parent, Debug, State);
%% {error, closed} ->
%% ok = io:format("~p Retiring: Listen socket closed.~n", [self()]),
%% exit(normal)
%% end.
%% '''
%%
%% @end
-module(echod).
-include("./ansi.hrl").
-export([main/0]).
% ho from pimp pov
-record(pho,
{pid :: pid(),
ref :: reference(),
name :: string()}).
%% pimp state
-record(ps,
{lsock = none :: gen_tcp:socket(),
hos = [] :: [#pho{}],
ho_names = init_ho_names() :: [string()]}).
%% ho state
-record(hs,
{name = none :: string(),
daddy = none :: pid(),
asock = none :: gen_tcp:socket()}).
main() ->
io:format("RBX example 2 telnet echo server daemon~n"
"`one' part of the many-to-one relationship~n",
[]),
p_init().
% pimp process startup
p_init() ->
p_logln("pimp startup (p_init)"),
Port = 6969,
Opts = [binary, {packet, line}, {active, once}],
case gen_tcp:listen(Port, Opts) of
{ok, LSock} ->
ok = p_logfln("pimp starting: ~tp", [self()]),
PS_I = #ps{lsock = LSock},
PS_II = p_spawn_ho(PS_I),
p_loop(PS_II);
{error, closed} ->
%% dont be pimp
p_logln("listen socket closed")
end.
p_loop(PS = #ps{hos = Hos}) ->
receive
{ho, _HoPid, taken} ->
NewPS = p_spawn_ho(PS),
p_loop(NewPS);
{ho, HoPid, sent, Data} ->
HoName = p_ho_name(HoPid, PS),
SendMessage = io_lib:format("~ts :: ~ts\r\n", [HoName, Data]),
ok = p_echo_except(HoPid, HoName, SendMessage, Hos),
p_loop(PS);
Down = {'DOWN', _, _, _, _} ->
NewPS = p_ho_down(Down, PS),
p_loop(NewPS);
Unknown ->
ok = p_logfln("unknown message: ~tw", [Unknown]),
p_loop(PS)
end.
% given pid of ho, get her name
p_ho_name(HoPid, #ps{hos = Hos}) ->
case lists:keyfind(HoPid, #pho.pid, Hos) of
#pho{name = Name} -> Name;
false -> "JaneDoe"
end.
% pimp spawns new ho
p_spawn_ho(PS = #ps{lsock = LSock, hos = Hos, ho_names = Names}) ->
% new name for ho, initial ho state
{N, NewNames} = recycle(Names),
InitHS = #hs{name = N, daddy = self()},
% spawn the new ho
ok = p_logfln("spawning new ho: ~ts", [N]),
SpawnHo = fun() -> h_init(LSock, InitHS) end,
{HPid, HRef} = erlang:spawn_monitor(SpawnHo),
% new ho record from Pimp POV
NewHo = #pho{pid = HPid, ref = HRef, name = N},
ok = p_logfln("spawned new ho: ~tw", [NewHo]),
NewPS = PS#ps{hos = [NewHo | Hos], ho_names = NewNames},
NewPS.
% ho down, remove her from roster
p_ho_down({'DOWN', _Ref, process, Pid, Info}, PS = #ps{hos = Hos}) ->
ok = p_logfln("ho down: ~tp; reason: ~tw", [Pid, Info]),
NewHos = lists:keydelete(Pid, #pho.pid, Hos),
NewPS = PS#ps{hos = NewHos},
NewPS.
% echo message from one ho to all the rest
% skip if equal
p_echo_except(SrcPid, SrcName, Data, [#pho{pid = SrcPid} | Rest]) ->
p_echo_except(SrcPid, SrcName, Data, Rest);
p_echo_except(SrcPid, SrcName, Data, [#pho{pid = DstPid} | Rest]) ->
DstPid ! {pimp, send, Data},
p_echo_except(SrcPid, SrcName, Data, Rest);
p_echo_except(_, _, _, []) ->
ok.
%% runs in ho context
h_init(LSock, HS = #hs{name = Name, daddy = Daddy, asock = none}) ->
h_logln(Name, "h_init"),
% this is key: accept needs to run in ho context, otherwise she
% doesn't own the acceptor socket
%
% blocks until client connects
case gen_tcp:accept(LSock) of
{ok, ASock} ->
h_logln(Name, "taken"),
% tell daddy to spawn the next ho
Daddy ! {ho, self(), taken},
Welcome = io_lib:format("HI! I'm ~ts. What's your name?\r\n",
[Name]),
ok = h_send(Name, ASock, Welcome),
HS_II = HS#hs{asock = ASock},
h_loop(HS_II);
{error, closed} ->
h_logln(Name, "didn't get taken")
end.
h_loop(HS = #hs{name = Name, daddy = Daddy, asock = ASock}) ->
ok = inet:setopts(ASock, [{active, once}]),
receive
{tcp, ASock, Data} ->
Data_II = string:chomp(Data),
ok = h_rcvd(Name, Data_II),
Daddy ! {ho, self(), sent, Data_II},
h_loop(HS);
{pimp, send, Data} ->
ok = h_send(Name, ASock, Data),
h_loop(HS);
{tcp_closed, ASock} ->
exit(its_fine);
Unknown ->
ok = h_logfln(Name, "unknown message: ~tw", [Unknown]),
h_loop(HS)
end.
% pimp log in bold
p_logln(LogStr) ->
Msg = io_lib:format("!! Daddy :: ~ts~n", [LogStr]),
PimpChars = ?ANSI_BOLD([Msg]),
io:put_chars(PimpChars).
p_logfln(LogStr, Args) ->
p_logln(io_lib:format(LogStr, Args)).
% ho log dimmed
h_logln(HoName, LogStr) ->
FMsg = io_lib:format("!! ~ts :: ~ts~n", [HoName, LogStr]),
HoChars = ?ANSI_DIM([FMsg]),
io:put_chars(HoChars).
h_logfln(HoName, MsgStr, Args) ->
h_logln(HoName, io_lib:format(MsgStr, Args)).
% normal: john messages
h_rcvd(Name, MsgStr) ->
Msg = io_lib:format("<< ~ts :: ~ts~n", [Name, MsgStr]),
io:put_chars(Msg).
h_send(Name, Socket, Msg) ->
ok = io:format(">> ~ts :: ~ts~n", [Name, Msg]),
gen_tcp:send(Socket, Msg).
init_ho_names() ->
["Crystal", "Tiffany", "Amber", "Brandy", "Lola", "Angel",
"Ginger", "Candy", "Charity", "Anastasia", "Cherry", "Kitty",
"Jade", "Destiny", "Devon", "Chastity", "Raven", "Scarlett",
"Bambi", "Star", "Paris", "Dallas", "Diamond", "Skye",
"Trinity", "Tawny", "Layla", "Lexie", "Roxy", "Porsche",
"Nevaeh", "Ashlynn", "Aspen", "Chyna", "Lexus", "Unique",
"Chardonnay", "Houston", "London", "Coco", "Luscious",
"Delight", "Capri", "Trixie", "Cinnamon"].
recycle([N | Ns]) ->
{N, Ns ++ [N]}.
+62
View File
@@ -0,0 +1,62 @@
Alexander
Anthony
Asher
Beau
Benjamin
Bennett
Caleb
Carter
Charles
Cooper
Daniel
David
Dylan
Elias
Elijah
Ethan
Ezekiel
Ezra
Gabriel
Grayson
Henry
Hudson
Isaac
Isaiah
Jack
Jackson
Jacob
James
John
Joseph
Josiah
Julian
Leo
Levi
Liam
Logan
Luca
Lucas
Luke
Mason
Mateo
Matthew
Maverick
Michael
Miles
Noah
Oliver
Owen
Roman
Rowan
Samuel
Santiago
Sebastian
Theodore
Thiago
Thomas
Waylon
Wesley
Weston
William
Wyatt
+9
View File
@@ -0,0 +1,9 @@
liquidate_hos([{HoPid, _HoRef} | Hos]) ->
p_logfln("liquidating ~tp", [HoPid]),
exit(HoPid, kill),
liquidate_hos(Hos);
liquidate_hos([]) ->
p_logln("it's done").