From 71909e79b0dfe719ab3c2e708a8b07628e9a3aa0 Mon Sep 17 00:00:00 2001 From: Peter Harpending Date: Tue, 16 Sep 2025 14:41:39 -0700 Subject: [PATCH] initial commit; --- .gitignore | 15 +++ Emakefile | 1 + LICENSE | 26 ++++ ebin/fewd.app | 9 ++ src/fd_client.erl | 204 +++++++++++++++++++++++++++++ src/fd_client_man.erl | 298 ++++++++++++++++++++++++++++++++++++++++++ src/fd_client_sup.erl | 70 ++++++++++ src/fd_clients.erl | 48 +++++++ src/fd_sup.erl | 46 +++++++ src/fewd.erl | 76 +++++++++++ zomp.meta | 17 +++ 11 files changed, 810 insertions(+) create mode 100644 .gitignore create mode 100644 Emakefile create mode 100644 LICENSE create mode 100644 ebin/fewd.app create mode 100644 src/fd_client.erl create mode 100644 src/fd_client_man.erl create mode 100644 src/fd_client_sup.erl create mode 100644 src/fd_clients.erl create mode 100644 src/fd_sup.erl create mode 100644 src/fewd.erl create mode 100644 zomp.meta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20177b4 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Emakefile b/Emakefile new file mode 100644 index 0000000..68c7b67 --- /dev/null +++ b/Emakefile @@ -0,0 +1 @@ +{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..926f1b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright 2025 Peter Harpending + +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. diff --git a/ebin/fewd.app b/ebin/fewd.app new file mode 100644 index 0000000..5ef8bbb --- /dev/null +++ b/ebin/fewd.app @@ -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,[]}}]}. diff --git a/src/fd_client.erl b/src/fd_client.erl new file mode 100644 index 0000000..f5d7758 --- /dev/null +++ b/src/fd_client.erl @@ -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 "). +-copyright("Peter Harpending "). +-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}. diff --git a/src/fd_client_man.erl b/src/fd_client_man.erl new file mode 100644 index 0000000..611cbcf --- /dev/null +++ b/src/fd_client_man.erl @@ -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 "). +-copyright("Peter Harpending "). +-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. diff --git a/src/fd_client_sup.erl b/src/fd_client_sup.erl new file mode 100644 index 0000000..8040311 --- /dev/null +++ b/src/fd_client_sup.erl @@ -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 "). +-copyright("Peter Harpending "). +-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]}}. diff --git a/src/fd_clients.erl b/src/fd_clients.erl new file mode 100644 index 0000000..db23186 --- /dev/null +++ b/src/fd_clients.erl @@ -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 "). +-copyright("Peter Harpending "). +-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}}. diff --git a/src/fd_sup.erl b/src/fd_sup.erl new file mode 100644 index 0000000..f84c8ce --- /dev/null +++ b/src/fd_sup.erl @@ -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 "). +-copyright("Peter Harpending "). +-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}}. diff --git a/src/fewd.erl b/src/fewd.erl new file mode 100644 index 0000000..54ef416 --- /dev/null +++ b/src/fewd.erl @@ -0,0 +1,76 @@ +%%% @doc +%%% front end web development lab +%%% @end + +-module(fewd). +-vsn("0.1.0"). +-behavior(application). +-author("Peter Harpending "). +-copyright("Peter Harpending "). +-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. diff --git a/zomp.meta b/zomp.meta new file mode 100644 index 0000000..fdf6649 --- /dev/null +++ b/zomp.meta @@ -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,[]}.