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