defmodule Systemd.Manager do @moduledoc """ Client for `org.freedesktop.systemd1.Manager`. """ alias Systemd.{DBus, Error, Job, JobStatus, Unit, UnitFileOperation, UnitFileStatus, UnitObject} alias Systemd.Manager.Options alias Systemd.TransientUnit.{AuxUnit, Property} @destination "org.freedesktop.systemd1" @path "/org/freedesktop/systemd1" @interface "org.freedesktop.systemd1.Manager" @doc """ Connects to the system bus. """ @spec connect(keyword()) :: {:ok, pid()} | {:error, Error.t()} def connect(opts \\ []) do opts = Options.new(opts) DBus.connect(opts.bus, []) end @doc """ Lists currently loaded systemd units. Accepts either an existing D-Bus connection PID or connection options. Passing options opens a short-lived connection. """ @spec list_units(pid() | keyword()) :: {:ok, [Unit.t()]} | {:error, Error.t()} def list_units(conn_or_opts \\ []) def list_units(conn) when is_pid(conn) do with {:ok, [units]} <- call(conn, "ListUnits") do {:ok, Enum.map(units, &Unit.from_list_units_row/1)} end end def list_units(opts) when is_list(opts) do with {:ok, conn} <- connect(opts) do list_units(conn) end end @doc """ Lists currently queued jobs. """ @spec list_jobs(pid()) :: {:ok, [JobStatus.t()]} | {:error, Error.t()} def list_jobs(conn) when is_pid(conn) do with {:ok, [jobs]} <- call(conn, "ListJobs") do {:ok, Enum.map(jobs, &JobStatus.from_dbus/1)} end end @doc """ Gets the D-Bus object path for a loaded unit. """ @spec get_unit(pid(), String.t()) :: {:ok, UnitObject.t()} | {:error, Error.t()} def get_unit(conn, unit_name) when is_pid(conn) and is_binary(unit_name) do with {:ok, [object_path]} <- call(conn, "GetUnit", [unit_name], "s") do {:ok, UnitObject.new(object_path, unit_name)} end end @doc """ Gets the D-Bus object path for a unit by main process ID. """ @spec get_unit_by_pid(pid(), non_neg_integer()) :: {:ok, UnitObject.t()} | {:error, Error.t()} def get_unit_by_pid(conn, pid) when is_pid(conn) and is_integer(pid) and pid >= 0 do with {:ok, [object_path]} <- call(conn, "GetUnitByPID", [pid], "u") do {:ok, UnitObject.new(object_path)} end end @doc """ Returns unit files known to systemd and their enablement state. """ @spec list_unit_files(pid()) :: {:ok, [UnitFileStatus.t()]} | {:error, Error.t()} def list_unit_files(conn) when is_pid(conn) do with {:ok, [unit_files]} <- call(conn, "ListUnitFiles") do {:ok, Enum.map(unit_files, &UnitFileStatus.from_dbus/1)} end end @doc """ Returns the enablement state of a unit file. """ @spec unit_file_state(pid(), String.t()) :: {:ok, String.t()} | {:error, Error.t()} def unit_file_state(conn, unit_name) when is_pid(conn) and is_binary(unit_name) do with {:ok, [state]} <- call(conn, "GetUnitFileState", [unit_name], "s") do {:ok, state} end end @doc """ Starts a unit and returns the queued systemd job. """ @spec start_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def start_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "StartUnit", unit_name, opts) end @doc """ Stops a unit and returns the queued systemd job. """ @spec stop_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def stop_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "StopUnit", unit_name, opts) end @doc """ Restarts a unit and returns the queued systemd job. """ @spec restart_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def restart_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "RestartUnit", unit_name, opts) end @doc """ Reloads a unit and returns the queued systemd job. """ @spec reload_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def reload_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "ReloadUnit", unit_name, opts) end @doc """ Tries to restart a unit only if it is already active. """ @spec try_restart_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def try_restart_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "TryRestartUnit", unit_name, opts) end @doc """ Reloads a unit if supported, otherwise restarts it. """ @spec reload_or_restart_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def reload_or_restart_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "ReloadOrRestartUnit", unit_name, opts) end @doc """ Reloads a unit if supported, otherwise tries to restart it only if active. """ @spec reload_or_try_restart_unit(pid(), String.t(), keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def reload_or_try_restart_unit(conn, unit_name, opts \\ []) do unit_operation(conn, "ReloadOrTryRestartUnit", unit_name, opts) end @doc """ Resets failed state for a unit. """ @spec reset_failed_unit(pid(), String.t()) :: :ok | {:error, Error.t()} def reset_failed_unit(conn, unit_name) when is_pid(conn) and is_binary(unit_name) do with {:ok, []} <- call(conn, "ResetFailedUnit", [unit_name], "s"), do: :ok end @doc """ Sends a Unix signal to processes belonging to a unit. """ @spec kill_unit(pid(), String.t(), String.t(), integer()) :: :ok | {:error, Error.t()} def kill_unit(conn, unit_name, who \\ "all", signal \\ 15) def kill_unit(conn, unit_name, who, signal) when is_pid(conn) and is_binary(unit_name) and is_binary(who) and is_integer(signal) do with {:ok, []} <- call(conn, "KillUnit", [unit_name, who, signal], "ssi"), do: :ok end @doc """ Starts a transient unit and returns the queued systemd job. """ @spec start_transient_unit(pid(), String.t(), [Property.t()], keyword()) :: {:ok, Job.t()} | {:error, Error.t()} def start_transient_unit(conn, unit_name, properties, opts \\ []) do mode = Options.new(opts).mode properties = Enum.map(properties, &Property.to_dbus/1) aux_units = opts |> Keyword.get(:aux_units, []) |> Enum.map(&AuxUnit.to_dbus/1) with {:ok, [object_path]} <- call( conn, "StartTransientUnit", [unit_name, mode, properties, aux_units], "ssa(sv)a(sa(sv))" ) do {:ok, %Job{object_path: object_path}} end end @doc """ Enables unit files. """ @spec enable_unit_files(pid(), [String.t()], keyword()) :: {:ok, UnitFileOperation.t()} | {:error, Error.t()} def enable_unit_files(conn, files, opts \\ []) do opts = Options.new(opts) runtime? = opts.runtime force? = opts.force with {:ok, [carries_install_info?, changes]} <- call(conn, "EnableUnitFiles", [files, runtime?, force?], "asbb") do {:ok, UnitFileOperation.new(changes, carries_install_info: carries_install_info?)} end end @doc """ Disables unit files. """ @spec disable_unit_files(pid(), [String.t()], keyword()) :: {:ok, UnitFileOperation.t()} | {:error, Error.t()} def disable_unit_files(conn, files, opts \\ []) do runtime? = Options.new(opts).runtime with {:ok, [changes]} <- call(conn, "DisableUnitFiles", [files, runtime?], "asb") do {:ok, UnitFileOperation.new(changes)} end end @doc """ Masks unit files. """ @spec mask_unit_files(pid(), [String.t()], keyword()) :: {:ok, UnitFileOperation.t()} | {:error, Error.t()} def mask_unit_files(conn, files, opts \\ []) do opts = Options.new(opts) runtime? = opts.runtime force? = opts.force with {:ok, [changes]} <- call(conn, "MaskUnitFiles", [files, runtime?, force?], "asbb") do {:ok, UnitFileOperation.new(changes)} end end @doc """ Unmasks unit files. """ @spec unmask_unit_files(pid(), [String.t()], keyword()) :: {:ok, UnitFileOperation.t()} | {:error, Error.t()} def unmask_unit_files(conn, files, opts \\ []) do runtime? = Options.new(opts).runtime with {:ok, [changes]} <- call(conn, "UnmaskUnitFiles", [files, runtime?], "asb") do {:ok, UnitFileOperation.new(changes)} end end @doc """ Links unit files into systemd's search path. """ @spec link_unit_files(pid(), [String.t()], keyword()) :: {:ok, UnitFileOperation.t()} | {:error, Error.t()} def link_unit_files(conn, files, opts \\ []) do opts = Options.new(opts) runtime? = opts.runtime force? = opts.force with {:ok, [changes]} <- call(conn, "LinkUnitFiles", [files, runtime?, force?], "asbb") do {:ok, UnitFileOperation.new(changes)} end end @doc """ Reloads systemd manager configuration (`daemon-reload`). """ @spec reload(pid()) :: :ok | {:error, Error.t()} def reload(conn) when is_pid(conn) do with {:ok, []} <- call(conn, "Reload"), do: :ok end defp unit_operation(conn, member, unit_name, opts) do mode = Options.new(opts).mode with {:ok, [object_path]} <- call(conn, member, [unit_name, mode], "ss") do {:ok, %Job{object_path: object_path}} end end defp call(conn, member, body \\ [], signature \\ "") do DBus.call_body(conn, destination: @destination, path: @path, interface: @interface, member: member, signature: signature, body: body ) end end