Skip to content

nerves-networking/beam_notify

Repository files navigation

BEAMNotify

Hex version API docs CircleCI

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.

Overview

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:

  1. $BEAM_NOTIFY - the absolute path to the beam_notify executable
  2. $BEAM_NOTIFY_OPTIONS - how beam_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.

Example

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"], %{...}}

Supervision example

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.

Restricted shell environments

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.

License

This library is covered by the Apache 2 license.