Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85712322
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
11 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R14 exile
Attached
Detach File
Event Timeline
Log In to Comment