initial commit;
This commit is contained in:
commit
71909e79b0
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
*.swp
|
||||
erl_crash.dump
|
||||
ebin/*.beam
|
||||
doc/*.html
|
||||
doc/*.css
|
||||
doc/edoc-info
|
||||
doc/erlang.png
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
1
Emakefile
Normal file
1
Emakefile
Normal file
@ -0,0 +1 @@
|
||||
{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}.
|
||||
26
LICENSE
Normal file
26
LICENSE
Normal file
@ -0,0 +1,26 @@
|
||||
Copyright 2025 Peter Harpending <peterharpending@qpq.swiss>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
9
ebin/fewd.app
Normal file
9
ebin/fewd.app
Normal file
@ -0,0 +1,9 @@
|
||||
{application,fewd,
|
||||
[{description,"Front End Web Dev in Erlang stuff"},
|
||||
{registered,[]},
|
||||
{included_applications,[]},
|
||||
{applications,[stdlib,kernel]},
|
||||
{vsn,"0.1.0"},
|
||||
{modules,[fd_client,fd_client_man,fd_client_sup,fd_clients,
|
||||
fd_sup,fewd]},
|
||||
{mod,{fewd,[]}}]}.
|
||||
204
src/fd_client.erl
Normal file
204
src/fd_client.erl
Normal file
@ -0,0 +1,204 @@
|
||||
%%% @doc
|
||||
%%% front end web development lab Client
|
||||
%%%
|
||||
%%% An extremely naive (currently Telnet) client handler.
|
||||
%%% Unlike other modules that represent discrete processes, this one does not adhere
|
||||
%%% to any OTP behavior. It does, however, adhere to OTP.
|
||||
%%%
|
||||
%%% In some cases it is more comfortable to write socket handlers or a certain
|
||||
%%% category of state machines as "pure" Erlang processes. This approach is made
|
||||
%%% OTP-able by use of the proc_lib module, which is the underlying library used
|
||||
%%% to write the stdlib's behaviors like gen_server, gen_statem, gen_fsm, etc.
|
||||
%%%
|
||||
%%% http://erlang.org/doc/design_principles/spec_proc.html
|
||||
%%% @end
|
||||
|
||||
-module(fd_client).
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-license("BSD-2-Clause-FreeBSD").
|
||||
|
||||
-export([start/1]).
|
||||
-export([start_link/1, init/2]).
|
||||
-export([system_continue/3, system_terminate/4,
|
||||
system_get_state/1, system_replace_state/2]).
|
||||
|
||||
|
||||
%%% Type and Record Definitions
|
||||
|
||||
|
||||
-record(s, {socket = none :: none | gen_tcp:socket()}).
|
||||
|
||||
|
||||
%% An alias for the state record above. Aliasing state can smooth out annoyances
|
||||
%% that can arise from using the record directly as its own type all over the code.
|
||||
|
||||
-type state() :: #s{}.
|
||||
|
||||
|
||||
%%% Service Interface
|
||||
|
||||
|
||||
-spec start(ListenSocket) -> Result
|
||||
when ListenSocket :: gen_tcp:socket(),
|
||||
Result :: {ok, pid()}
|
||||
| {error, Reason},
|
||||
Reason :: {already_started, pid()}
|
||||
| {shutdown, term()}
|
||||
| term().
|
||||
%% @private
|
||||
%% How the fd_client_man or a prior fd_client kicks things off.
|
||||
%% This is called in the context of fd_client_man or the prior fd_client.
|
||||
|
||||
start(ListenSocket) ->
|
||||
fd_client_sup:start_acceptor(ListenSocket).
|
||||
|
||||
|
||||
-spec start_link(ListenSocket) -> Result
|
||||
when ListenSocket :: gen_tcp:socket(),
|
||||
Result :: {ok, pid()}
|
||||
| {error, Reason},
|
||||
Reason :: {already_started, pid()}
|
||||
| {shutdown, term()}
|
||||
| term().
|
||||
%% @private
|
||||
%% This is called by the fd_client_sup. While start/1 is called to iniate a startup
|
||||
%% (essentially requesting a new worker be started by the supervisor), this is
|
||||
%% actually called in the context of the supervisor.
|
||||
|
||||
start_link(ListenSocket) ->
|
||||
proc_lib:start_link(?MODULE, init, [self(), ListenSocket]).
|
||||
|
||||
|
||||
-spec init(Parent, ListenSocket) -> no_return()
|
||||
when Parent :: pid(),
|
||||
ListenSocket :: gen_tcp:socket().
|
||||
%% @private
|
||||
%% This is the first code executed in the context of the new worker itself.
|
||||
%% This function does not have any return value, as the startup return is
|
||||
%% passed back to the supervisor by calling proc_lib:init_ack/2.
|
||||
%% We see the initial form of the typical arity-3 service loop form here in the
|
||||
%% call to listen/3.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
-spec listen(Parent, Debug, ListenSocket) -> no_return()
|
||||
when Parent :: pid(),
|
||||
Debug :: [sys:dbg_opt()],
|
||||
ListenSocket :: gen_tcp:socket().
|
||||
%% @private
|
||||
%% This function waits for a TCP connection. The owner of the socket is still
|
||||
%% the fd_client_man (so it can still close it on a call to fd_client_man:ignore/0),
|
||||
%% but the only one calling gen_tcp:accept/1 on it is this process. Closing the socket
|
||||
%% is one way a manager process can gracefully unblock child workers that are blocking
|
||||
%% on a network accept.
|
||||
%%
|
||||
%% Once it makes a TCP connection it will call start/1 to spawn its successor.
|
||||
|
||||
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 = fd_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.
|
||||
|
||||
|
||||
-spec loop(Parent, Debug, State) -> no_return()
|
||||
when Parent :: pid(),
|
||||
Debug :: [sys:dbg_opt()],
|
||||
State :: state().
|
||||
%% @private
|
||||
%% The service loop itself. This is the service state. The process blocks on receive
|
||||
%% of Erlang messages, TCP segments being received themselves as Erlang messages.
|
||||
|
||||
loop(Parent, Debug, State = #s{socket = Socket}) ->
|
||||
ok = inet:setopts(Socket, [{active, once}]),
|
||||
receive
|
||||
{tcp, Socket, <<"bye\r\n">>} ->
|
||||
ok = io:format("~p Client saying goodbye. Bye!~n", [self()]),
|
||||
ok = gen_tcp:send(Socket, "Bye!\r\n"),
|
||||
ok = gen_tcp:shutdown(Socket, read_write),
|
||||
exit(normal);
|
||||
{tcp, Socket, Message} ->
|
||||
ok = io:format("~p received: ~tp~n", [self(), Message]),
|
||||
ok = fd_client_man:echo(Message),
|
||||
loop(Parent, Debug, State);
|
||||
{relay, Sender, Message} when Sender == self() ->
|
||||
ok = gen_tcp:send(Socket, ["Message from YOU: ", Message]),
|
||||
loop(Parent, Debug, State);
|
||||
{relay, Sender, Message} ->
|
||||
From = io_lib:format("Message from ~tp: ", [Sender]),
|
||||
ok = gen_tcp:send(Socket, [From, Message]),
|
||||
loop(Parent, Debug, State);
|
||||
{tcp_closed, Socket} ->
|
||||
ok = io:format("~p Socket closed, retiring.~n", [self()]),
|
||||
exit(normal);
|
||||
{system, From, Request} ->
|
||||
sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
|
||||
Unexpected ->
|
||||
ok = io:format("~p Unexpected message: ~tp", [self(), Unexpected]),
|
||||
loop(Parent, Debug, State)
|
||||
end.
|
||||
|
||||
|
||||
-spec system_continue(Parent, Debug, State) -> no_return()
|
||||
when Parent :: pid(),
|
||||
Debug :: [sys:dbg_opt()],
|
||||
State :: state().
|
||||
%% @private
|
||||
%% The function called by the OTP internal functions after a system message has been
|
||||
%% handled. If the worker process has several possible states this is one place
|
||||
%% resumption of a specific state can be specified and dispatched.
|
||||
|
||||
system_continue(Parent, Debug, State) ->
|
||||
loop(Parent, Debug, State).
|
||||
|
||||
|
||||
-spec system_terminate(Reason, Parent, Debug, State) -> no_return()
|
||||
when Reason :: term(),
|
||||
Parent :: pid(),
|
||||
Debug :: [sys:dbg_opt()],
|
||||
State :: state().
|
||||
%% @private
|
||||
%% Called by the OTP inner bits to allow the process to terminate gracefully.
|
||||
%% Exactly when and if this is callback gets called is specified in the docs:
|
||||
%% See: http://erlang.org/doc/design_principles/spec_proc.html#msg
|
||||
|
||||
system_terminate(Reason, _Parent, _Debug, _State) ->
|
||||
exit(Reason).
|
||||
|
||||
|
||||
|
||||
-spec system_get_state(State) -> {ok, State}
|
||||
when State :: state().
|
||||
%% @private
|
||||
%% This function allows the runtime (or anything else) to inspect the running state
|
||||
%% of the worker process at any arbitrary time.
|
||||
|
||||
system_get_state(State) -> {ok, State}.
|
||||
|
||||
|
||||
-spec system_replace_state(StateFun, State) -> {ok, NewState, State}
|
||||
when StateFun :: fun(),
|
||||
State :: state(),
|
||||
NewState :: term().
|
||||
%% @private
|
||||
%% This function allows the system to update the process state in-place. This is most
|
||||
%% useful for state transitions between code types, like when performing a hot update
|
||||
%% (very cool, but sort of hard) or hot patching a running system (living on the edge!).
|
||||
|
||||
system_replace_state(StateFun, State) ->
|
||||
{ok, StateFun(State), State}.
|
||||
298
src/fd_client_man.erl
Normal file
298
src/fd_client_man.erl
Normal file
@ -0,0 +1,298 @@
|
||||
%%% @doc
|
||||
%%% front end web development lab Client Manager
|
||||
%%%
|
||||
%%% This is the "manager" part of the service->worker pattern.
|
||||
%%% It keeps track of who is connected and can act as a router among the workers.
|
||||
%%% Having this process allows us to abstract and customize service-level concepts
|
||||
%%% (the high-level ideas we care about in terms of solving an external problem in the
|
||||
%%% real world) and keep them separate from the lower-level details of supervision that
|
||||
%%% OTP should take care of for us.
|
||||
%%% @end
|
||||
|
||||
-module(fd_client_man).
|
||||
-vsn("0.1.0").
|
||||
-behavior(gen_server).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-license("BSD-2-Clause-FreeBSD").
|
||||
|
||||
-export([listen/1, ignore/0]).
|
||||
-export([enroll/0, echo/1]).
|
||||
-export([start_link/0]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
code_change/3, terminate/2]).
|
||||
|
||||
|
||||
%%% Type and Record Definitions
|
||||
|
||||
|
||||
-record(s, {port_num = none :: none | inet:port_number(),
|
||||
listener = none :: none | gen_tcp:socket(),
|
||||
clients = [] :: [pid()]}).
|
||||
|
||||
|
||||
-type state() :: #s{}.
|
||||
|
||||
|
||||
|
||||
%%% Service Interface
|
||||
|
||||
|
||||
-spec listen(PortNum) -> Result
|
||||
when PortNum :: inet:port_number(),
|
||||
Result :: ok
|
||||
| {error, Reason},
|
||||
Reason :: {listening, inet:port_number()}.
|
||||
%% @doc
|
||||
%% Tell the service to start listening on a given port.
|
||||
%% Only one port can be listened on at a time in the current implementation, so
|
||||
%% an error is returned if the service is already listening.
|
||||
|
||||
listen(PortNum) ->
|
||||
gen_server:call(?MODULE, {listen, PortNum}).
|
||||
|
||||
|
||||
-spec ignore() -> ok.
|
||||
%% @doc
|
||||
%% Tell the service to stop listening.
|
||||
%% It is not an error to call this function when the service is not listening.
|
||||
|
||||
ignore() ->
|
||||
gen_server:cast(?MODULE, ignore).
|
||||
|
||||
|
||||
|
||||
%%% Client Process Interface
|
||||
|
||||
|
||||
-spec enroll() -> ok.
|
||||
%% @doc
|
||||
%% Clients register here when they establish a connection.
|
||||
%% Other processes can enroll as well.
|
||||
|
||||
enroll() ->
|
||||
gen_server:cast(?MODULE, {enroll, self()}).
|
||||
|
||||
|
||||
-spec echo(Message) -> ok
|
||||
when Message :: string().
|
||||
%% @doc
|
||||
%% The function that tells the manager to broadcast a message to all clients.
|
||||
%% This can broadcast arbitrary strings to clients from non-clients as well.
|
||||
|
||||
echo(Message) ->
|
||||
gen_server:cast(?MODULE, {echo, Message, self()}).
|
||||
|
||||
|
||||
|
||||
%%% Startup Functions
|
||||
|
||||
|
||||
-spec start_link() -> Result
|
||||
when Result :: {ok, pid()}
|
||||
| {error, Reason :: term()}.
|
||||
%% @private
|
||||
%% This should only ever be called by fd_clients (the service-level supervisor).
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
|
||||
|
||||
|
||||
-spec init(none) -> {ok, state()}.
|
||||
%% @private
|
||||
%% Called by the supervisor process to give the process a chance to perform any
|
||||
%% preparatory work necessary for proper function.
|
||||
|
||||
init(none) ->
|
||||
ok = io:format("Starting.~n"),
|
||||
State = #s{},
|
||||
{ok, State}.
|
||||
|
||||
|
||||
|
||||
%%% gen_server Message Handling Callbacks
|
||||
|
||||
|
||||
-spec handle_call(Message, From, State) -> Result
|
||||
when Message :: term(),
|
||||
From :: {pid(), reference()},
|
||||
State :: state(),
|
||||
Result :: {reply, Response, NewState}
|
||||
| {noreply, State},
|
||||
Response :: ok
|
||||
| {error, {listening, inet:port_number()}},
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% The gen_server:handle_call/3 callback.
|
||||
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3
|
||||
|
||||
handle_call({listen, PortNum}, _, State) ->
|
||||
{Response, NewState} = do_listen(PortNum, State),
|
||||
{reply, Response, NewState};
|
||||
handle_call(Unexpected, From, State) ->
|
||||
ok = io:format("~p Unexpected call from ~tp: ~tp~n", [self(), From, Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
-spec handle_cast(Message, State) -> {noreply, NewState}
|
||||
when Message :: term(),
|
||||
State :: state(),
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% The gen_server:handle_cast/2 callback.
|
||||
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2
|
||||
|
||||
handle_cast({enroll, Pid}, State) ->
|
||||
NewState = do_enroll(Pid, State),
|
||||
{noreply, NewState};
|
||||
handle_cast({echo, Message, Sender}, State) ->
|
||||
ok = do_echo(Message, Sender, State),
|
||||
{noreply, State};
|
||||
handle_cast(ignore, State) ->
|
||||
NewState = do_ignore(State),
|
||||
{noreply, NewState};
|
||||
handle_cast(Unexpected, State) ->
|
||||
ok = io:format("~p Unexpected cast: ~tp~n", [self(), Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
-spec handle_info(Message, State) -> {noreply, NewState}
|
||||
when Message :: term(),
|
||||
State :: state(),
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% The gen_server:handle_info/2 callback.
|
||||
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2
|
||||
|
||||
handle_info({'DOWN', Mon, process, Pid, Reason}, State) ->
|
||||
NewState = handle_down(Mon, Pid, Reason, State),
|
||||
{noreply, NewState};
|
||||
handle_info(Unexpected, State) ->
|
||||
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
|
||||
%%% OTP Service Functions
|
||||
|
||||
-spec code_change(OldVersion, State, Extra) -> Result
|
||||
when OldVersion :: {down, Version} | Version,
|
||||
Version :: term(),
|
||||
State :: state(),
|
||||
Extra :: term(),
|
||||
Result :: {ok, NewState}
|
||||
| {error, Reason :: term()},
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% The gen_server:code_change/3 callback.
|
||||
%% See: http://erlang.org/doc/man/gen_server.html#Module:code_change-3
|
||||
|
||||
code_change(_, State, _) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
-spec terminate(Reason, State) -> no_return()
|
||||
when Reason :: normal
|
||||
| shutdown
|
||||
| {shutdown, term()}
|
||||
| term(),
|
||||
State :: state().
|
||||
%% @private
|
||||
%% The gen_server:terminate/2 callback.
|
||||
%% See: http://erlang.org/doc/man/gen_server.html#Module:terminate-2
|
||||
|
||||
terminate(_, _) ->
|
||||
ok.
|
||||
|
||||
|
||||
|
||||
%%% Doer Functions
|
||||
|
||||
-spec do_listen(PortNum, State) -> {Result, NewState}
|
||||
when PortNum :: inet:port_number(),
|
||||
State :: state(),
|
||||
Result :: ok
|
||||
| {error, Reason :: {listening, inet:port_number()}},
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% The "doer" procedure called when a "listen" message is received.
|
||||
|
||||
do_listen(PortNum, State = #s{port_num = none}) ->
|
||||
SocketOptions =
|
||||
[inet6,
|
||||
{packet, line},
|
||||
{active, once},
|
||||
{mode, binary},
|
||||
{keepalive, true},
|
||||
{reuseaddr, true}],
|
||||
{ok, Listener} = gen_tcp:listen(PortNum, SocketOptions),
|
||||
{ok, _} = fd_client:start(Listener),
|
||||
{ok, State#s{port_num = PortNum, listener = Listener}};
|
||||
do_listen(_, State = #s{port_num = PortNum}) ->
|
||||
ok = io:format("~p Already listening on ~p~n", [self(), PortNum]),
|
||||
{{error, {listening, PortNum}}, State}.
|
||||
|
||||
|
||||
-spec do_ignore(State) -> NewState
|
||||
when State :: state(),
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% The "doer" procedure called when an "ignore" message is received.
|
||||
|
||||
do_ignore(State = #s{listener = none}) ->
|
||||
State;
|
||||
do_ignore(State = #s{listener = Listener}) ->
|
||||
ok = gen_tcp:close(Listener),
|
||||
State#s{port_num = none, listener = none}.
|
||||
|
||||
|
||||
-spec do_enroll(Pid, State) -> NewState
|
||||
when Pid :: pid(),
|
||||
State :: state(),
|
||||
NewState :: state().
|
||||
|
||||
do_enroll(Pid, State = #s{clients = Clients}) ->
|
||||
case lists:member(Pid, Clients) of
|
||||
false ->
|
||||
Mon = monitor(process, Pid),
|
||||
ok = io:format("Monitoring ~tp @ ~tp~n", [Pid, Mon]),
|
||||
State#s{clients = [Pid | Clients]};
|
||||
true ->
|
||||
State
|
||||
end.
|
||||
|
||||
|
||||
-spec do_echo(Message, Sender, State) -> ok
|
||||
when Message :: string(),
|
||||
Sender :: pid(),
|
||||
State :: state().
|
||||
%% @private
|
||||
%% The "doer" procedure called when an "echo" message is received.
|
||||
|
||||
do_echo(Message, Sender, #s{clients = Clients}) ->
|
||||
Send = fun(Client) -> Client ! {relay, Sender, Message} end,
|
||||
lists:foreach(Send, Clients).
|
||||
|
||||
|
||||
-spec handle_down(Mon, Pid, Reason, State) -> NewState
|
||||
when Mon :: reference(),
|
||||
Pid :: pid(),
|
||||
Reason :: term(),
|
||||
State :: state(),
|
||||
NewState :: state().
|
||||
%% @private
|
||||
%% Deal with monitors. When a new process enrolls as a client a monitor is set and
|
||||
%% the process is added to the client list. When the process terminates we receive
|
||||
%% a 'DOWN' message from the monitor. More sophisticated work managers typically have
|
||||
%% an "unenroll" function, but this echo service doesn't need one.
|
||||
|
||||
handle_down(Mon, Pid, Reason, State = #s{clients = Clients}) ->
|
||||
case lists:member(Pid, Clients) of
|
||||
true ->
|
||||
NewClients = lists:delete(Pid, Clients),
|
||||
State#s{clients = NewClients};
|
||||
false ->
|
||||
Unexpected = {'DOWN', Mon, process, Pid, Reason},
|
||||
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||
State
|
||||
end.
|
||||
70
src/fd_client_sup.erl
Normal file
70
src/fd_client_sup.erl
Normal file
@ -0,0 +1,70 @@
|
||||
%%% @doc
|
||||
%%% front end web development lab Client Supervisor
|
||||
%%%
|
||||
%%% This process supervises the client socket handlers themselves. It is a peer of the
|
||||
%%% fd_client_man (the manager interface to this network service component),
|
||||
%%% and a child of the supervisor named fd_clients.
|
||||
%%%
|
||||
%%% Because we don't know (or care) how many client connections the server may end up
|
||||
%%% handling this is a simple_one_for_one supervisor which can spawn and manage as
|
||||
%%% many identically defined workers as required, but cannot supervise any other types
|
||||
%%% of processes (one of the tradeoffs of the "simple" in `simple_one_for_one').
|
||||
%%%
|
||||
%%% http://erlang.org/doc/design_principles/sup_princ.html#id79244
|
||||
%%% @end
|
||||
|
||||
-module(fd_client_sup).
|
||||
-vsn("0.1.0").
|
||||
-behaviour(supervisor).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-license("BSD-2-Clause-FreeBSD").
|
||||
|
||||
|
||||
-export([start_acceptor/1]).
|
||||
-export([start_link/0]).
|
||||
-export([init/1]).
|
||||
|
||||
|
||||
|
||||
-spec start_acceptor(ListenSocket) -> Result
|
||||
when ListenSocket :: gen_tcp:socket(),
|
||||
Result :: {ok, pid()}
|
||||
| {error, Reason},
|
||||
Reason :: {already_started, pid()}
|
||||
| {shutdown, term()}
|
||||
| term().
|
||||
%% @private
|
||||
%% Spawns the first listener at the request of the fd_client_man when
|
||||
%% fewd:listen/1 is called, or the next listener at the request of the
|
||||
%% currently listening fd_client when a connection is made.
|
||||
%%
|
||||
%% Error conditions, supervision strategies and other important issues are
|
||||
%% explained in the supervisor module docs:
|
||||
%% http://erlang.org/doc/man/supervisor.html
|
||||
|
||||
start_acceptor(ListenSocket) ->
|
||||
supervisor:start_child(?MODULE, [ListenSocket]).
|
||||
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
%% @private
|
||||
%% This supervisor's own start function.
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, none).
|
||||
|
||||
|
||||
-spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
%% @private
|
||||
%% The OTP init/1 function.
|
||||
|
||||
init(none) ->
|
||||
RestartStrategy = {simple_one_for_one, 1, 60},
|
||||
Client = {fd_client,
|
||||
{fd_client, start_link, []},
|
||||
temporary,
|
||||
brutal_kill,
|
||||
worker,
|
||||
[fd_client]},
|
||||
{ok, {RestartStrategy, [Client]}}.
|
||||
48
src/fd_clients.erl
Normal file
48
src/fd_clients.erl
Normal file
@ -0,0 +1,48 @@
|
||||
%%% @doc
|
||||
%%% front end web development lab Client Service Supervisor
|
||||
%%%
|
||||
%%% This is the service-level supervisor of the system. It is the parent of both the
|
||||
%%% client connection handlers and the client manager (which manages the client
|
||||
%%% connection handlers). This is the child of fd_sup.
|
||||
%%%
|
||||
%%% See: http://erlang.org/doc/apps/kernel/application.html
|
||||
%%% @end
|
||||
|
||||
-module(fd_clients).
|
||||
-vsn("0.1.0").
|
||||
-behavior(supervisor).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-license("BSD-2-Clause-FreeBSD").
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([init/1]).
|
||||
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
%% @private
|
||||
%% This supervisor's own start function.
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, none).
|
||||
|
||||
-spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
%% @private
|
||||
%% The OTP init/1 function.
|
||||
|
||||
init(none) ->
|
||||
RestartStrategy = {rest_for_one, 1, 60},
|
||||
ClientMan = {fd_client_man,
|
||||
{fd_client_man, start_link, []},
|
||||
permanent,
|
||||
5000,
|
||||
worker,
|
||||
[fd_client_man]},
|
||||
ClientSup = {fd_client_sup,
|
||||
{fd_client_sup, start_link, []},
|
||||
permanent,
|
||||
5000,
|
||||
supervisor,
|
||||
[fd_client_sup]},
|
||||
Children = [ClientSup, ClientMan],
|
||||
{ok, {RestartStrategy, Children}}.
|
||||
46
src/fd_sup.erl
Normal file
46
src/fd_sup.erl
Normal file
@ -0,0 +1,46 @@
|
||||
%%% @doc
|
||||
%%% front end web development lab Top-level Supervisor
|
||||
%%%
|
||||
%%% The very top level supervisor in the system. It only has one service branch: the
|
||||
%%% client handling service. In a more complex system the client handling service would
|
||||
%%% only be one part of a larger system. Were this a game system, for example, the
|
||||
%%% item data management service would be a peer, as would a login credential provision
|
||||
%%% service, game world event handling, and so on.
|
||||
%%%
|
||||
%%% See: http://erlang.org/doc/design_principles/applications.html
|
||||
%%% See: http://zxq9.com/archives/1311
|
||||
%%% @end
|
||||
|
||||
-module(fd_sup).
|
||||
-vsn("0.1.0").
|
||||
-behaviour(supervisor).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-license("BSD-2-Clause-FreeBSD").
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([init/1]).
|
||||
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
%% @private
|
||||
%% This supervisor's own start function.
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
|
||||
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
%% @private
|
||||
%% The OTP init/1 function.
|
||||
|
||||
init([]) ->
|
||||
RestartStrategy = {one_for_one, 1, 60},
|
||||
Clients = {fd_clients,
|
||||
{fd_clients, start_link, []},
|
||||
permanent,
|
||||
5000,
|
||||
supervisor,
|
||||
[fd_clients]},
|
||||
Children = [Clients],
|
||||
{ok, {RestartStrategy, Children}}.
|
||||
76
src/fewd.erl
Normal file
76
src/fewd.erl
Normal file
@ -0,0 +1,76 @@
|
||||
%%% @doc
|
||||
%%% front end web development lab
|
||||
%%% @end
|
||||
|
||||
-module(fewd).
|
||||
-vsn("0.1.0").
|
||||
-behavior(application).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-license("BSD-2-Clause-FreeBSD").
|
||||
|
||||
-export([listen/1, ignore/0]).
|
||||
-export([start/0, start/1]).
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
|
||||
|
||||
-spec listen(PortNum) -> Result
|
||||
when PortNum :: inet:port_num(),
|
||||
Result :: ok
|
||||
| {error, {listening, inet:port_num()}}.
|
||||
%% @doc
|
||||
%% Make the server start listening on a port.
|
||||
%% Returns an {error, Reason} tuple if it is already listening.
|
||||
|
||||
listen(PortNum) ->
|
||||
fd_client_man:listen(PortNum).
|
||||
|
||||
|
||||
-spec ignore() -> ok.
|
||||
%% @doc
|
||||
%% Make the server stop listening if it is, or continue to do nothing if it isn't.
|
||||
|
||||
ignore() ->
|
||||
fd_client_man:ignore().
|
||||
|
||||
|
||||
-spec start() -> ok.
|
||||
%% @doc
|
||||
%% Start the server in an "ignore" state.
|
||||
|
||||
start() ->
|
||||
ok = application:ensure_started(sasl),
|
||||
ok = application:start(fewd),
|
||||
io:format("Starting...").
|
||||
|
||||
|
||||
-spec start(PortNum) -> ok
|
||||
when PortNum :: inet:port_number().
|
||||
%% @doc
|
||||
%% Start the server and begin listening immediately. Slightly more convenient when
|
||||
%% playing around in the shell.
|
||||
|
||||
start(PortNum) ->
|
||||
ok = start(),
|
||||
ok = fd_client_man:listen(PortNum),
|
||||
io:format("Startup complete, listening on ~w~n", [PortNum]).
|
||||
|
||||
|
||||
-spec start(normal, term()) -> {ok, pid()}.
|
||||
%% @private
|
||||
%% Called by OTP to kick things off. This is for the use of the "application" part of
|
||||
%% OTP, not to be called by user code.
|
||||
%% See: http://erlang.org/doc/apps/kernel/application.html
|
||||
|
||||
start(normal, _Args) ->
|
||||
fd_sup:start_link().
|
||||
|
||||
|
||||
-spec stop(term()) -> ok.
|
||||
%% @private
|
||||
%% Similar to start/2 above, this is to be called by the "application" part of OTP,
|
||||
%% not client code. Causes a (hopefully graceful) shutdown of the application.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
17
zomp.meta
Normal file
17
zomp.meta
Normal file
@ -0,0 +1,17 @@
|
||||
{name,"front end web development lab"}.
|
||||
{type,app}.
|
||||
{modules,[]}.
|
||||
{prefix,"fd"}.
|
||||
{author,"Peter Harpending"}.
|
||||
{desc,"Front End Web Dev in Erlang stuff"}.
|
||||
{package_id,{"otpr","fewd",{0,1,0}}}.
|
||||
{deps,[]}.
|
||||
{key_name,none}.
|
||||
{a_email,"peterharpending@qpq.swiss"}.
|
||||
{c_email,"peterharpending@qpq.swiss"}.
|
||||
{copyright,"Peter Harpending"}.
|
||||
{file_exts,[]}.
|
||||
{license,"BSD-2-Clause-FreeBSD"}.
|
||||
{repo_url,[]}.
|
||||
{tags,[]}.
|
||||
{ws_url,[]}.
|
||||
Loading…
x
Reference in New Issue
Block a user