diff --git a/c_src/spawner.c b/c_src/spawner.c index 6bb8e3e..df75486 100644 --- a/c_src/spawner.c +++ b/c_src/spawner.c @@ -146,9 +146,19 @@ static int exec_process(char const *bin, char *const *args, int socket, _exit(FORK_EXEC_FAILURE); } - if (strcmp(stderr_str, "consume") == 0) { + if (strcmp(stderr_str, "redirect_to_stdout") == 0) { close(STDERR_FILENO); close(r_cmderr); + close(w_cmderr); + + if (dup2(w_cmdout, STDERR_FILENO) < 0) { + perror("[spawner] failed to redirect stderr to stdout"); + _exit(FORK_EXEC_FAILURE); + } + } else if (strcmp(stderr_str, "consume") == 0) { + close(STDERR_FILENO); + close(r_cmderr); + if (dup2(w_cmderr, STDERR_FILENO) < 0) { perror("[spawner] failed to dup to stderr"); _exit(FORK_EXEC_FAILURE); diff --git a/lib/exile.ex b/lib/exile.ex index 83422d5..2354dd6 100644 --- a/lib/exile.ex +++ b/lib/exile.ex @@ -197,8 +197,9 @@ defmodule Exile do * `stderr` - different ways to handle stderr stream. possible values `:console`, `:disable`, `:stream`. 1. `:console` - stderr output is redirected to console (Default) - 2. `:disable` - stderr output is redirected `/dev/null` suppressing all output - 3. `:consume` - connects stderr for the consumption. The output stream will contain stderr + 2. `:redirect_to_stdout` - stderr output is redirected to stdout + 3. `:disable` - stderr output is redirected `/dev/null` suppressing all output + 4. `:consume` - connects stderr for the consumption. The output stream will contain stderr data along with stdout. Stream data will be either `{:stdout, iodata}` or `{:stderr, iodata}` to differentiate different streams. See example below. diff --git a/lib/exile/process.ex b/lib/exile/process.ex index 80ebae2..ba8b3b2 100644 --- a/lib/exile/process.ex +++ b/lib/exile/process.ex @@ -303,8 +303,9 @@ defmodule Exile.Process do * `stderr` - different ways to handle stderr stream. possible values `:console`, `:disable`, `:stream`. 1. `:console` - stderr output is redirected to console (Default) - 2. `:disable` - stderr output is redirected `/dev/null` suppressing all output - 3. `:consume` - connects stderr for the consumption. When set to stream the output must be consumed to + 2. `:redirect_to_stdout` - stderr output is redirected to stdout + 3. `:disable` - stderr output is redirected `/dev/null` suppressing all output + 4. `:consume` - connects stderr for the consumption. When set to stream the output must be consumed to avoid external program from blocking. Caller of the process will be the owner owner of the Exile Process. diff --git a/lib/exile/process/exec.ex b/lib/exile/process/exec.ex index 4e5dc4b..b783c43 100644 --- a/lib/exile/process/exec.ex +++ b/lib/exile/process/exec.ex @@ -52,7 +52,7 @@ defmodule Exile.Process.Exec do cmd_with_args: nonempty_list(), cd: charlist, env: env, - stderr: :console | :disable | :consume + stderr: :console | :redirect_to_stdout | :disable | :consume }} | {:error, String.t()} def normalize_exec_args(cmd_with_args, opts) do @@ -192,18 +192,19 @@ defmodule Exile.Process.Exec do end end - @spec normalize_stderr(stderr :: :console | :disable | :consume | nil) :: - {:ok, :console | :disable | :consume} | {:error, String.t()} + @spec normalize_stderr(stderr :: :console | :redirect_to_stdout | :disable | :consume | nil) :: + {:ok, :console | :redirect_to_stdout | :disable | :consume} | {:error, String.t()} defp normalize_stderr(stderr) do case stderr do nil -> {:ok, :console} - stderr when stderr in [:console, :disable, :consume] -> + stderr when stderr in [:redirect_to_stdout, :console, :disable, :consume] -> {:ok, stderr} _ -> - {:error, ":stderr must be an atom and one of :console, :disable, :consume"} + {:error, + ":stderr must be an atom and one of :redirect_to_stdout, :console, :disable, :consume"} end end diff --git a/lib/exile/process/state.ex b/lib/exile/process/state.ex index 60f637f..c8b1f5c 100644 --- a/lib/exile/process/state.ex +++ b/lib/exile/process/state.ex @@ -9,7 +9,7 @@ defmodule Exile.Process.State do @type read_mode :: :stdout | :stderr | :stdout_or_stderr - @type stderr_mode :: :console | :disable | :consume + @type stderr_mode :: :console | :redirect_to_stdout | :disable | :consume @type pipes :: %{ stdin: Pipe.t(), diff --git a/lib/exile/stream.ex b/lib/exile/stream.ex index 0a4d23e..0425231 100644 --- a/lib/exile/stream.ex +++ b/lib/exile/stream.ex @@ -297,11 +297,12 @@ defmodule Exile.Stream do nil -> {:ok, :console} - stderr when stderr in [:console, :disable, :consume] -> + stderr when stderr in [:console, :redirect_to_stdout, :disable, :consume] -> {:ok, stderr} _ -> - {:error, ":stderr must be an atom and one of :console, :disable, :consume"} + {:error, + ":stderr must be an atom and one of :console, :redirect_to_stdout, :disable, :consume"} end end diff --git a/test/exile_test.exs b/test/exile_test.exs index 5f8e9ee..b4b6ea8 100644 --- a/test/exile_test.exs +++ b/test/exile_test.exs @@ -46,6 +46,53 @@ defmodule ExileTest do assert IO.iodata_to_binary(stderr) == "Hello World\n" end + test "stderr redirect_to_stdout" do + merged_output = + Exile.stream!( + [fixture("write_stderr.sh"), "Hello World"], + stderr: :redirect_to_stdout + ) + |> Enum.to_list() + |> IO.iodata_to_binary() + + assert merged_output == "Hello World\n" + end + + test "order must be preserved when stderr is redirect to stdout" do + merged_output = + Exile.stream!( + ["sh", "-c", "for s in $(seq 1 10); do echo stdout $s; echo stderr $s >&2; done"], + stderr: :redirect_to_stdout, + ignore_epipe: true + ) + |> Enum.to_list() + |> IO.iodata_to_binary() + |> String.trim() + + assert [ + "stdout 1", + "stderr 1", + "stdout 2", + "stderr 2", + "stdout 3", + "stderr 3", + "stdout 4", + "stderr 4", + "stdout 5", + "stderr 5", + "stdout 6", + "stderr 6", + "stdout 7", + "stderr 7", + "stdout 8", + "stderr 8", + "stdout 9", + "stderr 9", + "stdout 10", + "stderr 10" + ] == String.split(merged_output, "\n") + end + test "multiple streams" do script = """ for i in {1..1000}; do