Thursday, January 14, 2010

Minor Erlang Interface Tricks

erlrc requires that you drop a file whose name is your node name and whose contents is the node cookie into a particular directory so that the packaging system can find running Erlang VMs and ask them to do hot-code upgrades. It's not that hard, but I figured I would put some shell scripts into the erlrc google code project demonstrating how to do this and other minor tricks that make it a little nicer to talk to an Erlang VM from the (non-Erlang) command line.
All of these scripts are driven by a POSIX shell syntax configuration file describing an Erlang VM, which can be pretty short. Here's one I'm using right now for my personal stuff:
% cat /usr/share/myerlnode/myerlnode.rc
node_name_file='/etc/erlrc.d/nodes/erlang'

run_extra_args='+A 64 -noshell -noinput -s crypto -s mnesia -eval "case erlrc_boot:boot () of ok -> ok; _ -> init:stop () end" >erlang.out 2>erlang.err &'

node_name_file (the only required config setting) defines what the node name will be (via the basename). This config file drives four scripts:
  1. erlstart-run-erlang: starts an Erlang VM. doesn't do that much, really: creates the node file for erlrc, and sets the heart command in case you want to use heart.
  2. erlstart-remsh: starts a remote shell on an Erlang VM.
  3. erlstart-eval: takes the argument, evals it on an Erlang VM, and prints the result to standard out. very useful shell script glue for maintenance scripts.
  4. erlstart-etop: runs etop on an Erlang VM.
If you put your config file in the default location (/etc/erlstart.rc) or export an environment variable indicating the location (ERLSTART_CONFIG_FILE) then things are pretty zero-configuration. Doing this kind of thing at the (non-Erlang) shell gets very addictive:
% erlstart-eval 'application:which_applications()'
[{anwhereos,"Rest API for time series persistence and retrieval.","0.1.0"},
{drurlyjsclientsrv,"Serve the drurly jsclient from the drurly server.",
"0.0.1"},
{drurly,"Social sharing server.","2.2.0"},
{mcedemo,"TinyMCE + Nitrogen demo.","1.3.0"},
{inets,"INETS CXC 138 49","5.0.12"},
{mochiweb,"MochiWeb is an Erlang library for building lightweight HTTP servers.",
"0.2009.05.26"},
{nitrogen,"Nitrogen web framework for Erlang.","0.2009.05.12.3"},
{nitromce,"A Nitrogen element which corresponds to a TinyMCE editor instance.",
"4.0.1"},
{sgte,"String template language for Erlang.","0.7.1"},
{signzor,"Erlang library to generate signed printable encodings.","0.0.1"},
{tcerl,"Erlang driver for tokyocabinet.","1.3.1h"},
{webmachine,"An Erlang REST framework.","0.2009.09.24b"},
{erlrc,"Extensible application management.","0.2.3"},
{mnesia,"Mnesia storage API extensions.","4.4.7.6.1"},
{crypto,"CRYPTO version 1","1.5.3"},
{sasl,"SASL CXC 138 11","2.1.5.4"},
{stdlib,"ERTS CXC 138 10","1.15.5"},
{kernel,"ERTS CXC 138 10","2.12.5"}]
erlstart-run-erlang is fairly low level, so for an init script I would do something like the following: first, a little stub installed into /etc/rc.d
#! /bin/sh

# chkconfig: 2345 20 80
# description: Control my personal Erlang node.

exec myerlnodectl "$@"

and then the actual guts installed as myerlnodectl
#! /bin/sh

eval_with_main_node () \
{
erl -name myerlnodetmp$$ \
-hidden \
-setcookie "$cookie" \
-noshell -noinput \
-eval "MainNode = list_to_atom (\"$1\"), $2" \
-s erlang halt
}

get_hostname () \
{
erl -name myerlnodetmp$$ -setcookie $$ -noshell -noinput -eval '
[ Host ] = tl (string:tokens (atom_to_list (node ()), "@")),
io:format ("~s~n", [ Host ])
' -s init stop
}

id=`basename "$0"`

if test -d /root
then
HOME=${HOME-/root}
else if test -d /var/root
then
HOME=${HOME-/var/root}
fi
fi
export HOME

ERLSTART_CONFIG_FILE=${ERLSTART_CONFIG_FILE-/usr/share/myerlnode/myerlnode.rc}
export ERLSTART_CONFIG_FILE

