Send a message to the BEAM from a shell script
This is one solution sending notifications from non-BEAM programs into Elixir.
BEAMNotify
lets you set up a GenServer that listens for notifications from
shell scripts or anything that can invoke an OS process. Communication is via a
Unix Domain socket. Messages are limited to strings that passed via commandline
arguments or the environment to the beam_notify
binary.
There are, of course, other ways of solving this problem. Some non-Elixir programs already expose Unix domain or TCP socket interfaces for communication. This might be a better choice. You could also use erl_call or write a C node and communicate over distributed Erlang.
BEAMNotify
would typically be added to a supervision tree in your program.
Options to BEAMNotify
specify things like its name, a dispatch function to
call, and other things.
The shell script (or any program) needs to call the beam_notify
program
supplied by this library. The message is passed via commandline arguments or
environment variables (see :report_env
option).
Since beam_notify
needs to know how to connect to the appropriate
BEAMNotify
GenServer (there may be more than one), the shell script must pass
some options. To make this easy, BEAMNotify
provides two environment
variables by calling BEAMNotify.env/1
:
$BEAM_NOTIFY
- the absolute path to thebeam_notify
executable$BEAM_NOTIFY_OPTIONS
- howbeam_notify
should connect
In the shell script, run $BEAM_NOTIFY
and pass it any arguments that you want
send up. BEAMNotify
reports environment variables too.
If it is not possible to pass the $BEAM_NOTIFY*
environment variables through
to your script due to a restricted shell environment, see the restricted shell
section below.
Back in Elixir, whenever a proper message is received, BEAMNotify
will call
the dispatch function. The dispatch function is responsible for forwarding on
messages however makes sense in your application. If handling is simple, you can
process them in the dispatch function. You could also publish them through
Phoenix.PubSub
or another pubsub service. BEAMNotify
only handles strings,
so if you want to be fancier with your messages or filter them, you'll have to
add that to your dispatcher function.
It is important to keep in mind that the amount of data that can be sent in a notification is limited by the transport and by OS limits on commandline arguments. Suffice it to say that this is not intended for file transfer.
What we're going to do is create a script that sends a message to Elixir.
First, make sure that you have :beam_notify
by either cloning this project or
creating a test Elixir project (mix new ...
) and adding it to the mix.exs
:
def deps do
[
{:beam_notify, "~> 0.2.0"}
]
end
Now open an editor and create simple.sh
with the following contents:
#!/bin/sh
echo "This is simple.sh"
$BEAM_NOTIFY Hello world
Start up Elixir with iex -S mix
:
# Get the PID that's running the IEx console
iex> us = self()
#PID<0.204.0>
# Start a BEAMNotify GenServer. The dispatcher function sends a tuple with the
# arguments and environment passed in from the shell script.
iex> BEAMNotify.start_link(name: "sulu", report_env: true, dispatcher: &send(us, {&1, &2}))
{:ok, #PID<0.211.0>}
# Run the shell script. We're doing this from Elixir, but you
# can also grab the environment by calling `BEAMNotify.env/1` and run it
# in another terminal window.
iex> System.cmd("/bin/sh", ["simple.sh"], env: BEAMNotify.env("sulu"))
{"This is simple.sh\n", 0}
# See what was sent
iex> flush
{["Hello", "world"], %{...}}
Here's a code snippet of starting a hypothetical non-Elixir program that needs
to send messages back to Elixir. This code is part of a module-based
supervisor,
but this isn't necessary. Two GenServers are started: one for BEAMNotify
and
one to start and monitor the non-Elixir program using
MuonTrap.Daemon
.
Note how BEAMNotify.env/1
is used to pass the proper environment to the
program.
@impl Supervisor
def init(_) do
beam_notify_options = [name: "my_beam", dispatcher: &Some.function/2]
children = [
{BEAMNotify, beam_notify_options},
{MuonTrap.Daemon,
[
"/path/to/program",
["-s", "script_calling_beam_notify.sh"],
[log_output: :debug, env: BEAMNotify.env(beam_notify_options)]
]}
]
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
end
If you're lucky, it might be sufficient to call BEAMNotify.bin_path/0
to get
the path to the beam_notify
program and pass that directly to the non-Elixir
program. You'll still need to set the environment for beam_notify
to work. On
the bright side, this will skip out having your system start bash
on each
notification.
Some programs clear the OS environment before running programs as a security precaution. It's still possible send messages to Elixir.
You'll need to know the path to the beam_notify
binary and have a place to put
the communications socket that both Elixir and the beam_notify
binary can
open. In this example, the socket will be created as
/tmp/my_beam_notify_socket
. In Elixir, the BEAMNotify
child_spec might look
like this:
{BEAMNotify, name: "any name", path: "/tmp/my_beam_notify_socket", dispatcher: &Some.function/2}
For the script, here's a sample for Nerves devices where code is installed under
/srv/erlang
.
#!/bin/sh
BEAM_NOTIFY=$(ls /srv/erlang/lib/beam_notify-*/priv/beam_notify)
$BEAM_NOTIFY -p /tmp/my_beam_notify_socket -- hello
The arguments following the --
are passed. The -p /tmp/my_beam_notify_socket
part will be dropped.
Arguments are only parsed (and dropped) if $BEAM_NOTIFY_OPTIONS
isn't defined.
In other words, $BEAM_NOTIFY_OPTIONS
takes precedence.
This library is covered by the Apache 2 license.