How to send line by line of System.cmd to LiveView when a task is running

Hi friend,
I run 3 or 4 different command with System.cmd, so users wait for these tasks are done and after getting status 0 can do another jobs, but I need a way to show them System.cmd log line by line when is running not show all the log at the end.

System.cmd(operation, [command], into: IO.stream(), stderr_to_stdout: true, env: [{"MIX_ENV", "#{Mix.env()}"}])

And if this feature is available at any time, it will be very attractive. For example, from the middle of the log, if requested, show not the whole log or sending any line to PubSub to the channel.

Thank you

You might be able to do that with Port.open and then receive messages coming from it and send them to the liveview process.

port.exs

defmodule Command do
  def exec(cmd, args) do
    path = System.find_executable(cmd)
    port = Port.open({:spawn_executable, path}, [:binary, :exit_status, args: args, line: 1000])
    loop(port)
  end

  defp loop(port) do
    receive do
      {^port, {:data, data}} ->
        IO.inspect(data: data)
        loop(port)

      {^port, {:exit_status, exit_status}} ->
        IO.inspect(exit_status: exit_status)
    end
  end
end

Command.exec("cat", ["port.exs"])
> elixir port.exs
[data: {:eol, "defmodule Command do"}]
[data: {:eol, "  def exec(cmd, args) do"}]
[data: {:eol, "    path = System.find_executable(cmd)"}]
[
  data: {:eol,
   "    port = Port.open({:spawn_executable, path}, [:binary, :exit_status, args: args, line: 1000])"}
]
[data: {:eol, "    loop(port)"}]
[data: {:eol, "  end"}]
[data: {:eol, ""}]
[data: {:eol, "  defp loop(port) do"}]
[data: {:eol, "    receive do"}]
[data: {:eol, "      {^port, {:data, data}} ->"}]
[data: {:eol, "        IO.inspect(data: data)"}]
[data: {:eol, "        loop(port)"}]
[data: {:eol, ""}]
[data: {:eol, "      {^port, {:exit_status, exit_status}} ->"}]
[data: {:eol, "        IO.inspect(exit_status: exit_status)"}]
[data: {:eol, "    end"}]
[data: {:eol, "  end"}]
[data: {:eol, "end"}]
[data: {:eol, ""}]
[data: {:eol, "Command.exec(\"cat\", [\"port.exs\"])"}]
[exit_status: 0]
4 Likes

I’d recommend looking into exexec | Hex (which wraps the erlexec Erlang library) especially if the command you want to run are not written to conform to the behavior that Port expects.

1 Like

Hi dear @axelson, it has no document with sample code?

There’re some on OS Process Manager for Erlang VM

2 Likes

I have a question, where we can put env like env: [{"MIX_ENV", "#{Mix.env()}"}]?

path = System.find_executable("mix")
port = Port.open({:spawn_executable, path}, [:binary, :exit_status, args: ["deps.get"], line: 1000])
flush()
Port.open({:spawn_executable, path}, env: [{'MIX_ENV', 'dev'}])

Note that Mix.env() is not always available at runtime.

1 Like

I have this error:

Port.open({:spawn_executable, path}, env: [{"MIX_ENV", "dev"}])
** (ArgumentError) errors were found at the given arguments:

  * 2nd argument: invalid option in list

    :erlang.open_port({:spawn_executable, "/Users/shahryar/.asdf/installs/elixir/1.13.3-otp-24/bin/mix"}, [env: [{"MIX_ENV", "dev"}]])

You put it without [:binary, :exit_status, args: ["deps.get"], line: 1000]

I even try like this:

port = Port.open({:spawn_executable, path},[:binary, :exit_status, args: ["deps.get"], env: [{"MIX_ENV", "dev"}], line: 1000])

but I have this error:

** (ArgumentError) errors were found at the given arguments:

  * 2nd argument: invalid option in list

    :erlang.open_port({:spawn_executable, "/Users/shahryar/.asdf/installs/elixir/1.13.3-otp-24/bin/mix"}, [:binary, :exit_status, {:args, ["deps.get"]}, {:env, [{"MIX_ENV", "dev"}]}, {:line, 1000}])

Try using charlists instead of binaries:

Port.open({:spawn_executable, path}, env: [{'MIX_ENV', 'dev'}])
1 Like

Thank you for all your efforts, it works, and I need to make it comfortable with my code and idea in my project; but I think I got my needed answer, so I need to do more research and work with it.