Packages
A typed Elixir wrapper for the Docker CLI with struct-based commands and pipeline composition
Current section
Files
Jump to
Current section
Files
lib/docker/stream.ex
defmodule Docker.Stream do
@moduledoc """
Port-based streaming for long-running Docker commands.
A BEAM-native extension not present in the Rust crate. Useful for
`docker logs --follow`, `docker events`, `docker build`, etc.
Lines are sent as messages to the subscriber process:
{:docker_stream, pid, {:stdout, line}}
{:docker_stream, pid, {:exit, exit_code}}
## Examples
# Stream logs from a container
{:ok, stream} = Docker.Stream.start_link(
Docker.Commands.Logs.new("my-container") |> Docker.Commands.Logs.follow(),
subscriber: self()
)
receive do
{:docker_stream, ^stream, {:stdout, line}} ->
IO.puts("LOG: \#{line}")
{:docker_stream, ^stream, {:exit, 0}} ->
IO.puts("Stream ended")
end
# Stop streaming
Docker.Stream.stop(stream)
## Options
* `:subscriber` - PID to receive stream messages (required)
* `:config` - `Docker.Config` to use (default: `Docker.Config.new()`)
"""
use GenServer
@type stream_message ::
{:docker_stream, pid(), {:stdout, String.t()}}
| {:docker_stream, pid(), {:exit, non_neg_integer()}}
defstruct [:port, :subscriber, :config, buffer: ""]
# -- Client API --
@doc """
Starts a streaming process for the given command.
"""
@spec start_link(struct(), keyword()) :: GenServer.on_start()
def start_link(%module{} = cmd, opts) do
GenServer.start_link(__MODULE__, {module, cmd, opts})
end
@doc """
Stops the streaming process.
"""
@spec stop(GenServer.server()) :: :ok
def stop(pid), do: GenServer.stop(pid)
# -- Server Callbacks --
@impl true
def init({module, cmd, opts}) do
subscriber = Keyword.fetch!(opts, :subscriber)
config = Keyword.get(opts, :config, Docker.Config.new())
args = Docker.Config.base_args(config) ++ module.args(cmd)
port =
Port.open(
{:spawn_executable, config.binary},
[:binary, :exit_status, :stderr_to_stdout, args: args]
)
{:ok, %__MODULE__{port: port, subscriber: subscriber, config: config}}
end
@impl true
def handle_info({port, {:data, data}}, %{port: port} = state) do
{lines, remaining} = split_lines(state.buffer <> data)
for line <- lines do
send(state.subscriber, {:docker_stream, self(), {:stdout, line}})
end
{:noreply, %{state | buffer: remaining}}
end
def handle_info({port, {:exit_status, code}}, %{port: port} = state) do
if state.buffer != "" do
send(state.subscriber, {:docker_stream, self(), {:stdout, state.buffer}})
end
send(state.subscriber, {:docker_stream, self(), {:exit, code}})
{:stop, :normal, state}
end
# -- Private --
defp split_lines(data) do
parts = String.split(data, "\n")
lines = Enum.slice(parts, 0..-2//1)
remaining = List.last(parts)
{lines, remaining}
end
end