Embedded Applications with Yaws
08 February 2008Much like the previous article about dynamically sizing fragmented mnesia tables, this article continues the quest to create examples for things thatI see as lacking in documentation or example.
This is going to be a very brief guide to creating erlang applications that implement an embedded yaws handler. The name of our example application is yawsapp and its layout looks something like this:
yawsapp.app
yawsapp.hrl
yawsapp.erl
yawsapp_sup.erl
yawsapp_server.erl
yawsapp_handler.erlThe process topology looks like this
yawsapp
+- yawsapp_sup
+- yawsapp_server
yaws
+- yawsapp_handler
The yawsapp.app and yawsapp.erl files define and implement the basic application behavior. The yawsapp_sup.erl module creates the supervisor tree as part of the application. The yawsapp_server.erl module manages the embedded yaws application and configuration as part of the gen_server behavior.
Listing 1-1: The yawsapp.app application configuration file.
{application, yawsapp,
[{description, "An embedded yaws application example."},
{vsn, "0.1"},
{modules, [yawsapp, yawsapp_sup, yawsapp_server]},
{registered, [yawsapp]},
{env, [
{port, 8006},
{working_dir, "/Users/ngerakines/dev/projects/tutorials/yaws_app/"}
]},
{applications, [kernel, stdlib, sasl]},
{mod, {yawsapp, []}}
]}.
The application configure, as seen in listing 1-1, is pretty basic. We define the application, its related modules and set a few environmental variables. The working_dir variable is used to determine where the yaws log and tmp directory will be located.
Listing 1-2: The yawsapp.hrl file.
-define(MAX_RESTART, 5).
-define(MAX_TIME, 60).
-define(SHUTDOWN_WAITING_TIME, 2000).
The yawsapp.hrl file would be were we defined any records or global constants used by our application. In this application, we have three constants that are used to define the behavior of the supervisor tree.
Listing 1-3: The yawsapp.erl file.
-module(yawsapp).
-behaviour(application).
-export([start/2, stop/1]).
-include("yawsapp.hrl").
start(_Type, _Args) ->
mnesia:start(),
application:start(inets),
Args = lists:map(
fun (Var) -> {ok, Value} = application:get_env(?MODULE, Var), Value end,
[port, working_dir]
),
yawsapp_sup:start_link(Args).
stop(_State) -> ok.
The core yawsapp.erl file provides the yawsapp:start/2 and yawsapp:stop/1 functions as per the application behavior. You'll also note that it pulls the two environmental variables set in the application configuration file. Those values are passed to the yawsapp_sup:start_link/1 function.
Listing 1-4: The yawsapp_sup.erl file.
-module(yawsapp_sup).
-behaviour(supervisor).
-export([start_link/1, init/1]).
-include("yawsapp.hrl").
start_link(Args) ->
supervisor:start_link({local, ?MODULE}, ?MODULE, Args).
init(Args) ->
get_init_result(Args).
get_init_result(Args) ->
Flags = {one_for_one, 2, 10},
Children = [worker_spec(yawsapp_yaws1, yawsapp_server, [Args])],
{ok, {Flags, Children}}.
worker_spec(Id, Module, Args) ->
StartFunc = {Module, start_link, Args},
{Id, StartFunc, permanent, ?SHUTDOWN_WAITING_TIME, worker, [Module]}.
The yawsapp_sup module defines the supervisor tree for the application. The only thing to note here is the use of the get_init_results/1 and worker_spec/3 functions. This module includes no magic.
Listing 1-5: The yawsapp_server.erl file.
-module(yawsapp_server).
-behaviour(gen_server).
-include("/usr/lib/yaws/include/yaws.hrl").
-include("yawsapp.hrl").
-export([
start_link/1, init/1,
handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3
]).
start_link(Args) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
init(Args) ->
process_flag(trap_exit, true),
case application:start(yaws) of
ok -> set_conf(Args);
Error -> {stop, Error}
end.
set_conf([Port, WorkingDir]) ->
GC = #gconf{
trace = false,
logdir = WorkingDir ++ "/logs",
yaws = "YawsApp 1.0",
tmpdir = WorkingDir ++ "/.yaws"
},
SC = #sconf{
port = Port,
servername = "localhost",
listen = {0, 0, 0, 0},
docroot = "/tmp",
appmods = [{"/", yawsapp_handler}]
},
case catch yaws_api:setconf(GC, [[SC]]) of
ok -> {ok, started};
Error -> {stop, Error}
end.
handle_call(Request, _From, State) -> {stop, {unknown_call, Request}, State}.
handle_cast(_Message, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) ->
application:stop(yaws),
ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
The yawsapp_server module does the bulk of the work involved in setting up and initializing the embedded yaws application and its configuration. This module is called first through the yawsapp_server:start_link/1 function that implements the gen_server behavior of the module.
During the init phase the module attempts to start the yaws application and on success it builds two configuration variables that are then passed to yaws_api:setconf/2. The configuration variables set the standard options, such trace level, log directory, port, docroot, etc. It also sets the appmod configuration option dictating that the "/" request is handled by the yawsapp_handler module.
Listing 1-6: The yawsapp_handler.erl file.
-module(yawsapp_handler).
-include("/usr/lib/yaws/include/yaws.hrl").
-include("/usr/lib/yaws/include/yaws_api.hrl").
-include("yawsapp.hrl").
-export([out/1, handle_request/3]).
out(Arg) ->
Req = Arg#arg.req,
ReqPath = get_path(Arg),
handle_request(Req#http_request.method, ReqPath, Arg).
get_path(Arg) ->
Req = Arg#arg.req,
{abs_path, Path} = Req#http_request.path,
Path.
handle_request('GET', [47,97,99,99,111,117,110,116 | _], _Arg) -> % "/account" ...
make_response(200, "<p>Please login or logout.</p>");
handle_request('GET', [47,112,114,111,102,105,108,101 | _], _Arg) -> % "/profile" ...
make_response(200, "<p>This is a slick profile.</p>");
handle_request(_, _, _Arg) -> % catchall
make_response(200, "<p>What exactly are you looking for?</p>").
make_response(Status, Message) ->
make_response(Status, "text/html", Message).
make_response(Status, Type, Message) ->
make_all_response(Status, make_header(Type), Message).
make_header(Type) ->
[{header, ["Content-Type: ", Type]}].
make_all_response(Status, Headers, Message) ->
[{status, Status}, {allheaders, Headers}, {html, Message}].
The yawsapp_handler module is the module used by yaws to process incoming requests. It provides the yawsapp_handler:out/1 function that Yaws calls with the record that defines the incoming request. From that method we do a very simple dispatch through the yawsapp_handler:handle_request/3 function based on the request method, request path and request argument.
In this example we provide functions to handle /account and /profile locations as well as a function to handle all other requests (a catch-all). This simple application can easily be extended to act as an interface to your erlang applications. You may have to adjust some of the yaws include locations to suite your erlang installation, but its generic enough to work for most people as-is.
I use the following shell command to start the erlang shell when running this application:
erl +A 1 +Ktrue -boot start_sasl +W w -sname yawsappnode -pa /usr/lib/yaws/ebin -yaws embedded true
Once in the shell the application can be started with application:start(yawsapp). to get things going.