Current section
Files
Jump to
Current section
Files
lib/mash/runner.ex
defmodule Mash.Runner do
@moduledoc """
The process that runs the jobs through a series of Tasks, writes the IO, and monitors the results.
"""
use GenServer
@type job_state :: :to_run | :running | :passed | :failed | :skipped
@type result :: term()
def tick do
GenServer.cast(self(), :tick)
end
def start_link(opts \\ []) do
opts = Keyword.put(opts, :parent, self())
GenServer.start_link(__MODULE__, opts)
end
@impl GenServer
def init(opts \\ []) do
parent = Keyword.get(opts, :parent)
tick()
jobs = Enum.map(Mash.Config.jobs(), &Map.put(&1, :state, :to_run))
{:ok, {parent, jobs}}
end
@impl GenServer
def handle_cast(:tick, {parent, jobs}) do
new_jobs = Enum.map(jobs, &tick_job(&1, jobs))
if any_running?(new_jobs) do
tick()
else
send(parent, {:result, Enum.filter(jobs, &(&1.state == :failed))})
Process.exit(self(), :normal)
end
{:noreply, {parent, new_jobs}}
end
@spec tick_job(%{state: job_state()}, [Mash.Job.t()]) :: Mash.Job.t()
def tick_job(%{state: :to_run} = job, jobs) do
cond do
!if?(job, jobs) ->
%{job | state: :skipped}
ready?(job, jobs) ->
%{job | state: :running}
skippable?(job, jobs) ->
%{job | state: :skipped}
true ->
job
end
end
def tick_job(%{name: name, state: :running, task: nil} = job, _jobs) do
IO.puts("[runner] Running #{name}")
{task, io_pid} = Mash.Execution.run(job)
%{job | task: task, io_pid: io_pid}
end
def tick_job(%{name: name, state: :running, task: task, io_pid: io_pid} = job, _jobs) when is_struct(task, Task) do
flush_io(name, io_pid)
job
end
def tick_job(%{state: state} = job, _jobs) when state in [:passed, :failed, :skipped], do: job
def if?(%{if: if_fun}, jobs) when is_function(if_fun), do: !!if_fun.(jobs)
def if?(%{if: if_val}, _jobs), do: !!if_val
def any_running?(jobs) do
Enum.any?(jobs, &(&1.state == :running))
end
defp flush_io(name, io_pid) do
io_pid
|> StringIO.flush()
|> then(fn
"" ->
:ok
other ->
other
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.map(&"[#{name}] #{&1}\n")
|> IO.puts()
end)
end
defp ready?(%{needs: needs}, jobs) do
Enum.all?(needs, fn
{needed_job_name, :optional} ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state in [:skipped, :passed]
needed_job_name ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state == :passed
end)
end
defp skippable?(%{needs: needs}, jobs) do
Enum.any?(needs, fn
{needed_job_name, :optional} ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state == :failed
needed_job_name ->
needed_job = Enum.find(jobs, &(&1.name == needed_job_name))
needed_job.state in [:failed, :skipped]
end)
end
defp interpret_result({_stream_data, 0}), do: :passed
defp interpret_result({_stream_data, exit_code}) when exit_code > 0, do: :failed
defp interpret_result(:ok), do: :passed
defp interpret_result({:ok, _}), do: :passed
defp interpret_result(_), do: :failed
@impl GenServer
def handle_info({_ref, {job, result}}, {parent, jobs}) do
%{name: job_name} = job
StringIO.close(job.io_pid)
new_state = interpret_result(result)
IO.puts("[runner] Finishing #{job_name} -- #{new_state}")
new_job = %{
job
| state: new_state,
task: nil,
io_pid: nil
}
new_jobs =
Enum.map(jobs, fn
%{name: ^job_name} ->
new_job
job ->
job
end)
{:noreply, {parent, new_jobs}}
end
def handle_info({:DOWN, _ref, :process, _pid, _reason}, state) do
{:noreply, state}
end
end