Skip to content

Commit 1ca0bdf

Browse files
committed
Expand builders and unit file D-Bus queries
1 parent d28c9c9 commit 1ca0bdf

11 files changed

Lines changed: 252 additions & 14 deletions

File tree

lib/systemd.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ defmodule Systemd do
7373
end)
7474
end
7575

76+
@doc """
77+
Returns unit files known to systemd using a short-lived connection.
78+
"""
79+
@spec list_unit_files(keyword()) :: {:ok, [Systemd.UnitFileStatus.t()]} | {:error, Error.t()}
80+
def list_unit_files(opts \\ []) do
81+
with_connection(opts, &Manager.list_unit_files/1)
82+
end
83+
84+
@doc """
85+
Returns the enablement state of a unit file using a short-lived connection.
86+
"""
87+
@spec unit_file_state(String.t(), keyword()) :: {:ok, String.t()} | {:error, Error.t()}
88+
def unit_file_state(name, opts \\ []) do
89+
with_connection(opts, &Manager.unit_file_state(&1, name))
90+
end
91+
7692
@doc """
7793
Starts a unit and waits for the returned job by default.
7894
"""

lib/systemd/dbus/signature.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule Systemd.DBus.Signature do
33

44
@supported_complex_signatures MapSet.new([
55
"a(ssssssouso)",
6+
"a(ss)",
67
"a{sv}",
78
"a(sv)",
89
"a(sa(sv))",

lib/systemd/manager.ex

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Systemd.Manager do
33
Client for `org.freedesktop.systemd1.Manager`.
44
"""
55

6-
alias Systemd.{DBus, Error, Job, Unit, UnitFileOperation, UnitObject}
6+
alias Systemd.{DBus, Error, Job, Unit, UnitFileOperation, UnitFileStatus, UnitObject}
77
alias Systemd.Manager.Options
88
alias Systemd.TransientUnit.{AuxUnit, Property}
99

@@ -47,7 +47,37 @@ defmodule Systemd.Manager do
4747
@spec get_unit(pid(), String.t()) :: {:ok, UnitObject.t()} | {:error, Error.t()}
4848
def get_unit(conn, unit_name) when is_pid(conn) and is_binary(unit_name) do
4949
with {:ok, [object_path]} <- call(conn, "GetUnit", [unit_name], "s") do
50-
{:ok, %UnitObject{name: unit_name, object_path: object_path}}
50+
{:ok, UnitObject.new(object_path, unit_name)}
51+
end
52+
end
53+
54+
@doc """
55+
Gets the D-Bus object path for a unit by main process ID.
56+
"""
57+
@spec get_unit_by_pid(pid(), non_neg_integer()) :: {:ok, UnitObject.t()} | {:error, Error.t()}
58+
def get_unit_by_pid(conn, pid) when is_pid(conn) and is_integer(pid) and pid >= 0 do
59+
with {:ok, [object_path]} <- call(conn, "GetUnitByPID", [pid], "u") do
60+
{:ok, UnitObject.new(object_path)}
61+
end
62+
end
63+
64+
@doc """
65+
Returns unit files known to systemd and their enablement state.
66+
"""
67+
@spec list_unit_files(pid()) :: {:ok, [UnitFileStatus.t()]} | {:error, Error.t()}
68+
def list_unit_files(conn) when is_pid(conn) do
69+
with {:ok, [unit_files]} <- call(conn, "ListUnitFiles") do
70+
{:ok, Enum.map(unit_files, &UnitFileStatus.from_dbus/1)}
71+
end
72+
end
73+
74+
@doc """
75+
Returns the enablement state of a unit file.
76+
"""
77+
@spec unit_file_state(pid(), String.t()) :: {:ok, String.t()} | {:error, Error.t()}
78+
def unit_file_state(conn, unit_name) when is_pid(conn) and is_binary(unit_name) do
79+
with {:ok, [state]} <- call(conn, "GetUnitFileState", [unit_name], "s") do
80+
{:ok, state}
5181
end
5282
end
5383

lib/systemd/unit_file.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ defmodule Systemd.UnitFile do
2020
@spec service(keyword()) :: t()
2121
defdelegate service(opts), to: Builder
2222

23+
@doc """
24+
Builds a socket unit file from common `Unit`, `Socket`, and `Install` sections.
25+
"""
26+
@spec socket(keyword()) :: t()
27+
defdelegate socket(opts), to: Builder
28+
29+
@doc """
30+
Builds a timer unit file from common `Unit`, `Timer`, and `Install` sections.
31+
"""
32+
@spec timer(keyword()) :: t()
33+
defdelegate timer(opts), to: Builder
34+
2335
@doc """
2436
Parses unit file text.
2537
"""

lib/systemd/unit_file/builder.ex

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,33 @@ defmodule Systemd.UnitFile.Builder do
88
@type directives :: keyword() | %{optional(atom() | String.t()) => term()}
99

1010
@directive_names %{
11-
limit_nofile: "LimitNOFILE"
11+
cpu_accounting: "CPUAccounting",
12+
io_accounting: "IOAccounting",
13+
ip_accounting: "IPAccounting",
14+
limit_as: "LimitAS",
15+
limit_core: "LimitCORE",
16+
limit_cpu: "LimitCPU",
17+
limit_data: "LimitDATA",
18+
limit_fsize: "LimitFSIZE",
19+
limit_locks: "LimitLOCKS",
20+
limit_memlock: "LimitMEMLOCK",
21+
limit_msgqueue: "LimitMSGQUEUE",
22+
limit_nice: "LimitNICE",
23+
limit_nofile: "LimitNOFILE",
24+
limit_nproc: "LimitNPROC",
25+
limit_rss: "LimitRSS",
26+
limit_rtprio: "LimitRTPRIO",
27+
limit_rttime: "LimitRTTIME",
28+
limit_sigpending: "LimitSIGPENDING",
29+
oom_policy: "OOMPolicy",
30+
pid_file: "PIDFile",
31+
runtime_directory_preserve: "RuntimeDirectoryPreserve",
32+
selinux_context: "SELinuxContext",
33+
smack_process_label: "SmackProcessLabel",
34+
syslog_identifier: "SyslogIdentifier",
35+
tty_path: "TTYPath",
36+
usb_function_descriptors: "USBFunctionDescriptors",
37+
usb_function_strings: "USBFunctionStrings"
1238
}
1339

1440
@doc """
@@ -22,6 +48,28 @@ defmodule Systemd.UnitFile.Builder do
2248
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
2349
end
2450

51+
@doc """
52+
Builds a socket unit file from common `Unit`, `Socket`, and `Install` sections.
53+
"""
54+
@spec socket(keyword()) :: UnitFile.t()
55+
def socket(opts) when is_list(opts) do
56+
UnitFile.parse!("")
57+
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
58+
|> maybe_append_section("Socket", Keyword.get(opts, :socket, []))
59+
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
60+
end
61+
62+
@doc """
63+
Builds a timer unit file from common `Unit`, `Timer`, and `Install` sections.
64+
"""
65+
@spec timer(keyword()) :: UnitFile.t()
66+
def timer(opts) when is_list(opts) do
67+
UnitFile.parse!("")
68+
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
69+
|> maybe_append_section("Timer", Keyword.get(opts, :timer, []))
70+
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
71+
end
72+
2573
@doc """
2674
Builds a unit file with one section.
2775
"""

lib/systemd/unit_file/validator.ex

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ defmodule Systemd.UnitFile.Validator do
2525
@known_directives %{
2626
"Unit" =>
2727
MapSet.new(
28-
~w(Description Documentation Requires Wants After Before BindsTo PartOf Conflicts ConditionPathExists AssertPathExists StartLimitIntervalSec StartLimitBurst)
28+
~w(Description Documentation Requires Wants After Before BindsTo PartOf Conflicts RequiresMountsFor ConditionPathExists AssertPathExists StartLimitIntervalSec StartLimitBurst)
2929
),
3030
"Service" =>
3131
MapSet.new(
32-
~w(Type ExecStart ExecStartPre ExecStartPost ExecReload ExecStop ExecStopPost Restart RestartSec User Group WorkingDirectory Environment EnvironmentFile TimeoutStartSec TimeoutStopSec KillSignal KillMode RemainAfterExit PIDFile RuntimeDirectory StateDirectory CacheDirectory LogsDirectory StandardOutput StandardError LimitNOFILE)
32+
~w(Type ExecStart ExecStartPre ExecStartPost ExecCondition ExecReload ExecStop ExecStopPost Restart RestartSec User Group WorkingDirectory RootDirectory Environment EnvironmentFile PassEnvironment UnsetEnvironment TimeoutStartSec TimeoutStopSec TimeoutAbortSec KillSignal KillMode RemainAfterExit GuessMainPID PIDFile RuntimeDirectory RuntimeDirectoryPreserve StateDirectory CacheDirectory LogsDirectory ConfigurationDirectory StandardOutput StandardError SyslogIdentifier LimitNOFILE LimitNPROC LimitMEMLOCK LimitCORE LimitCPU LimitAS LimitFSIZE NoNewPrivileges PrivateTmp PrivateDevices ProtectSystem ProtectHome AmbientCapabilities CapabilityBoundingSet ReadWritePaths ReadOnlyPaths InaccessiblePaths OOMPolicy)
3333
),
3434
"Install" => MapSet.new(~w(WantedBy RequiredBy Also Alias DefaultInstance)),
3535
"Socket" =>
3636
MapSet.new(
37-
~w(ListenStream ListenDatagram ListenSequentialPacket SocketUser SocketGroup SocketMode Accept Service)
37+
~w(ListenStream ListenDatagram ListenSequentialPacket ListenFIFO ListenSpecial ListenNetlink ListenMessageQueue ListenUSBFunction SocketUser SocketGroup SocketMode DirectoryMode Accept Writable MaxConnections MaxConnectionsPerSource KeepAlive NoDelay FreeBind BindIPv6Only Backlog Service)
3838
),
3939
"Timer" =>
4040
MapSet.new(
41-
~w(OnActiveSec OnBootSec OnStartupSec OnUnitActiveSec OnUnitInactiveSec OnCalendar Unit Persistent AccuracySec RandomizedDelaySec)
41+
~w(OnActiveSec OnBootSec OnStartupSec OnUnitActiveSec OnUnitInactiveSec OnCalendar Unit Persistent AccuracySec RandomizedDelaySec FixedRandomDelay WakeSystem RemainAfterElapse)
4242
),
4343
"Target" => MapSet.new(~w(AllowIsolate)),
4444
"Mount" =>
@@ -215,10 +215,25 @@ defmodule Systemd.UnitFile.Validator do
215215
end
216216

217217
defp value_errors("Service", directive, value, span)
218-
when directive in ["TimeoutStartSec", "TimeoutStopSec", "RestartSec"] do
218+
when directive in ["TimeoutStartSec", "TimeoutStopSec", "TimeoutAbortSec", "RestartSec"] do
219219
duration("Service", directive, value, span)
220220
end
221221

222+
defp value_errors("Service", directive, value, span)
223+
when directive in [
224+
"RemainAfterExit",
225+
"GuessMainPID",
226+
"NoNewPrivileges",
227+
"PrivateTmp",
228+
"PrivateDevices"
229+
] do
230+
boolean("Service", directive, value, span)
231+
end
232+
233+
defp value_errors("Service", "KillMode", value, span) do
234+
one_of("Service", "KillMode", value, ~w(control-group mixed process none), span)
235+
end
236+
222237
defp value_errors("Timer", directive, value, span)
223238
when directive in [
224239
"OnActiveSec",
@@ -232,19 +247,37 @@ defmodule Systemd.UnitFile.Validator do
232247
duration("Timer", directive, value, span)
233248
end
234249

235-
defp value_errors("Timer", "Persistent", value, span),
236-
do: boolean("Timer", "Persistent", value, span)
250+
defp value_errors("Timer", directive, value, span)
251+
when directive in ["Persistent", "FixedRandomDelay", "WakeSystem", "RemainAfterElapse"] do
252+
boolean("Timer", directive, value, span)
253+
end
237254

238255
defp value_errors("Timer", "OnCalendar", value, span),
239256
do: non_empty("Timer", "OnCalendar", value, span)
240257

241258
defp value_errors("Socket", directive, value, span)
242-
when directive in ["ListenStream", "ListenDatagram", "ListenSequentialPacket"] do
259+
when directive in [
260+
"ListenStream",
261+
"ListenDatagram",
262+
"ListenSequentialPacket",
263+
"ListenFIFO",
264+
"ListenSpecial",
265+
"ListenNetlink",
266+
"ListenMessageQueue",
267+
"ListenUSBFunction"
268+
] do
243269
non_empty("Socket", directive, value, span)
244270
end
245271

246-
defp value_errors("Socket", "SocketMode", value, span),
247-
do: octal_mode("Socket", "SocketMode", value, span)
272+
defp value_errors("Socket", directive, value, span)
273+
when directive in ["SocketMode", "DirectoryMode"] do
274+
octal_mode("Socket", directive, value, span)
275+
end
276+
277+
defp value_errors("Socket", directive, value, span)
278+
when directive in ["Accept", "Writable", "KeepAlive", "NoDelay", "FreeBind"] do
279+
boolean("Socket", directive, value, span)
280+
end
248281

249282
defp value_errors("Install", directive, value, span)
250283
when directive in ["WantedBy", "RequiredBy", "Also", "Alias"] do

lib/systemd/unit_file_status.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
defmodule Systemd.UnitFileStatus do
2+
@moduledoc """
3+
Unit-file status returned by systemd's `ListUnitFiles` D-Bus method.
4+
"""
5+
6+
@enforce_keys [:path, :state]
7+
@type t :: %__MODULE__{
8+
path: String.t(),
9+
state: String.t()
10+
}
11+
12+
defstruct [:path, :state]
13+
14+
@doc false
15+
@spec new(String.t(), String.t()) :: t()
16+
def new(path, state) when is_binary(path) and is_binary(state) do
17+
%__MODULE__{path: path, state: state}
18+
end
19+
20+
@doc false
21+
@spec from_dbus(tuple() | list()) :: t()
22+
def from_dbus({path, state}), do: from_dbus([path, state])
23+
def from_dbus([path, state]), do: new(path, state)
24+
end

test/systemd/dbus/signature_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Systemd.DBus.SignatureTest do
66
test "documents signatures this package intentionally uses" do
77
assert Signature.supported?("")
88
assert Signature.supported?("asbb")
9+
assert Signature.supported?("a(ss)")
910
assert Signature.supported?("ssa(sv)a(sa(sv))")
1011
refute Signature.supported?("a{sa{sv}}")
1112
end

test/systemd/manager_integration_test.exs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
defmodule Systemd.ManagerIntegrationTest do
22
use ExUnit.Case, async: false
33

4-
alias Systemd.{Error, Job, Manager, TransientUnit, Unit, UnitFileOperation, UnitObject}
4+
alias Systemd.{
5+
Error,
6+
Job,
7+
Manager,
8+
TransientUnit,
9+
Unit,
10+
UnitFileOperation,
11+
UnitFileStatus,
12+
UnitObject
13+
}
514

615
@moduletag :integration
716

@@ -13,6 +22,19 @@ defmodule Systemd.ManagerIntegrationTest do
1322
assert Enum.any?(units, &(&1.name == "init.scope" or &1.name == "-.slice"))
1423
end
1524

25+
test "lists unit files and reads unit-file state" do
26+
assert {:ok, conn} = Manager.connect()
27+
28+
assert {:ok, unit_files} = Manager.list_unit_files(conn)
29+
30+
assert Enum.any?(unit_files, fn %UnitFileStatus{path: path, state: state} ->
31+
is_binary(path) and String.ends_with?(path, ".service") and is_binary(state)
32+
end)
33+
34+
assert {:ok, state} = Manager.unit_file_state(conn, "dbus.service")
35+
assert is_binary(state)
36+
end
37+
1638
test "gets a unit object and reads common properties" do
1739
assert {:ok, conn} = Manager.connect()
1840
assert {:ok, %UnitObject{} = unit} = Manager.get_unit(conn, "dbus.service")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
defmodule Systemd.UnitFileStatusTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Systemd.UnitFileStatus
5+
6+
test "builds unit-file status from D-Bus rows" do
7+
assert %UnitFileStatus{path: "dbus.service", state: "enabled"} =
8+
UnitFileStatus.from_dbus(["dbus.service", "enabled"])
9+
10+
assert %UnitFileStatus{path: "ssh.service", state: "disabled"} =
11+
UnitFileStatus.from_dbus({"ssh.service", "disabled"})
12+
end
13+
end

0 commit comments

Comments
 (0)