Packages
Create a GenServer in any language. Exos contains a very simple GenServer which proxy calls and casts to a given port command, encoding and decoding the message to the port using erlang external binary term format. (see related projects : clojure|python|node_erlastic on https://github.com/kbrw)
Current section
Files
Jump to
Current section
Files
lib/exos.ex
defmodule Exos.Proc do
use GenServer
alias :erlang, as: Erl
@doc """
Launch a GenServer which starts a port and proxify cast and call to
it using a port protocol with `packet: 4`, (32bits-length+data)
messages are transmitted throught stdin/out. Input terms are
encoded using `binary_to_term` and received terms are decoded using
`term_to_binary`.
- `cmd` is the shell command to launch the port
- when the port starts, it automatically receives as first message the `init`
term if `init !== :no_init`
- `port_opts` are options for `Port.open` (for instance `[cd: "/path/"]`)
- `gen_server_opts` are options for `GenServer.start_link` (for instance `[name: :servername]`)
- messages received from the port outside of a `GenServer.call`
context trigger a `event_fun.(event)` call if `event_fun` is not `nil` (default)
- `etf_opts` are options for `:erlang.term_to_binary` and `:erlang.binary_to_term`
- to allow easy supervision, if the port die with a return code == 0, then
the GenServer die with the reason `:normal`, else with the reason `:port_terminated`
"""
@deprecated "Use #{__MODULE__}.start_link/3 instead"
def start_link(cmd, init, port_opts, gen_server_opts, event_fun) do
start_link(cmd, init, [
port_opts: port_opts,
gen_server_opts: gen_server_opts,
event_fun: event_fun,
])
end
@deprecated "Use #{__MODULE__}.start_link/3 instead"
def start_link(cmd, init, port_opts, gen_server_opts) do
start_link(cmd, init, [
port_opts: port_opts,
gen_server_opts: gen_server_opts,
])
end
def start_link(cmd, init, opts \\ []) do
known_opts = [:port_opts, :gen_server_opts, :event_fun, :etf_opts]
case Keyword.keyword?(opts) and match?({:ok, _}, Keyword.validate(opts, known_opts)) do
true ->
port_opts = Keyword.get(opts, :port_opts, [])
gen_server_opts = Keyword.get(opts, :gen_server_opts, [])
event_fun = Keyword.get(opts, :event_fun)
etf_opts = Keyword.get(opts, :etf_opts, [])
GenServer.start_link(Exos.Proc, {cmd, init, port_opts, event_fun, etf_opts}, gen_server_opts)
false ->
# If no key matches with our opts, then it means it is the old API being used.
IO.warn("You are using the old API of #{__MODULE__}.start_link/3 that takes Port.open/2
options as 3rd parameter. The new API takes a Keyword list instead to handle options of
the old API.")
start_link(cmd, init, [port_opts: opts])
end
end
def init({cmd,initarg,opts,etf_opts}), do: init({cmd,initarg,opts,nil,etf_opts})
def init({cmd,initarg,opts,event_fun,etf_opts}) do
port = Port.open({:spawn,~c'#{cmd}'}, [:binary,:exit_status, packet: 4] ++ opts)
if initarg !== :no_init, do:
send(port,{self(),{:command,Erl.term_to_binary(initarg,etf_opts)}})
{:ok, {port, event_fun, etf_opts}}
end
def handle_info({port,{:exit_status,0}},{port,_,_}=state), do: {:stop,:normal,state}
def handle_info({port,{:exit_status,_}},{port,_,_}=state), do: {:stop,:port_terminated,state}
def handle_info({port,{:data,b}},{port,event_fun,_}=state) do
if event_fun do event_fun.(Erl.binary_to_term(b)) end
{:noreply,state}
end
def handle_cast(term,{port,_,etf_opts}=state) do
send(port,{self(),{:command,Erl.term_to_binary(term,etf_opts)}})
{:noreply,state}
end
def handle_call(term,_reply_to,{port,_,etf_opts}=state) do
send(port,{self(),{:command,Erl.term_to_binary(term,etf_opts)}})
res = receive do
{^port,{:data,b}}->Erl.binary_to_term(b)
{^port,{:exit_status,_}}=exit_msg->send(self(),exit_msg);{:error,:port_terminated} # catch exit msg and resend it
end
{:reply,res,state}
end
end