Saturday, 3 November 2012

A simple erlang daemon

Hello gentle readers.

Recently I have been learning the erlang programming language.  One task I gave myself was to write a linux daemon.

As you probably already know, daemons are used to run unix services.  Services commonly controlled by daemons include database servers, web servers, web proxies etc.  In this example the server is very simple, the client calls the function "say_hi" and the server responds with "hello".

In the linux environment daemons are controlled by scripts that are stored in places such as /etc/init.d.  These scripts respond according to convention to the commands start, stop and restart.

 Let us start with the shell script:

#!/bin/sh

EBIN=$HOME/Documents/Erlang/Daemon

ERL=/usr/bin/erl

case $1 in

  start|stop|restart)
    $ERL -detached -sname mynode \
           -run daemon shell_do $1  >> daemon2.log
    ;;


  *)
    echo "Usage: $0 {start|stop|restart}"
    exit 1
esac

exit 0


This has to be one of the simplest shell scripts that you have ever seen.  Daemon respond to three different commands, stop, start and restart.  In this script the command is simply passed through to the daemon.  One improvement would be to exit with the return code from the daemon execution.

So how about the daemon?  Here it is...

%% PURPOSE
%%
%% Manage an erlang daemon process as controlled by a shell scripts
%%   Allow standard daemon control verbs
%%      Start  - Starts a daemon in detached mode and exits
%%      Stop   - Attaches to the daemon, monitors it, sends an EXIT message and waits for it to die
%%      Restart - Calls stop and then start
%%   Log events
%%   Return UNIX compatible codes for functions called from shell scripts
%%   Exit shell script calls so as to not stop the scripts from completing
%%   Shell scripts expected to use shell_do to execute functions
%%
%% Allow interaction with daemon from other erlang nodes. 
%%   Erlang processes are expected to call functions directly rather than through shell_do
%%
%% MOTIVATION
%%   Erlang is great, but as an application it needs to be managed by system scripts.
%%   This is particularly for process that are expected to be running without user initiation.
%%
%% INVOCATION
%%   See daemon.sh for details of calling this module from a shell script.
%%
%% TO DO
%%   Define and use error handler for spawn call.

-module(daemon).
%-compile([{debug_info}]).
-export [start/0,start/1,stop_daemon/0,say_hi/0,kill/0,shell_do/1].
%%-define (DAEMON_NAME,daemon@blessing).
-define (DAEMON_NAME,list_to_atom("daemon@"++net_adm:localhost())).
-define (UNIX_OKAY_RESULT,0).
-define (TIMEOUT_STARTING_VM,1).
-define (VM_STARTED_WITHOUT_NAME,2).
-define (INVALID_VERB,3).
-define (COULD_NOT_CONNECT,4).
-define (TIMEOUT_WAITING_QUIT,5).
-define (TIMEOUT_STOPPING_VM,6).

wait_vm_start(_,0) -> ?TIMEOUT_STARTING_VM;
wait_vm_start(D,N) ->
   net_kernel:connect(D),
   Dl = lists:filter(fun(X) -> X==D end,nodes()),
   if Dl =:= [] ->
       receive after 1000 -> true end,
       wait_vm_start(D,N-1);
     Dl /= [] -> ?UNIX_OKAY_RESULT
   end.

wait_vm_stop(_,0) -> ?TIMEOUT_STOPPING_VM;
wait_vm_stop(D,N) ->
   net_kernel:connect(D),
   Dl = lists:filter(fun(X) -> X==D end,nodes()),
   if Dl /= [] ->
       receive after 1000 -> true end,
       wait_vm_start(D,N-1);
      Dl == [] -> ?UNIX_OKAY_RESULT
   end.

flush() ->
    receive
        _ ->
            flush()
    after
        0 ->
            true
    end.

