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.

To send a message from a shell script to your BEAMNotify GenServer, you'll need two environment variables. These can be gotten by calling BEAMNotify.env/1 with the name that you gave it.

  1. $BEAM_NOTIFY - the absolute path to the beam_notify executable
  2. $BEAM_NOTIFY_OPTIONS - how beam_notify should find the appropriate BEAM instance and process the notification

In the shell script, run $BEAM_NOTIFY and pass it any arguments that you want sent up. BEAMNotify reports environment variables too.

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.1.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 just sends a tuple
# with the arguments and environment passed in from the shell script.
iex> BEAMNotify.start_link(name: "sulu", 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.

License

This library is covered by the Apache 2 license.