defmodule Systemd do
@moduledoc """
Pure Elixir tools for working with systemd.
The package provides a D-Bus backed manager client, unit object/property APIs,
job awaiting, installation helpers, and a loss-aware unit file parser/generator.
## Examples
{:ok, units} = Systemd.list_units()
:ok = Systemd.start_unit("example.service")
{:ok, state} = Systemd.unit_state("dbus.service")
unit_file =
Systemd.UnitFile.service(
unit: [description: "Example"],
service: [exec_start: "/bin/true", type: :oneshot],
install: [wanted_by: "multi-user.target"]
)
:ok = Systemd.UnitFile.validate(unit_file, :service)
Mutating calls can return `{:error, %Systemd.Error{category: :permission}}`
when systemd or polkit denies the D-Bus operation.
"""
alias Systemd.{Error, Manager}
alias Systemd.Manager.Options
@type connection_option :: {:bus, Systemd.DBus.bus()}
@doc """
Runs a function with a short-lived systemd manager D-Bus connection.
"""
@spec with_connection(keyword(), (pid() -> result)) :: result | {:error, Error.t()}
when result: term()
def with_connection(opts \\ [], fun) when is_function(fun, 1) do
with {:ok, conn} <- Manager.connect(opts) do
try do
fun.(conn)
after
close(conn)
end
end
end
@doc """
Closes a D-Bus connection process opened by this package.
"""
@spec close(pid()) :: :ok
def close(conn) when is_pid(conn) do
if Process.alive?(conn), do: Process.exit(conn, :shutdown)
:ok
end
@doc """
Lists loaded units using a short-lived connection.
"""
@spec list_units(keyword()) :: {:ok, [Systemd.Unit.t()]} | {:error, Error.t()}
def list_units(opts \\ []) do
with_connection(opts, &Manager.list_units/1)
end
@doc """
Lists queued jobs using a short-lived connection.
"""
@spec list_jobs(keyword()) :: {:ok, [Systemd.JobStatus.t()]} | {:error, Error.t()}
def list_jobs(opts \\ []) do
with_connection(opts, &Manager.list_jobs/1)
end
@doc """
Reads common state for a unit using a short-lived connection.
"""
@spec unit_state(String.t(), keyword()) :: {:ok, Systemd.UnitState.t()} | {:error, Error.t()}
def unit_state(name, opts \\ []) do
with_connection(opts, fn conn ->
with {:ok, unit} <- Manager.get_unit(conn, name) do
Systemd.UnitObject.state(conn, unit)
end
end)
end
@doc """
Returns unit files known to systemd using a short-lived connection.
"""
@spec list_unit_files(keyword()) :: {:ok, [Systemd.UnitFileStatus.t()]} | {:error, Error.t()}
def list_unit_files(opts \\ []) do
with_connection(opts, &Manager.list_unit_files/1)
end
@doc """
Returns the enablement state of a unit file using a short-lived connection.
"""
@spec unit_file_state(String.t(), keyword()) :: {:ok, String.t()} | {:error, Error.t()}
def unit_file_state(name, opts \\ []) do
with_connection(opts, &Manager.unit_file_state(&1, name))
end
@doc """
Starts a unit and waits for the returned job by default.
"""
@spec start_unit(String.t(), keyword()) :: :ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def start_unit(name, opts \\ []) do
run_unit_operation(:start_unit, name, opts)
end
@doc """
Stops a unit and waits for the returned job by default.
"""
@spec stop_unit(String.t(), keyword()) :: :ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def stop_unit(name, opts \\ []) do
run_unit_operation(:stop_unit, name, opts)
end
@doc """
Restarts a unit and waits for the returned job by default.
"""
@spec restart_unit(String.t(), keyword()) :: :ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def restart_unit(name, opts \\ []) do
run_unit_operation(:restart_unit, name, opts)
end
@doc """
Reloads a unit and waits for the returned job by default.
"""
@spec reload_unit(String.t(), keyword()) :: :ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def reload_unit(name, opts \\ []) do
run_unit_operation(:reload_unit, name, opts)
end
@doc """
Tries to restart a unit only if it is already active.
"""
@spec try_restart_unit(String.t(), keyword()) ::
:ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def try_restart_unit(name, opts \\ []) do
run_unit_operation(:try_restart_unit, name, opts)
end
@doc """
Reloads a unit if supported, otherwise restarts it.
"""
@spec reload_or_restart_unit(String.t(), keyword()) ::
:ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def reload_or_restart_unit(name, opts \\ []) do
run_unit_operation(:reload_or_restart_unit, name, opts)
end
@doc """
Reloads a unit if supported, otherwise tries to restart it only if active.
"""
@spec reload_or_try_restart_unit(String.t(), keyword()) ::
:ok | {:ok, Systemd.Job.t()} | {:error, Error.t()}
def reload_or_try_restart_unit(name, opts \\ []) do
run_unit_operation(:reload_or_try_restart_unit, name, opts)
end
@doc """
Resets failed state for a unit using a short-lived connection.
"""
@spec reset_failed_unit(String.t(), keyword()) :: :ok | {:error, Error.t()}
def reset_failed_unit(name, opts \\ []) do
with_connection(opts, &Manager.reset_failed_unit(&1, name))
end
@doc """
Sends a Unix signal to processes belonging to a unit using a short-lived connection.
"""
@spec kill_unit(String.t(), String.t(), integer(), keyword()) :: :ok | {:error, Error.t()}
def kill_unit(name, who \\ "all", signal \\ 15, opts \\ []) do
with_connection(opts, &Manager.kill_unit(&1, name, who, signal))
end
@doc """
Enables unit files using a short-lived connection.
"""
@spec enable_unit_files([String.t()], keyword()) ::
{:ok, Systemd.UnitFileOperation.t()} | {:error, Error.t()}
def enable_unit_files(files, opts \\ []) do
with_connection(opts, &Manager.enable_unit_files(&1, files, opts))
end
@doc """
Disables unit files using a short-lived connection.
"""
@spec disable_unit_files([String.t()], keyword()) ::
{:ok, Systemd.UnitFileOperation.t()} | {:error, Error.t()}
def disable_unit_files(files, opts \\ []) do
with_connection(opts, &Manager.disable_unit_files(&1, files, opts))
end
@doc """
Masks unit files using a short-lived connection.
"""
@spec mask_unit_files([String.t()], keyword()) ::
{:ok, Systemd.UnitFileOperation.t()} | {:error, Error.t()}
def mask_unit_files(files, opts \\ []) do
with_connection(opts, &Manager.mask_unit_files(&1, files, opts))
end
@doc """
Unmasks unit files using a short-lived connection.
"""
@spec unmask_unit_files([String.t()], keyword()) ::
{:ok, Systemd.UnitFileOperation.t()} | {:error, Error.t()}
def unmask_unit_files(files, opts \\ []) do
with_connection(opts, &Manager.unmask_unit_files(&1, files, opts))
end
@doc """
Links unit files using a short-lived connection.
"""
@spec link_unit_files([String.t()], keyword()) ::
{:ok, Systemd.UnitFileOperation.t()} | {:error, Error.t()}
def link_unit_files(files, opts \\ []) do
with_connection(opts, &Manager.link_unit_files(&1, files, opts))
end
@doc """
Reloads systemd manager configuration using a short-lived connection.
"""
@spec reload(keyword()) :: :ok | {:error, Error.t()}
def reload(opts \\ []) do
with_connection(opts, &Manager.reload/1)
end
defp run_unit_operation(operation, name, opts) do
opts = Options.new(opts)
wait? = opts.wait
await_opts = Options.await_opts(opts)
with_connection([bus: opts.bus], fn conn ->
Manager
|> apply(operation, [conn, name, opts])
|> maybe_await_job(conn, wait?, await_opts)
end)
end
defp maybe_await_job({:ok, job}, conn, true, await_opts),
do: Systemd.Job.await(conn, job, await_opts)
defp maybe_await_job({:ok, job}, _conn, false, _await_opts), do: {:ok, job}
defp maybe_await_job({:error, error}, _conn, _wait?, _await_opts), do: {:error, error}
end