sd(Hdl) ->
   MyNode=node(),
   if
      MyNode =:= nonode@nohost ->
         info(stdout,"~s","Error: Erlang not started with a name.  Use -sname <name>"),
         ?VM_STARTED_WITHOUT_NAME;
      MyNode /= nonode@nohost ->
         Atm_daemon = ?DAEMON_NAME,
         Connected = net_kernel:connect(Atm_daemon),
         case Connected of
            true ->
               info(Hdl,"~s",["daemon process already started"]),
               ?UNIX_OKAY_RESULT;
            false ->
               info(Hdl,"~s",["starting daemon process"]),
               StartString = "erl -detached -sname daemon",
               os:cmd(StartString),
               Vm_daemon = wait_vm_start(Atm_daemon,10),
               case Vm_daemon of
                  ?UNIX_OKAY_RESULT ->
                     info(Hdl,"~s",["spawning main daemon process"]),
                     spawn(Atm_daemon,?MODULE,start,[]), ?UNIX_OKAY_RESULT;
                  A -> A
               end
         end % case Connected %
   end.


say_hi() ->
   Daemon = ?DAEMON_NAME,
   Connected = net_kernel:connect(Daemon),
   if Connected ->
      {listener,Daemon} ! {hello,self()},
      receive
          Response -> Response
      after 10000 -> timeout end;
      not Connected -> could_not_connect
   end.


stop_daemon() ->
   Daemon = ?DAEMON_NAME,
   Connected = net_kernel:connect(Daemon),
   if Connected ->
         flush(),
         {listener,Daemon} ! {quit,self()},
         receive
         bye -> wait_vm_stop(Daemon,10)
     after 10000 -> ?TIMEOUT_WAITING_QUIT
         end;
      not Connected -> ?COULD_NOT_CONNECT
   end.

shell_do(Verb) ->
   {A,Hdl} = file:open('daemon_client.log',[append]),
   case A of
      ok ->
       info(Hdl,"~s",[Verb]);
      error  -> error
   end,
   Result = handle_verb(Hdl,Verb),
   info(Hdl,"Return status ~.10B",[Result]),
   init:stop(Result).

%%handle_verb(_,_) -> 0;

handle_verb(Hdl,["start"]) -> sd(Hdl);
handle_verb(_,["stop"]) ->  stop_daemon();
handle_verb(Hdl,["restart"]) ->
    stop_daemon(),
    sd(Hdl);
handle_verb(Hdl,X) ->   
    info(Hdl,"handle_verb failed to match ~p",[X]),
    ?INVALID_VERB.
    
kill() ->
    rpc:call(?DAEMON_NAME, init, stop, []).

start(Source) ->
    Source ! starting,
    start().
   
start() ->
   register(listener,self()),
   case {_,Hdl}=file:open("daemon_server.log",[append]) of
     {ok,Hdl}    -> server(Hdl);
     {error,Hdl} -> {error,Hdl}
   end.
  
info(Hdl,Fmt,D)->
  io:fwrite(Hdl,"~w"++Fmt++"~n",[erlang:localtime()] ++ D).

server(Hdl) ->
   info(Hdl,"~s",["waiting"]),
   receive
      {hello,Sender} ->
          info(Hdl,"~s~w",["hello received from",Sender]),
          Sender ! hello,
          server(Hdl);
      {getpid,Sender} ->
          info(Hdl,"~s~w",["pid request from ",Sender]),
          Sender ! self(),
          server(Hdl);
      {quit,Sender} ->
          info(Hdl,"~s~w",["quit recevied from ",Sender]),
          Sender ! bye,
          init:stop();
      _ ->
          info(Hdl,"~s",["Unknown message received"])
      after
          50000 ->
             server(Hdl)
   end.


For the reader not used to reading erlang, there some of this code is run as a result of the shell script we saw above.  Other code in this file is the daemon itself.  Referring back to the shell script we see that the script calls procedure shell_do.  Shell_do writes log entries, calls handle_verb and exits.  Handle_verb implements the different behaviours for each verb.  Starting the daemon is handled by function sd, which creates the daemon by an operating system call os:cmd, waits for the erlang virtual machine to initialise, and then spawns the server code called start, which in turn calls server.

This daemon code is quite straightforward and could form the basis of a generic server in the style of OTP.  I hope it will be useful to others who wish to build their own daemons in erlang.


Tony