mbu/lib/apprunner.exs

86 lines
2 KiB
Elixir

defmodule MBU.AppRunner do
@moduledoc """
AppRunner is a script for ensuring that external apps are terminated
properly when executed from BEAM.
If you start an application that does not listen to STDIN from BEAM
and BEAM is terminated, the application's STDIN is closed but it may
never close. AppRunner is meant to sit in between, monitoring the
STDIN pipe and killing the application if STDIN closes. This ensures
no zombie processes are left behind if the host BEAM dies.
Output from the application will be sent back to the host BEAM. If the
application dies for any reason, AppRunner is also closed.
"""
def wait_for_eof() do
case IO.getn("", 1024) do
:eof -> nil
_ -> wait_for_eof()
end
end
def exec(program, args) do
Port.open(
{:spawn_executable, program},
[
:exit_status,
:stderr_to_stdout, # Redirect stderr to stdout to log properly
args: args,
line: 1024
]
)
end
def get_pid(port) do
case Port.info(port) do
nil ->
nil
info when is_list(info) ->
case Keyword.get(info, :os_pid) do
nil -> nil
pid -> Integer.to_string(pid)
end
end
end
def kill(pid) do
System.find_executable("kill") |> System.cmd([pid])
end
def wait_loop(port) do
receive do
{_, {:data, {:eol, msg}}} ->
msg |> :unicode.characters_to_binary(:unicode) |> IO.puts()
{_, {:data, {:noeol, msg}}} ->
msg |> :unicode.characters_to_binary(:unicode) |> IO.write()
{_, :eof_received} ->
get_pid(port) |> kill()
:erlang.halt(0)
{_, :closed} ->
:erlang.halt(0)
{_, {:exit_status, status}} ->
:erlang.halt(status)
{:EXIT, _, _} ->
:erlang.halt(1)
end
wait_loop(port)
end
end
[program | args] = System.argv()
port = MBU.AppRunner.exec(program, args)
Task.async(fn ->
MBU.AppRunner.wait_for_eof()
:eof_received
end)
MBU.AppRunner.wait_loop(port)