initial commit;

This commit is contained in:
Peter Harpending 2025-09-16 14:41:39 -07:00
commit 71909e79b0
11 changed files with 810 additions and 0 deletions

15
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1 @@
{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}.

26
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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,[]}.