Packages

Porcelain implements a saner approach to launching and communicating with external OS processes from Elixir. Built on top of Erlang's ports, it provides richer functionality and simpler API.

Current section

Files

Jump to
porcelain lib porcelain drivers goon_driver.ex
Raw

lib/porcelain/drivers/goon_driver.ex

defmodule Porcelain.Driver.Goon do
@moduledoc """
Porcelain driver that offers additional features over the basic one.
Users are not supposed to call functions in this module directly. Use
functions in `Porcelain` instead.
This driver will be used by default if it can locate the external program
named `goon` in the executable path. If `goon` is not found, Porcelain will
fall back to the basic driver.
The additional functionality provided by this driver is as follows:
* ability to signal EOF to the external program
* send an OS signal to the program
* (to be implemented) more efficient piping of multiple programs
"""
alias Porcelain.Driver.Common
alias Common.StreamServer
@behaviour Common
@doc false
def exec(prog, args, opts) do
do_exec(prog, args, opts, :noshell)
end
@doc false
def exec_shell(prog, opts) do
do_exec(prog, nil, opts, :shell)
end
@doc false
def spawn(prog, args, opts) do
do_spawn(prog, args, opts, :noshell)
end
@doc false
def spawn_shell(prog, opts) do
do_spawn(prog, nil, opts, :shell)
end
###
defp do_exec(prog, args, opts, shell_flag) do
opts = Common.compile_options(opts)
exe = find_executable(prog, opts, shell_flag)
port = Port.open(exe, port_options(shell_flag, prog, args, opts))
Common.communicate(port, opts[:in], opts[:out], opts[:err], {&process_data/3, &feed_input/2, &send_signal/2},
async_input: opts[:async_in])
end
defp do_spawn(prog, args, opts, shell_flag) do
opts = Common.compile_options(opts)
exe = find_executable(prog, opts, shell_flag)
out_opt = opts[:out]
out_ret = case out_opt do
:stream ->
{:ok, server} = StreamServer.start()
out_opt = {:stream, server}
Stream.unfold(server, &Common.read_stream/1)
{atom, ""} when atom in [:string, :iodata] ->
atom
_ -> out_opt
end
pid = spawn(fn ->
port = Port.open(exe, port_options(shell_flag, prog, args, opts))
Common.communicate(port, opts[:in], out_opt, opts[:err], {&process_data/3, &feed_input/2, &send_signal/2},
async_input: true, result: opts[:result])
end)
%Porcelain.Process{
pid: pid,
out: out_ret,
err: opts[:err],
}
end
@proto_version "2.0"
@doc false
defp find_executable(prog, _, :noshell) do
if Common.find_executable(prog) do
{:spawn_executable, goon_exe()}
else
throw "Command not found: #{prog}"
end
end
defp find_executable(_, _, :shell) do
{:spawn_executable, goon_exe()}
end
defp goon_exe() do
{:ok, goon} = :application.get_env(:porcelain, :driver_state)
goon
end
defp port_options(:noshell, prog, args, opts) do
args = List.flatten([goon_options(opts), "--", Common.find_executable(prog), args])
[{:args, args} | common_port_options(opts)]
end
defp port_options(:shell, prog, _, opts) do
{sh, sh_args} = Common.shell_command(prog)
args = List.flatten([goon_options(opts), "--", List.to_string(sh), sh_args])
[{:args, args} | common_port_options(opts)]
end
defp goon_options(opts) do
ret = []
if opts[:in], do: ret = ["-in"|ret]
if opts[:out], do: ret = ["-out"|ret]
case opts[:err] do
:out -> ret = ["-err", "out"|ret]
nil -> nil
_ -> ret = ["-err", "err"|ret]
end
if dir=opts[:dir], do: ret = ["-dir", dir|ret]
case :application.get_env(:porcelain, :goon_driver_log) do
:undefined -> nil
{:ok, val} -> ret = ["-log", val|ret]
end
["-proto", @proto_version|ret]
end
defp common_port_options(opts) do
[{:packet,2}|Common.port_options(opts)]
end
###
defp process_data(<<0x0>> <> data, output, error) do
{Common.process_port_output(output, data, :out), error}
end
defp process_data(<<0x1>> <> data, output, error) do
{output, Common.process_port_output(error, data, :err)}
end
# Maximum chunk size to fit in a single packet. One byte is used as a marker
# in the Goex protocol v2.0.
@input_chunk_size 65535-1
# EOF from user code
defp feed_input(port, "") do
send_eof(port)
end
defp feed_input(port, iodata) do
do_feed_input(port, iodata)
end
# EOF internal EOF marker
defp do_feed_input(port, :eof) do
send_eof(port)
end
defp do_feed_input(port, data) when is_binary(data) do
size = byte_size(data)
feed_in_chunks(port, data, @input_chunk_size, 0, size)
end
defp do_feed_input(port, iolist) when is_list(iolist) do
for_each(iolist, &do_feed_input(port, &1))
end
defp do_feed_input(port, byte) when is_integer(byte) do
port_command(port, [byte])
end
defp feed_in_chunks(_port, _data, _chunk_size, data_size, data_size), do: nil
defp feed_in_chunks(port, data, chunk_size, start, data_size) do
size = min(chunk_size, data_size-start)
chunk = :binary.part(data, start, size)
port_command(port, chunk)
feed_in_chunks(port, data, chunk_size, start+size, data_size)
end
defp for_each([], _fun), do: :ok
defp for_each([h|t], fun) do
fun.(h)
for_each(t, fun)
end
defp port_command(port, data) do
Port.command(port, [0,data])
#:timer.sleep(1)
end
defp send_eof(port) do
Port.command(port, [])
end
defp send_signal(port, :int) do
Port.command(port, [1,128])
end
defp send_signal(port, :kill) do
Port.command(port, [1,129])
end
defp send_signal(port, sig) when is_integer(sig) do
Port.command(port, [1,sig])
end
###
@doc false
def check_goon_version(path) do
ackstr = for << <<byte>> <- :crypto.rand_bytes(8) >>,
byte != 0, into: "", do: <<byte>>
args = ["-proto", @proto_version, "-ack", ackstr]
opts = {[out: {:string, ""}], []}
result = %Porcelain.Result{} = Porcelain.Driver.Basic.exec(path, args, opts)
result.status == 0 and result.out == ackstr
end
@doc false
def proto_version, do: @proto_version
end