Page MenuHomePhorge

No OneTemporary

Size
11 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/exile/process.ex b/lib/exile/process.ex
index 2e828d6..d8355ea 100644
--- a/lib/exile/process.ex
+++ b/lib/exile/process.ex
@@ -1,312 +1,317 @@
defmodule Exile.Process do
alias Exile.ProcessHelper
require Logger
use GenServer
# delay between retries when io is busy (in milliseconds)
@default_opts %{io_busy_wait: 1, stderr_to_console: false}
def start_link(cmd, args, opts \\ %{}) do
opts = Map.merge(@default_opts, opts)
GenServer.start(__MODULE__, %{cmd: cmd, args: args, opts: opts})
end
def close_stdin(process) do
GenServer.call(process, :close_stdin, :infinity)
end
def write(process, binary) do
GenServer.call(process, {:write, binary}, :infinity)
end
def read(process, bytes) do
GenServer.call(process, {:read, bytes}, :infinity)
end
def os_pid(process) do
GenServer.call(process, :os_pid, :infinity)
end
def kill(process, signal) when signal in [:sigkill, :sigterm] do
GenServer.call(process, {:kill, signal}, :infinity)
end
def await_exit(process, timeout \\ :infinity) do
GenServer.call(process, {:await_exit, timeout}, :infinity)
end
- def stop(process) do
- if exited?(process) do
- GenServer.stop(process, :normal, :infinity)
- else
- GenServer.stop(process, :kill, :infinity)
- end
- end
-
- def exited?(process) do
- GenServer.call(process, :exited?, :infinity)
- end
+ def stop(process), do: GenServer.call(process, :stop, :infinity)
## Server
def init(%{cmd: cmd, args: args, opts: opts}) do
path = :os.find_executable(to_charlist(cmd))
unless path do
raise "Command not found: #{cmd}"
end
state = %{
cmd: path,
args: args,
opts: opts,
read_acc: [],
errno: nil,
status: :init,
await: %{}
}
{:ok, state, {:continue, nil}}
end
def handle_continue(nil, state) do
exec_args = Enum.map(state.args, &to_charlist/1)
stderr_to_console = if state.opts.stderr_to_console, do: 1, else: 0
case ProcessHelper.exec_proc([state.cmd | exec_args], stderr_to_console) do
{:ok, {pid, stdin, stdout}} ->
start_watcher(pid, stdin, stdout)
state = Map.merge(state, %{pid: pid, stdin: stdin, stdout: stdout, status: :start})
{:noreply, state}
{:error, errno} ->
raise "Failed to start command: #{state.cmd}, errno: #{errno}"
end
end
- def handle_call(:exited?, _from, state), do: {:reply, !ProcessHelper.is_alive(state.pid), state}
+ def handle_call(:stop, _from, state) do
+ do_close(state, :stdin)
+ do_close(state, :stdout)
+
+ if ProcessHelper.is_alive(state.pid) do
+ do_kill(state.pid, :sigkill)
+ {:stop, :process_killed, :ok, %{state | status: {:exit, :killed}}}
+ else
+ {:stop, :normal, :ok, state}
+ end
+ end
def handle_call(:os_pid, _from, state), do: {:reply, state.pid, state}
def handle_call(_, _from, %{status: {:exit, status}}), do: {:reply, {:error, {:exit, status}}}
def handle_call({:await_exit, timeout}, from, state) do
tref =
if timeout != :infinity do
Process.send_after(self(), {:await_exit_timeout, from}, timeout)
else
nil
end
state = put_timer(state, from, :timeout, tref)
check_exit(state, from)
end
def handle_call({:write, _binary}, _from, %{stdin: :closed} = state),
do: {:reply, {:error, :closed}, state}
def handle_call({:write, binary}, from, state), do: do_write(state, binary, from)
def handle_call({:read, bytes}, from, state), do: do_read(state, bytes, from)
- def handle_call(:close_stdin, _from, %{stdin: :closed} = state), do: {:reply, :closed, state}
-
- def handle_call(:close_stdin, _from, state) do
- case ProcessHelper.close_pipe(state.stdin) do
- :ok -> {:reply, :ok, %{state | stdin: :closed}}
- {:error, errno} -> {:reply, {:error, errno}, %{state | errno: errno}}
- end
- end
+ def handle_call(:close_stdin, _from, state), do: do_close(state, :stdin)
def handle_call({:kill, signal}, _from, state) do
do_kill(state.pid, signal)
{:reply, :ok, %{state | status: {:exit, :killed}}}
end
def handle_info({:read, bytes, from}, state), do: do_read(state, bytes, from)
def handle_info({:write, binary, from}, state), do: do_write(state, binary, from)
def handle_info({:check_exit, from}, state), do: check_exit(state, from)
def handle_info({:await_exit_timeout, from}, state) do
cancel_timer(state, from, :check)
receive do
{:check_exit, ^from} -> :ok
after
0 -> :ok
end
GenServer.reply(from, :timeout)
{:noreply, clear_await(state, from)}
end
def handle_info(msg, _state), do: raise(msg)
defp do_write(state, binary, from) do
case ProcessHelper.write_proc(state.stdin, binary) do
{:ok, bytes} ->
if bytes < IO.iodata_length(binary) do
binary = IO.iodata_to_binary(binary)
binary = binary_part(binary, bytes, IO.iodata_length(binary) - bytes)
Process.send_after(self(), {:write, binary, from}, state.opts.io_busy_wait)
else
GenServer.reply(from, :ok)
end
{:noreply, state}
# EAGAIN
{:error, 35} ->
Process.send_after(self(), {:write, binary, from}, state.opts.io_busy_wait)
{:noreply, state}
{:error, errno} ->
GenServer.reply(from, {:error, errno})
{:noreply, %{state | errno: errno}}
end
end
defp do_read(state, nil, from) do
case ProcessHelper.read_proc(state.stdout, 65535) do
{:ok, <<>>} ->
GenServer.reply(from, {:eof, []})
{:noreply, state}
{:ok, binary} ->
GenServer.reply(from, {:ok, binary})
{:noreply, state}
# EAGAIN
{:error, 35} ->
Process.send_after(self(), {:read, nil, from}, state.opts.io_busy_wait)
{:noreply, state}
{:error, errno} ->
GenServer.reply(from, {:error, errno})
{:noreply, %{state | errno: errno}}
end
end
defp do_read(state, bytes, from) do
case ProcessHelper.read_proc(state.stdout, bytes) do
{:ok, <<>>} ->
GenServer.reply(from, {:eof, state.read_acc})
{:noreply, %{state | read_acc: []}}
{:ok, binary} ->
if IO.iodata_length(binary) < bytes do
Process.send_after(
self(),
{:read, bytes - IO.iodata_length(binary), from},
state.opts.io_busy_wait
)
{:noreply, %{state | read_acc: [state.read_acc | binary]}}
else
GenServer.reply(from, {:ok, [state.read_acc | binary]})
{:noreply, %{state | read_acc: []}}
end
# EAGAIN
{:error, 35} ->
Process.send_after(self(), {:read, bytes, from}, state.opts.io_busy_wait)
{:noreply, state}
{:error, errno} ->
GenServer.reply(from, {:error, errno})
{:noreply, %{state | errno: errno}}
end
end
defp check_exit(%{pid: pid} = state, from) do
case ProcessHelper.wait_proc(pid) do
{^pid, status} ->
GenServer.reply(from, {:ok, status})
cancel_timer(state, from, :timeout)
{:noreply, clear_await(state, from)}
{0, _} ->
tref = Process.send_after(self(), {:check_exit, from}, state.opts.io_busy_wait)
{:noreply, put_timer(state, from, :check, tref)}
{-1, status} ->
GenServer.reply(from, {:error, status})
cancel_timer(state, from, :timeout)
{:noreply, clear_await(state, from)}
end
end
defp do_kill(pid, :sigkill), do: ProcessHelper.kill_proc(pid)
defp do_kill(pid, :sigterm), do: ProcessHelper.terminate_proc(pid)
+ defp do_close(state, type) do
+ case state[type] do
+ :closed ->
+ {:reply, :ok, %{state | type => :closed}}
+
+ pipe ->
+ case ProcessHelper.close_pipe(pipe) do
+ :ok -> {:reply, :ok, %{state | type => :closed}}
+ {:error, errno} -> {:reply, {:error, errno}, %{state | errno: errno}}
+ end
+ end
+ end
+
defp clear_await(state, from) do
%{state | await: Map.delete(state.await, from)}
end
defp cancel_timer(state, from, key) do
case get_timer(state, from, key) do
nil -> :ok
tref -> Process.cancel_timer(tref)
end
end
defp put_timer(state, from, key, timer) do
if Map.has_key?(state.await, from) do
put_in(state, [:await, from, key], timer)
else
put_in(state, [:await], %{from => %{key => timer}})
end
end
defp get_timer(state, from, key), do: get_in(state, [:await, from, key])
@stdin_close_wait 3000
@sigterm_wait 1000
# Try to gracefully terminate external proccess if the genserver associated with the process is killed
defp start_watcher(pid, stdin, stdout) do
- parent = self()
-
- watcher_pid =
- spawn(fn ->
- ref = Process.monitor(parent)
- send(parent, {self(), :done})
-
- # TODO: should check if process is alreayd exit
- receive do
- {:DOWN, ^ref, :process, ^parent, :normal} ->
- :ok
-
- {:DOWN, ^ref, :process, ^parent, _reason} ->
- case ProcessHelper.wait_proc(pid) do
- {^pid, _status} ->
- # TODO: check stauts
- nil
-
- _ ->
- Logger.debug(fn -> "Killing #{pid}" end)
-
- with _ <- ProcessHelper.close_pipe(stdin),
- _ <- ProcessHelper.close_pipe(stdout),
- _ <- :timer.sleep(@stdin_close_wait),
- {p, _} <- ProcessHelper.wait_proc(pid),
- false <- p != pid,
- _ <- ProcessHelper.terminate_proc(pid),
- _ <- :timer.sleep(@sigterm_wait),
- {p, _} <- ProcessHelper.wait_proc(pid),
- false <- p != pid,
- _ <- ProcessHelper.kill_proc(pid) do
- Logger.debug(fn -> "Killed process: #{pid}" end)
- end
- end
- end
- end)
+ process_server = self()
+ watcher_pid = spawn(fn -> watcher(process_server, pid, stdin, stdout) end)
receive do
{^watcher_pid, :done} -> :ok
end
end
+
+ defp watcher(process_server, pid, stdin, stdout) do
+ ref = Process.monitor(process_server)
+ send(process_server, {self(), :done})
+
+ receive do
+ {:DOWN, ^ref, :process, ^process_server, :normal} ->
+ :ok
+
+ {:DOWN, ^ref, :process, ^process_server, _reason} ->
+ case ProcessHelper.wait_proc(pid) do
+ {^pid, _status} ->
+ # TODO: check stauts
+ nil
+
+ _ ->
+ Logger.debug(fn -> "Killing #{pid}" end)
+
+ with _ <- ProcessHelper.close_pipe(stdin),
+ _ <- ProcessHelper.close_pipe(stdout),
+ _ <- :timer.sleep(@stdin_close_wait),
+ {p, _} <- ProcessHelper.wait_proc(pid),
+ false <- p != pid,
+ _ <- ProcessHelper.terminate_proc(pid),
+ _ <- :timer.sleep(@sigterm_wait),
+ {p, _} <- ProcessHelper.wait_proc(pid),
+ false <- p != pid,
+ _ <- ProcessHelper.kill_proc(pid) do
+ Logger.debug(fn -> "Killed process: #{pid}" end)
+ end
+ end
+ end
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 19, 10:28 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769307
Default Alt Text
(11 KB)

Event Timeline