summaryrefslogtreecommitdiffstats
path: root/src/systemctl/systemctl-list-units.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
commitb750101eb236130cf056c675997decbac904cc49 (patch)
treea5df1a06754bdd014cb975c051c83b01c9a97532 /src/systemctl/systemctl-list-units.c
parentInitial commit. (diff)
downloadsystemd-b750101eb236130cf056c675997decbac904cc49.tar.xz
systemd-b750101eb236130cf056c675997decbac904cc49.zip
Adding upstream version 252.22.upstream/252.22upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/systemctl/systemctl-list-units.c')
-rw-r--r--src/systemctl/systemctl-list-units.c936
1 files changed, 936 insertions, 0 deletions
diff --git a/src/systemctl/systemctl-list-units.c b/src/systemctl/systemctl-list-units.c
new file mode 100644
index 0000000..08335d9
--- /dev/null
+++ b/src/systemctl/systemctl-list-units.c
@@ -0,0 +1,936 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-login.h"
+
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "format-table.h"
+#include "locale-util.h"
+#include "set.h"
+#include "sort-util.h"
+#include "systemctl-list-units.h"
+#include "systemctl-util.h"
+#include "systemctl.h"
+#include "terminal-util.h"
+
+static void message_set_freep(Set **set) {
+ set_free_with_destructor(*set, sd_bus_message_unref);
+}
+
+static int get_unit_list_recursive(
+ sd_bus *bus,
+ char **patterns,
+ UnitInfo **ret_unit_infos,
+ Set **ret_replies,
+ char ***ret_machines) {
+
+ _cleanup_free_ UnitInfo *unit_infos = NULL;
+ _cleanup_(message_set_freep) Set *replies = NULL;
+ sd_bus_message *reply;
+ int c, r;
+
+ assert(bus);
+ assert(ret_replies);
+ assert(ret_unit_infos);
+ assert(ret_machines);
+
+ replies = set_new(NULL);
+ if (!replies)
+ return log_oom();
+
+ c = get_unit_list(bus, NULL, patterns, &unit_infos, 0, &reply);
+ if (c < 0)
+ return c;
+
+ r = set_put(replies, reply);
+ if (r < 0) {
+ sd_bus_message_unref(reply);
+ return log_oom();
+ }
+
+ if (arg_recursive) {
+ _cleanup_strv_free_ char **machines = NULL;
+
+ r = sd_get_machine_names(&machines);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get machine names: %m");
+
+ STRV_FOREACH(i, machines) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *container = NULL;
+ int k;
+
+ r = sd_bus_open_system_machine(&container, *i);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to connect to container %s, ignoring: %m", *i);
+ continue;
+ }
+
+ k = get_unit_list(container, *i, patterns, &unit_infos, c, &reply);
+ if (k < 0)
+ return k;
+
+ c = k;
+
+ r = set_put(replies, reply);
+ if (r < 0) {
+ sd_bus_message_unref(reply);
+ return log_oom();
+ }
+ }
+
+ *ret_machines = TAKE_PTR(machines);
+ } else
+ *ret_machines = NULL;
+
+ *ret_unit_infos = TAKE_PTR(unit_infos);
+ *ret_replies = TAKE_PTR(replies);
+
+ return c;
+}
+
+static void output_legend(const char *type, size_t n_items) {
+ const char *on, *off;
+
+ assert(type);
+
+ on = n_items > 0 ? ansi_highlight() : ansi_highlight_red();
+ off = ansi_normal();
+
+ printf("\n%s%zu %ss listed.%s\n", on, n_items, type, off);
+ if (!arg_all)
+ printf("Pass --all to see loaded but inactive %ss, too.\n", type);
+}
+
+static int output_units_list(const UnitInfo *unit_infos, size_t c) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ size_t job_count = 0;
+ int r;
+
+ table = table_new("", "unit", "load", "active", "sub", "job", "description");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, arg_legend != 0);
+ if (arg_plain) {
+ /* Hide the 'glyph' column when --plain is requested */
+ r = table_hide_column_from_display(table, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to hide column: %m");
+ }
+ if (arg_full)
+ table_set_width(table, 0);
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ for (const UnitInfo *u = unit_infos; unit_infos && (size_t) (u - unit_infos) < c; u++) {
+ _cleanup_free_ char *j = NULL;
+ const char *on_underline = "", *on_loaded = "", *on_active = "";
+ const char *on_circle = "", *id;
+ bool circle = false, underline = false;
+
+ if (u + 1 < unit_infos + c &&
+ !streq(unit_type_suffix(u->id), unit_type_suffix((u + 1)->id))) {
+ on_underline = ansi_underline();
+ underline = true;
+ }
+
+ if (STR_IN_SET(u->load_state, "error", "not-found", "bad-setting", "masked") && !arg_plain) {
+ on_circle = underline ? ansi_highlight_yellow_underline() : ansi_highlight_yellow();
+ circle = true;
+ on_loaded = underline ? ansi_highlight_red_underline() : ansi_highlight_red();
+ } else if (streq(u->active_state, "failed") && !arg_plain) {
+ on_circle = underline ? ansi_highlight_red_underline() : ansi_highlight_red();
+ circle = true;
+ on_active = underline ? ansi_highlight_red_underline() : ansi_highlight_red();
+ } else {
+ on_circle = on_underline;
+ on_active = on_underline;
+ on_loaded = on_underline;
+ }
+
+ if (u->machine) {
+ j = strjoin(u->machine, ":", u->id);
+ if (!j)
+ return log_oom();
+
+ id = j;
+ } else
+ id = u->id;
+
+ r = table_add_many(table,
+ TABLE_STRING, circle ? special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE) : " ",
+ TABLE_SET_BOTH_COLORS, on_circle,
+ TABLE_STRING, id,
+ TABLE_SET_BOTH_COLORS, on_active,
+ TABLE_STRING, u->load_state,
+ TABLE_SET_BOTH_COLORS, on_loaded,
+ TABLE_STRING, u->active_state,
+ TABLE_SET_BOTH_COLORS, on_active,
+ TABLE_STRING, u->sub_state,
+ TABLE_SET_BOTH_COLORS, on_active,
+ TABLE_STRING, u->job_id ? u->job_type: "",
+ TABLE_SET_BOTH_COLORS, on_underline,
+ TABLE_STRING, u->description,
+ TABLE_SET_BOTH_COLORS, on_underline);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (u->job_id != 0)
+ job_count++;
+ }
+
+ if (job_count == 0) {
+ /* There's no data in the JOB column, so let's hide it */
+ r = table_hide_column_from_display(table, 5);
+ if (r < 0)
+ return log_error_errno(r, "Failed to hide column: %m");
+ }
+
+ r = output_table(table);
+ if (r < 0)
+ return r;
+
+ if (arg_legend != 0) {
+ const char *on, *off;
+ size_t records = table_get_rows(table) - 1;
+
+ if (records > 0) {
+ puts("\n"
+ "LOAD = Reflects whether the unit definition was properly loaded.\n"
+ "ACTIVE = The high-level unit activation state, i.e. generalization of SUB.\n"
+ "SUB = The low-level unit activation state, values depend on unit type.");
+ if (job_count > 0)
+ puts("JOB = Pending job for the unit.\n");
+ }
+
+ on = records > 0 ? ansi_highlight() : ansi_highlight_red();
+ off = ansi_normal();
+
+ if (arg_all || strv_contains(arg_states, "inactive"))
+ printf("%s%zu loaded units listed.%s\n"
+ "%sTo show all installed unit files use 'systemctl list-unit-files'.%s\n",
+ on, records, off,
+ ansi_grey(), ansi_normal());
+ else if (!arg_states)
+ printf("%s%zu loaded units listed.%s %sPass --all to see loaded but inactive units, too.%s\n"
+ "%sTo show all installed unit files use 'systemctl list-unit-files'.%s\n",
+ on, records, off,
+ ansi_grey(), ansi_normal(), ansi_grey(), ansi_normal());
+ else
+ printf("%zu loaded units listed.\n", records);
+ }
+
+ return 0;
+}
+
+int verb_list_units(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ UnitInfo *unit_infos = NULL;
+ _cleanup_(message_set_freep) Set *replies = NULL;
+ _cleanup_strv_free_ char **machines = NULL;
+ sd_bus *bus;
+ int r;
+
+ r = acquire_bus(BUS_MANAGER, &bus);
+ if (r < 0)
+ return r;
+
+ pager_open(arg_pager_flags);
+
+ if (arg_with_dependencies) {
+ _cleanup_strv_free_ char **names = NULL;
+
+ r = append_unit_dependencies(bus, strv_skip(argv, 1), &names);
+ if (r < 0)
+ return r;
+
+ r = get_unit_list_recursive(bus, names, &unit_infos, &replies, &machines);
+ if (r < 0)
+ return r;
+ } else {
+ r = get_unit_list_recursive(bus, strv_skip(argv, 1), &unit_infos, &replies, &machines);
+ if (r < 0)
+ return r;
+ }
+
+ typesafe_qsort(unit_infos, r, unit_info_compare);
+ return output_units_list(unit_infos, r);
+}
+
+static int get_triggered_units(
+ sd_bus *bus,
+ const char* path,
+ char*** ret) {
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(ret);
+
+ r = sd_bus_get_property_strv(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Unit",
+ "Triggers",
+ &error,
+ ret);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine triggers: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int get_listening(
+ sd_bus *bus,
+ const char* unit_path,
+ char*** listening) {
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ const char *type, *path;
+ int r, n = 0;
+
+ r = sd_bus_get_property(
+ bus,
+ "org.freedesktop.systemd1",
+ unit_path,
+ "org.freedesktop.systemd1.Socket",
+ "Listen",
+ &error,
+ &reply,
+ "a(ss)");
+ if (r < 0)
+ return log_error_errno(r, "Failed to get list of listening sockets: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ss)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ while ((r = sd_bus_message_read(reply, "(ss)", &type, &path)) > 0) {
+
+ r = strv_extend(listening, type);
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extend(listening, path);
+ if (r < 0)
+ return log_oom();
+
+ n++;
+ }
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ return n;
+}
+
+struct socket_info {
+ const char *machine;
+ const char* id;
+
+ char* type;
+ char* path;
+
+ /* Note: triggered is a list here, although it almost certainly will always be one
+ * unit. Nevertheless, dbus API allows for multiple values, so let's follow that. */
+ char** triggered;
+
+ /* The strv above is shared. free is set only in the first one. */
+ bool own_triggered;
+};
+
+static int socket_info_compare(const struct socket_info *a, const struct socket_info *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ r = strcasecmp_ptr(a->machine, b->machine);
+ if (r != 0)
+ return r;
+
+ r = strcmp(a->path, b->path);
+ if (r != 0)
+ return r;
+
+ return strcmp(a->type, b->type);
+}
+
+static int output_sockets_list(struct socket_info *socket_infos, size_t cs) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ assert(socket_infos || cs == 0);
+
+ table = table_new("listen", "type", "unit", "activates");
+ if (!table)
+ return log_oom();
+
+ if (!arg_show_types) {
+ /* Hide the second (TYPE) column */
+ r = table_set_display(table, (size_t) 0, (size_t) 2, (size_t) 3);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set columns to display: %m");
+ }
+
+ table_set_header(table, arg_legend != 0);
+ if (arg_full)
+ table_set_width(table, 0);
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ for (struct socket_info *s = socket_infos; s < socket_infos + cs; s++) {
+ _cleanup_free_ char *j = NULL;
+ const char *path;
+
+ if (s->machine) {
+ j = strjoin(s->machine, ":", s->path);
+ if (!j)
+ return log_oom();
+ path = j;
+ } else
+ path = s->path;
+
+ r = table_add_many(table,
+ TABLE_STRING, path,
+ TABLE_STRING, s->type,
+ TABLE_STRING, s->id);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (strv_isempty(s->triggered))
+ r = table_add_cell(table, NULL, TABLE_EMPTY, NULL);
+ else if (strv_length(s->triggered) == 1)
+ r = table_add_cell(table, NULL, TABLE_STRING, s->triggered[0]);
+ else
+ /* This should never happen, currently our socket units can only trigger a
+ * single unit. But let's handle this anyway, who knows what the future
+ * brings? */
+ r = table_add_cell(table, NULL, TABLE_STRV, s->triggered);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = output_table(table);
+ if (r < 0)
+ return r;
+
+ if (arg_legend != 0)
+ output_legend("socket", cs);
+
+ return 0;
+}
+
+int verb_list_sockets(int argc, char *argv[], void *userdata) {
+ _cleanup_(message_set_freep) Set *replies = NULL;
+ _cleanup_strv_free_ char **machines = NULL;
+ _cleanup_strv_free_ char **sockets_with_suffix = NULL;
+ _cleanup_free_ UnitInfo *unit_infos = NULL;
+ _cleanup_free_ struct socket_info *socket_infos = NULL;
+ size_t cs = 0;
+ int r, n;
+ sd_bus *bus;
+
+ r = acquire_bus(BUS_MANAGER, &bus);
+ if (r < 0)
+ return r;
+
+ pager_open(arg_pager_flags);
+
+ r = expand_unit_names(bus, strv_skip(argv, 1), ".socket", &sockets_with_suffix, NULL);
+ if (r < 0)
+ return r;
+
+ if (argc == 1 || sockets_with_suffix) {
+ n = get_unit_list_recursive(bus, sockets_with_suffix, &unit_infos, &replies, &machines);
+ if (n < 0)
+ return n;
+
+ for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
+ _cleanup_strv_free_ char **listening = NULL, **triggered = NULL;
+ int c;
+
+ if (!endswith(u->id, ".socket"))
+ continue;
+
+ r = get_triggered_units(bus, u->unit_path, &triggered);
+ if (r < 0)
+ goto cleanup;
+
+ c = get_listening(bus, u->unit_path, &listening);
+ if (c < 0) {
+ r = c;
+ goto cleanup;
+ }
+
+ if (!GREEDY_REALLOC(socket_infos, cs + c)) {
+ r = log_oom();
+ goto cleanup;
+ }
+
+ for (int i = 0; i < c; i++)
+ socket_infos[cs + i] = (struct socket_info) {
+ .machine = u->machine,
+ .id = u->id,
+ .type = listening[i*2],
+ .path = listening[i*2 + 1],
+ .triggered = triggered,
+ .own_triggered = i==0,
+ };
+
+ /* from this point on we will cleanup those socket_infos */
+ cs += c;
+ free(listening);
+ listening = triggered = NULL; /* avoid cleanup */
+ }
+
+ typesafe_qsort(socket_infos, cs, socket_info_compare);
+ }
+
+ output_sockets_list(socket_infos, cs);
+
+ cleanup:
+ assert(cs == 0 || socket_infos);
+ for (struct socket_info *s = socket_infos; s < socket_infos + cs; s++) {
+ free(s->type);
+ free(s->path);
+ if (s->own_triggered)
+ strv_free(s->triggered);
+ }
+
+ return r;
+}
+
+static int get_next_elapse(
+ sd_bus *bus,
+ const char *path,
+ dual_timestamp *next) {
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ dual_timestamp t;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(next);
+
+ r = sd_bus_get_property_trivial(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Timer",
+ "NextElapseUSecMonotonic",
+ &error,
+ 't',
+ &t.monotonic);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get next elapse time: %s", bus_error_message(&error, r));
+
+ r = sd_bus_get_property_trivial(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Timer",
+ "NextElapseUSecRealtime",
+ &error,
+ 't',
+ &t.realtime);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get next elapse time: %s", bus_error_message(&error, r));
+
+ *next = t;
+ return 0;
+}
+
+static int get_last_trigger(
+ sd_bus *bus,
+ const char *path,
+ usec_t *last) {
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(last);
+
+ r = sd_bus_get_property_trivial(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Timer",
+ "LastTriggerUSec",
+ &error,
+ 't',
+ last);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get last trigger time: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+struct timer_info {
+ const char* machine;
+ const char* id;
+ usec_t next_elapse;
+ usec_t last_trigger;
+ char** triggered;
+};
+
+static int timer_info_compare(const struct timer_info *a, const struct timer_info *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ r = strcasecmp_ptr(a->machine, b->machine);
+ if (r != 0)
+ return r;
+
+ r = CMP(a->next_elapse, b->next_elapse);
+ if (r != 0)
+ return r;
+
+ return strcmp(a->id, b->id);
+}
+
+static int output_timers_list(struct timer_info *timer_infos, size_t n) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ assert(timer_infos || n == 0);
+
+ table = table_new("next", "left", "last", "passed", "unit", "activates");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, arg_legend != 0);
+ if (arg_full)
+ table_set_width(table, 0);
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ for (struct timer_info *t = timer_infos; t < timer_infos + n; t++) {
+ _cleanup_free_ char *j = NULL, *activates = NULL;
+ const char *unit;
+
+ if (t->machine) {
+ j = strjoin(t->machine, ":", t->id);
+ if (!j)
+ return log_oom();
+ unit = j;
+ } else
+ unit = t->id;
+
+ activates = strv_join(t->triggered, ", ");
+ if (!activates)
+ return log_oom();
+
+ r = table_add_many(table,
+ TABLE_TIMESTAMP, t->next_elapse,
+ TABLE_TIMESTAMP_RELATIVE, t->next_elapse,
+ TABLE_TIMESTAMP, t->last_trigger,
+ TABLE_TIMESTAMP_RELATIVE, t->last_trigger,
+ TABLE_STRING, unit,
+ TABLE_STRING, activates);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = output_table(table);
+ if (r < 0)
+ return r;
+
+ if (arg_legend != 0)
+ output_legend("timer", n);
+
+ return 0;
+}
+
+usec_t calc_next_elapse(dual_timestamp *nw, dual_timestamp *next) {
+ usec_t next_elapse;
+
+ assert(nw);
+ assert(next);
+
+ if (timestamp_is_set(next->monotonic)) {
+ usec_t converted;
+
+ if (next->monotonic > nw->monotonic)
+ converted = nw->realtime + (next->monotonic - nw->monotonic);
+ else
+ converted = nw->realtime - (nw->monotonic - next->monotonic);
+
+ if (timestamp_is_set(next->realtime))
+ next_elapse = MIN(converted, next->realtime);
+ else
+ next_elapse = converted;
+
+ } else
+ next_elapse = next->realtime;
+
+ return next_elapse;
+}
+
+int verb_list_timers(int argc, char *argv[], void *userdata) {
+ _cleanup_(message_set_freep) Set *replies = NULL;
+ _cleanup_strv_free_ char **machines = NULL;
+ _cleanup_strv_free_ char **timers_with_suffix = NULL;
+ _cleanup_free_ struct timer_info *timer_infos = NULL;
+ _cleanup_free_ UnitInfo *unit_infos = NULL;
+ dual_timestamp nw;
+ size_t c = 0;
+ sd_bus *bus;
+ int n, r;
+
+ r = acquire_bus(BUS_MANAGER, &bus);
+ if (r < 0)
+ return r;
+
+ pager_open(arg_pager_flags);
+
+ r = expand_unit_names(bus, strv_skip(argv, 1), ".timer", &timers_with_suffix, NULL);
+ if (r < 0)
+ return r;
+
+ if (argc == 1 || timers_with_suffix) {
+ n = get_unit_list_recursive(bus, timers_with_suffix, &unit_infos, &replies, &machines);
+ if (n < 0)
+ return n;
+
+ dual_timestamp_get(&nw);
+
+ for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
+ _cleanup_strv_free_ char **triggered = NULL;
+ dual_timestamp next = DUAL_TIMESTAMP_NULL;
+ usec_t m, last = 0;
+
+ if (!endswith(u->id, ".timer"))
+ continue;
+
+ r = get_triggered_units(bus, u->unit_path, &triggered);
+ if (r < 0)
+ goto cleanup;
+
+ r = get_next_elapse(bus, u->unit_path, &next);
+ if (r < 0)
+ goto cleanup;
+
+ get_last_trigger(bus, u->unit_path, &last);
+
+ if (!GREEDY_REALLOC(timer_infos, c+1)) {
+ r = log_oom();
+ goto cleanup;
+ }
+
+ m = calc_next_elapse(&nw, &next);
+
+ timer_infos[c++] = (struct timer_info) {
+ .machine = u->machine,
+ .id = u->id,
+ .next_elapse = m,
+ .last_trigger = last,
+ .triggered = TAKE_PTR(triggered),
+ };
+ }
+
+ typesafe_qsort(timer_infos, c, timer_info_compare);
+ }
+
+ output_timers_list(timer_infos, c);
+
+ cleanup:
+ for (struct timer_info *t = timer_infos; t < timer_infos + c; t++)
+ strv_free(t->triggered);
+
+ return r;
+}
+
+struct automount_info {
+ const char *machine;
+ const char *id;
+ char *what;
+ char *where;
+ usec_t timeout_idle_usec;
+ bool mounted;
+};
+
+static int automount_info_compare(const struct automount_info *a, const struct automount_info *b) {
+ int r;
+
+ assert(a);
+ assert(b);
+
+ r = strcasecmp_ptr(a->machine, b->machine);
+ if (r != 0)
+ return r;
+
+ return strcmp(a->where, b->where);
+}
+
+static int collect_automount_info(sd_bus* bus, const UnitInfo* info, struct automount_info *ret_info) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *mount = NULL, *mount_path = NULL, *where = NULL, *what = NULL, *state = NULL;
+ usec_t timeout_idle_usec;
+ BusLocator locator;
+ int r;
+
+ assert(bus);
+ assert(info);
+ assert(ret_info);
+
+ locator = (BusLocator) {
+ .destination = "org.freedesktop.systemd1",
+ .path = info->unit_path,
+ .interface = "org.freedesktop.systemd1.Automount",
+ };
+
+ r = bus_get_property_string(bus, &locator, "Where", &error, &where);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get automount target: %s", bus_error_message(&error, r));
+
+ r = bus_get_property_trivial(bus, &locator, "TimeoutIdleUSec", &error, 't', &timeout_idle_usec);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get idle timeout: %s", bus_error_message(&error, r));
+
+ r = unit_name_from_path(where, ".mount", &mount);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate unit name from path: %m");
+
+ mount_path = unit_dbus_path_from_name(mount);
+ if (!mount_path)
+ return log_oom();
+
+ locator.path = mount_path;
+ locator.interface = "org.freedesktop.systemd1.Mount";
+
+ r = bus_get_property_string(bus, &locator, "What", &error, &what);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get mount source: %s", bus_error_message(&error, r));
+
+ locator.interface = "org.freedesktop.systemd1.Unit";
+
+ r = bus_get_property_string(bus, &locator, "ActiveState", &error, &state);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get mount state: %s", bus_error_message(&error, r));
+
+ *ret_info = (struct automount_info) {
+ .machine = info->machine,
+ .id = info->id,
+ .what = TAKE_PTR(what),
+ .where = TAKE_PTR(where),
+ .timeout_idle_usec = timeout_idle_usec,
+ .mounted = streq_ptr(state, "active"),
+ };
+
+ return 0;
+}
+
+static int output_automounts_list(struct automount_info *infos, size_t n_infos) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ assert(infos || n_infos == 0);
+
+ table = table_new("what", "where", "mounted", "idle timeout", "unit");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, arg_legend != 0);
+ if (arg_full)
+ table_set_width(table, 0);
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+
+ for (struct automount_info *info = infos; info < infos + n_infos; info++) {
+ _cleanup_free_ char *j = NULL;
+ const char *unit;
+
+ if (info->machine) {
+ j = strjoin(info->machine, ":", info->id);
+ if (!j)
+ return log_oom();
+ unit = j;
+ } else
+ unit = info->id;
+
+ r = table_add_many(table,
+ TABLE_STRING, info->what,
+ TABLE_STRING, info->where,
+ TABLE_BOOLEAN, info->mounted,
+ TABLE_TIMESPAN_MSEC, info->timeout_idle_usec,
+ TABLE_STRING, unit);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = output_table(table);
+ if (r < 0)
+ return r;
+
+ if (arg_legend != 0)
+ output_legend("automount", n_infos);
+
+ return 0;
+}
+
+int verb_list_automounts(int argc, char *argv[], void *userdata) {
+ _cleanup_(message_set_freep) Set *replies = NULL;
+ _cleanup_strv_free_ char **machines = NULL, **automounts = NULL;
+ _cleanup_free_ UnitInfo *unit_infos = NULL;
+ _cleanup_free_ struct automount_info *automount_infos = NULL;
+ size_t c = 0;
+ int r, n;
+ sd_bus *bus;
+
+ r = acquire_bus(BUS_MANAGER, &bus);
+ if (r < 0)
+ return r;
+
+ pager_open(arg_pager_flags);
+
+ r = expand_unit_names(bus, strv_skip(argv, 1), ".automount", &automounts, NULL);
+ if (r < 0)
+ return r;
+
+ if (argc == 1 || automounts) {
+ n = get_unit_list_recursive(bus, automounts, &unit_infos, &replies, &machines);
+ if (n < 0)
+ return n;
+
+ for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
+ if (!endswith(u->id, ".automount"))
+ continue;
+
+ if (!GREEDY_REALLOC(automount_infos, c + 1)) {
+ r = log_oom();
+ goto cleanup;
+ }
+
+ r = collect_automount_info(bus, u, &automount_infos[c]);
+ if (r < 0)
+ goto cleanup;
+
+ c++;
+ }
+
+ typesafe_qsort(automount_infos, c, automount_info_compare);
+ }
+
+ output_automounts_list(automount_infos, c);
+
+ cleanup:
+ assert(c == 0 || automount_infos);
+ for (struct automount_info *info = automount_infos; info < automount_infos + c; info++) {
+ free(info->what);
+ free(info->where);
+ }
+
+ return r;
+}