defmodule QuickBEAM.NodeChildProcess do @moduledoc false @default_max_buffer 1_048_576 @spec exec_sync(list()) :: map() def exec_sync([command, opts]) when is_binary(command) do opts = if is_map(opts), do: opts, else: %{} cwd = Map.get(opts, "cwd") timeout = Map.get(opts, "timeout") encoding = Map.get(opts, "encoding") max_buffer = Map.get(opts, "maxBuffer", @default_max_buffer) cmd_opts = [:binary, :exit_status, :stderr_to_stdout] cmd_opts = if cwd, do: [{:cd, cwd} | cmd_opts], else: cmd_opts run = fn -> port = Port.open({:spawn, "sh -c " <> shell_escape(command)}, cmd_opts) collect_output(port, <<>>, max_buffer) end {stdout, status} = if timeout do task = Task.async(fn -> run.() end) case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do {:ok, result} -> result nil -> {"", :timeout} end else run.() end if status == :timeout do %{"stdout" => "", "status" => nil, "error" => "ETIMEDOUT"} else stdout = if encoding == "buffer", do: {:bytes, stdout}, else: stdout %{"stdout" => stdout, "status" => status} end end @spec exec_sync([String.t()]) :: map() def exec_sync([command]) when is_binary(command) do exec_sync([command, %{}]) end defp collect_output(port, acc, max_buffer) do receive do {^port, {:data, data}} -> acc = <> acc = if byte_size(acc) > max_buffer, do: binary_part(acc, 0, max_buffer), else: acc collect_output(port, acc, max_buffer) {^port, {:exit_status, status}} -> {acc, status} end end defp shell_escape(command) do "'" <> String.replace(command, "'", "'\\''") <> "'" end end