akash-akya / exile

Alternative to ports for running external programs. It provides back-pressure, non-blocking io, and solves port related issues
Apache License 2.0
142 stars 12 forks source link

receiving messages directly async #42

Closed dch closed 5 months ago

dch commented 6 months ago

Firstly this is a lovely crafted & documented library. thanks!

I want to receive each response from Exile directly into a calling GenServer, as messages. As a simple example, ping(8) and dbg/1 are used.

Is there a simpler way to do this, perhaps using Exile.Process directly somehow?

iex> Exile.stream(~w"ping -c 3 localhost", stderr: :consume)
|> Stream.transform(nil, fn(a,b) ->  dbg({a,b}); {[], nil} end)
|> Stream.run()

[iex:36: (file)]
{a, b} #=> {{:stdout,
  "PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.165 ms\n"},
 nil}

[iex:36: (file)]
{a, b} #=> {{:stdout, "64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.126 ms\n"}, nil}

[iex:36: (file)]
{a, b} #=> {{:stdout,
  "64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.128 ms\n\n--- localhost ping statistics ---\n3 packets transmitted, 3 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.126/0.140/0.165/0.018 ms\n"},
 nil}

[iex:36: (file)]
{a, b} #=> {{:exit, {:status, 0}}, nil}

:ok
iex> pid = self()
iex> Exile.stream(~w"ping -c 3 localhost", stderr: :consume)
  |> Stream.transform(nil,
    fn(a,b) ->  Process.send(pid, {:stream, a,b}, [:noconnect])
    {[], nil} end)
  |> Stream.run()
...
akash-akya commented 6 months ago

Hi @dch, Are you are thinking of an interface similar to the one provided by Port? Currently there is no way to read data without caller explicitly calling read functions, to make it demand-driven.

You can simulate port like behavior by spawning a separate process, but I am not sure if that answers your question.

    parent = self()

    spawn_link(fn ->
      Exile.stream(~w"ping -c 3 localhost", stderr: :consume)
      |> Enum.each(&Process.send(parent, &1, [:noconnect]))
    end)

You can also use Exile.Process directly for finer control over reading.

defmodule Test do
  def run do
    parent = self()

    spawn_link(fn ->
      {:ok, proc} = Exile.Process.start_link(~w"ping -c 3 localhost", stderr: :consume)
      loop(parent, proc)
    end)
  end

  def loop(parent, proc) do
    case Exile.Process.read_any(proc) do
      {:ok, data} ->
        send(parent, data)
        loop(parent, proc)

      :eof ->
        :ok

      error ->
        raise error
    end
  end
end
dch commented 5 months ago

thanks for the examples. This is better than what I've come up with so far thanks.