. "$ERLSTART_CONFIG_FILE"

cookie=${cookie-turg}
user=${user-erlang}
hostname=${hostname-`get_hostname`}
node_name=`basename "$node_name_file"`
full_name="$node_name@$hostname"
shutdown_file=${shutdown_file-/var/run/myerlnode.shutting_down}

ERL_CRASH_DUMP=${ERL_CRASH_DUMP-/dev/null}
export ERL_CRASH_DUMP

case ${1-"status"} in
start)
test "`id -u`" -eq 0 || exec sudo $0 "$@"

printf "Starting Erlang... "

if test -f "$node_name_file" && \
test true = "`erlstart-eval 'true' 2>/dev/null`" 2>/dev/null
then
echo "already started."
exit 0
fi

pid=`eval_with_main_node "$full_name" '
io:format ("~p", [
case rpc:call (MainNode, os, getpid, []) of
{ badrpc, _ } -> undefined;
Pid -> list_to_integer (Pid)
end ])'`

test "$pid" -gt 0 2>/dev/null && {
test -f "$shutdown_file" && {
oldpid=`cat "$shutdown_file"`
test "$pid" -eq "$oldpid" && {
echo "shutdown in progress (pid = '$pid')." 1>&2
exit 1
}
}
}

rm -f "$shutdown_file"

# check for -s shell support
su -l -s /bin/sh $user -c true >/dev/null 2>/dev/null

if test $? = 0
then
dashs="-s /bin/sh"
else
dashs=""
fi

${niceness+ nice -n $niceness} \
su -l $dashs "$user" -c \
"env cookie=\"$cookie\" erlstart-run-erlang \"$ERLSTART_CONFIG_FILE\""

eval_with_main_node "$full_name" \
"Wait = fun (_, 0) ->
failed;
(Cont, Max) ->
case net_adm:ping (MainNode) of
pong ->
ok;
pang ->
timer:sleep (100),
Cont (Cont, Max - 1)
end
end,
DontTellMe = 100,
ok = Wait (Wait, DontTellMe)" || {
echo "" 1>&2
echo "$id: could not connect to node after 10 seconds" 1>&2
exit 1
}

eval_with_main_node "$full_name" \
"Wait = fun (_, 0) ->
failed;
(Cont, Max) ->
case rpc:call (MainNode, init, get_status, []) of
{ started, _ } ->
ok;
{ starting, _ } ->
timer:sleep (100),
Cont (Cont, Max - 1);
{ Status, _ } ->
{ failed, Status }
end
end,
DontTellMe = 100,
ok = Wait (Wait, DontTellMe)" || {
echo "" 1>&2
echo "$id: node did not boot after 10 seconds" 1>&2
exit 1
}

echo "done."
;;

stop)
test "`id -u`" -eq 0 || exec sudo $0 "$@"

printf "Stopping Erlang... "

if test ! -f "$node_name_file" || \
test true != "`erlstart-eval 'true' 2>/dev/null`" 2>/dev/null
then
echo "not running."
exit 0
fi

pid=`erlstart-eval 'os:getpid ()' 2>/dev/null`

test "$pid" -gt 0 2>/dev/null && {
printf '%s' $pid > "$shutdown_file"
chown $user:$user "$shutdown_file"
}

erlstart-eval 'init:stop ()' >/dev/null 2>/dev/null

eval_with_main_node "$full_name" \
"Wait = fun (_, 0) ->
failed;
(Cont, Max) ->
case net_adm:ping (MainNode) of
pong ->
timer:sleep (100),
Cont (Cont, Max - 1);
pang ->
ok
end
end,
DontTellMe = 100,
ok = Wait (Wait, DontTellMe)" || {
echo "" 1>&2
echo "$id: node still responsive after 100 seconds" 1>&2
exit 1
}

rm -f "$node_name_file"

echo "done."
;;

status)
if test -f "$node_name_file" && \
test true = "`erlstart-eval 'true' 2>/dev/null`" 2>/dev/null
then
echo "Erlang is running"
else
echo "Erlang is not running"
fi

;;

*)
echo "$id: unknown command $1" 1>&2
exit 1
esac

exit 0

Hopefully you found that inspirational.