]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/systemctl/systemctl-list-units.c
Merge pull request #26535 from yuwata/systemctl-list-cleanups
[thirdparty/systemd.git] / src / systemctl / systemctl-list-units.c
index e02a7608feef5d0a18a1d3c07fb6fab689058b73..c52eaec5547d1798d3b50ac93ebe18c4335940fa 100644 (file)
@@ -3,8 +3,10 @@
 #include "sd-login.h"
 
 #include "bus-error.h"
+#include "bus-locator.h"
 #include "format-table.h"
 #include "locale-util.h"
+#include "path-util.h"
 #include "set.h"
 #include "sort-util.h"
 #include "systemctl-list-units.h"
@@ -20,18 +22,16 @@ static int get_unit_list_recursive(
                 sd_bus *bus,
                 char **patterns,
                 UnitInfo **ret_unit_infos,
-                Set **ret_replies,
-                char ***ret_machines) {
+                Set **ret_replies) {
 
         _cleanup_free_ UnitInfo *unit_infos = NULL;
-        _cleanup_(message_set_freep) Set *replies;
-        sd_bus_message *reply;
+        _cleanup_(message_set_freep) Set *replies = NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
         int c, r;
 
         assert(bus);
         assert(ret_replies);
         assert(ret_unit_infos);
-        assert(ret_machines);
 
         replies = set_new(NULL);
         if (!replies)
@@ -42,14 +42,12 @@ static int get_unit_list_recursive(
                 return c;
 
         r = set_put(replies, reply);
-        if (r < 0) {
-                sd_bus_message_unref(reply);
+        if (r < 0)
                 return log_oom();
-        }
+        TAKE_PTR(reply);
 
         if (arg_recursive) {
                 _cleanup_strv_free_ char **machines = NULL;
-                char **i;
 
                 r = sd_get_machine_names(&machines);
                 if (r < 0)
@@ -72,15 +70,11 @@ static int get_unit_list_recursive(
                         c = k;
 
                         r = set_put(replies, reply);
-                        if (r < 0) {
-                                sd_bus_message_unref(reply);
+                        if (r < 0)
                                 return log_oom();
-                        }
+                        TAKE_PTR(reply);
                 }
-
-                *ret_machines = TAKE_PTR(machines);
-        } else
-                *ret_machines = NULL;
+        }
 
         *ret_unit_infos = TAKE_PTR(unit_infos);
         *ret_replies = TAKE_PTR(replies);
@@ -88,9 +82,42 @@ static int get_unit_list_recursive(
         return c;
 }
 
-static int output_units_list(const UnitInfo *unit_infos, unsigned 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 table_add_triggered(Table *table, char **triggered) {
+        assert(table);
+
+        if (strv_isempty(triggered))
+                return table_add_cell(table, NULL, TABLE_EMPTY, NULL);
+        else if (strv_length(triggered) == 1)
+                return table_add_cell(table, NULL, TABLE_STRING, 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? */
+                return table_add_cell(table, NULL, TABLE_STRV, triggered);
+}
+
+static char *format_unit_id(const char *unit, const char *machine) {
+        assert(unit);
+
+        return machine ? strjoin(machine, ":", unit) : strdup(unit);
+}
+
+static int output_units_list(const UnitInfo *unit_infos, size_t c) {
         _cleanup_(table_unrefp) Table *table = NULL;
-        unsigned job_count = 0;
+        size_t job_count = 0;
         int r;
 
         table = table_new("", "unit", "load", "active", "sub", "job", "description");
@@ -107,12 +134,11 @@ static int output_units_list(const UnitInfo *unit_infos, unsigned c) {
         if (arg_full)
                 table_set_width(table, 0);
 
-        (void) table_set_empty_string(table, "-");
+        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;
+                _cleanup_free_ char *id = NULL;
+                const char *on_underline = "", *on_loaded = "", *on_active = "", *on_circle = "";
                 bool circle = false, underline = false;
 
                 if (u + 1 < unit_infos + c &&
@@ -135,14 +161,9 @@ static int output_units_list(const UnitInfo *unit_infos, unsigned c) {
                         on_loaded = on_underline;
                 }
 
-                if (u->machine) {
-                        j = strjoin(u->machine, ":", u->id);
-                        if (!j)
-                                return log_oom();
-
-                        id = j;
-                } else
-                        id = u->id;
+                id = format_unit_id(u->id, u->machine);
+                if (!id)
+                        return log_oom();
 
                 r = table_add_many(table,
                                    TABLE_STRING, circle ? special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE) : " ",
@@ -188,13 +209,11 @@ static int output_units_list(const UnitInfo *unit_infos, unsigned c) {
                              "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 = ansi_highlight();
-                        off = ansi_normal();
-                } else {
-                        on = ansi_highlight_red();
-                        off = ansi_normal();
                 }
 
+                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"
                                "To show all installed unit files use 'systemctl list-unit-files'.\n",
@@ -210,10 +229,9 @@ static int output_units_list(const UnitInfo *unit_infos, unsigned c) {
         return 0;
 }
 
-int list_units(int argc, char *argv[], void *userdata) {
+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;
 
@@ -221,7 +239,7 @@ int list_units(int argc, char *argv[], void *userdata) {
         if (r < 0)
                 return r;
 
-        (void) pager_open(arg_pager_flags);
+        pager_open(arg_pager_flags);
 
         if (arg_with_dependencies) {
                 _cleanup_strv_free_ char **names = NULL;
@@ -230,11 +248,11 @@ int list_units(int argc, char *argv[], void *userdata) {
                 if (r < 0)
                         return r;
 
-                r = get_unit_list_recursive(bus, names, &unit_infos, &replies, &machines);
+                r = get_unit_list_recursive(bus, names, &unit_infos, &replies);
                 if (r < 0)
                         return r;
         } else {
-                r = get_unit_list_recursive(bus, strv_skip(argv, 1), &unit_infos, &replies, &machines);
+                r = get_unit_list_recursive(bus, strv_skip(argv, 1), &unit_infos, &replies);
                 if (r < 0)
                         return r;
         }
@@ -269,20 +287,79 @@ static int get_triggered_units(
         return 0;
 }
 
-static int get_listening(
+typedef struct SocketInfo {
+        const char *machine;
+        const char* id;
+
+        char* type;
+        char* path; /* absolute path or socket address */
+
+        /* 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;
+} SocketInfo;
+
+static void socket_info_array_free(SocketInfo *sockets, size_t n_sockets) {
+        assert(sockets || n_sockets == 0);
+
+        for (SocketInfo *s = sockets; s < sockets + n_sockets; s++) {
+                free(s->type);
+                free(s->path);
+                strv_free(s->triggered);
+        }
+
+        free(sockets);
+}
+
+static int socket_info_compare(const SocketInfo *a, const SocketInfo *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        r = strcasecmp_ptr(a->machine, b->machine);
+        if (r != 0)
+                return r;
+
+        r = CMP(path_is_absolute(a->path), path_is_absolute(b->path));
+        if (r != 0)
+                return r;
+
+        r = path_is_absolute(a->path) ? path_compare(a->path, b->path) : strcmp(a->path, b->path);
+        if (r != 0)
+                return r;
+
+        return strcmp(a->type, b->type);
+}
+
+static int socket_info_add(
                 sd_bus *bus,
-                const char* unit_path,
-                char*** listening) {
+                const UnitInfo *u,
+                SocketInfo **sockets,
+                size_t *n_sockets) {
 
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_strv_free_ char **triggered = NULL;
         const char *type, *path;
-        int r, n = 0;
+        int r;
+
+        assert(bus);
+        assert(u);
+        assert(sockets);
+        assert(n_sockets);
+
+        if (!endswith(u->id, ".socket"))
+                return 0;
+
+        r = get_triggered_units(bus, u->unit_path, &triggered);
+        if (r < 0)
+                return r;
 
         r = sd_bus_get_property(
                         bus,
                         "org.freedesktop.systemd1",
-                        unit_path,
+                        u->unit_path,
                         "org.freedesktop.systemd1.Socket",
                         "Listen",
                         &error,
@@ -296,16 +373,31 @@ static int get_listening(
                 return bus_log_parse_error(r);
 
         while ((r = sd_bus_message_read(reply, "(ss)", &type, &path)) > 0) {
+                _cleanup_free_ char *type_dup = NULL, *path_dup = NULL;
+                _cleanup_strv_free_ char **triggered_dup = NULL;
 
-                r = strv_extend(listening, type);
-                if (r < 0)
+                type_dup = strdup(type);
+                if (!type_dup)
                         return log_oom();
 
-                r = strv_extend(listening, path);
-                if (r < 0)
+                path_dup = strdup(path);
+                if (!path_dup)
                         return log_oom();
 
-                n++;
+                triggered_dup = strv_copy(triggered);
+                if (!triggered_dup)
+                        return log_oom();
+
+                if (!GREEDY_REALLOC(*sockets, *n_sockets + 1))
+                        return log_oom();
+
+                (*sockets)[(*n_sockets)++] = (SocketInfo) {
+                        .machine = u->machine,
+                        .id = u->id,
+                        .type = TAKE_PTR(type_dup),
+                        .path = TAKE_PTR(path_dup),
+                        .triggered = TAKE_PTR(triggered_dup),
+                };
         }
         if (r < 0)
                 return bus_log_parse_error(r);
@@ -314,46 +406,15 @@ static int get_listening(
         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);
+        return 0;
 }
 
-static int output_sockets_list(struct socket_info *socket_infos, unsigned cs) {
+static int output_sockets_list(const SocketInfo *sockets, size_t n_sockets) {
         _cleanup_(table_unrefp) Table *table = NULL;
-        const char *on, *off;
         int r;
 
+        assert(sockets || n_sockets == 0);
+
         table = table_new("listen", "type", "unit", "activates");
         if (!table)
                 return log_oom();
@@ -369,141 +430,76 @@ static int output_sockets_list(struct socket_info *socket_infos, unsigned cs) {
         if (arg_full)
                 table_set_width(table, 0);
 
-        (void) table_set_empty_string(table, "-");
-
-        if (cs) {
-                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);
+        table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
 
-                }
+        for (const SocketInfo *s = sockets; s < sockets + n_sockets; s++) {
+                _cleanup_free_ char *unit = NULL;
 
-                on = ansi_highlight();
-                off = ansi_normal();
-        } else {
-                on = ansi_highlight_red();
-                off = ansi_normal();
+                unit = format_unit_id(s->id, s->machine);
+                if (!unit)
+                        return log_oom();
+
+                r = table_add_many(table,
+                                   TABLE_STRING, s->path,
+                                   TABLE_STRING, s->type,
+                                   TABLE_STRING, unit);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                r = table_add_triggered(table, s->triggered);
+                if (r < 0)
+                        return table_log_add_error(r);
         }
 
         r = output_table(table);
         if (r < 0)
                 return r;
 
-        if (arg_legend != 0) {
-                printf("\n%s%u sockets listed.%s\n", on, cs, off);
-                if (!arg_all)
-                        printf("Pass --all to see loaded but inactive sockets, too.\n");
-        }
+        if (arg_legend != 0)
+                output_legend("socket", n_sockets);
 
         return 0;
 }
 
-int list_sockets(int argc, char *argv[], void *userdata) {
+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;
-        unsigned cs = 0;
-        size_t size = 0;
-        int r, n;
+        SocketInfo *sockets = NULL;
+        size_t n_sockets = 0;
         sd_bus *bus;
+        int r;
+
+        CLEANUP_ARRAY(sockets, n_sockets, socket_info_array_free);
 
         r = acquire_bus(BUS_MANAGER, &bus);
         if (r < 0)
                 return r;
 
-        (void) pager_open(arg_pager_flags);
+        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);
+                int n;
+
+                n = get_unit_list_recursive(bus, sockets_with_suffix, &unit_infos, &replies);
                 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);
+                        r = socket_info_add(bus, u, &sockets, &n_sockets);
                         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, size, 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 */
+                                return r;
                 }
-
-                typesafe_qsort(socket_infos, cs, socket_info_compare);
         }
 
-        output_sockets_list(socket_infos, cs);
+        typesafe_qsort(sockets, n_sockets, socket_info_compare);
+        output_sockets_list(sockets, n_sockets);
 
- 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;
+        return 0;
 }
 
 static int get_next_elapse(
@@ -574,15 +570,24 @@ static int get_last_trigger(
         return 0;
 }
 
-struct timer_info {
+typedef struct TimerInfo {
         const char* machine;
         const char* id;
         usec_t next_elapse;
         usec_t last_trigger;
-        char** triggered;
-};
+        char **triggered;
+} TimerInfo;
+
+static void timer_info_array_free(TimerInfo *timers, size_t n_timers) {
+        assert(timers || n_timers == 0);
+
+        for (TimerInfo *t = timers; t < timers + n_timers; t++)
+                strv_free(t->triggered);
+
+        free(timers);
+}
 
-static int timer_info_compare(const struct timer_info *a, const struct timer_info *b) {
+static int timer_info_compare(const TimerInfo *a, const TimerInfo *b) {
         int r;
 
         assert(a);
@@ -599,12 +604,11 @@ static int timer_info_compare(const struct timer_info *a, const struct timer_inf
         return strcmp(a->id, b->id);
 }
 
-static int output_timers_list(struct timer_info *timer_infos, unsigned n) {
+static int output_timers_list(const TimerInfo *timers, size_t n_timers) {
         _cleanup_(table_unrefp) Table *table = NULL;
-        const char *on, *off;
         int r;
 
-        assert(timer_infos || n == 0);
+        assert(timers || n_timers == 0);
 
         table = table_new("next", "left", "last", "passed", "unit", "activates");
         if (!table)
@@ -614,57 +618,43 @@ static int output_timers_list(struct timer_info *timer_infos, unsigned n) {
         if (arg_full)
                 table_set_width(table, 0);
 
-        (void) table_set_empty_string(table, "-");
+        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;
+        (void) table_set_align_percent(table, table_get_cell(table, 0, 1), 100);
+        (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
 
-                if (t->machine) {
-                        j = strjoin(t->machine, ":", t->id);
-                        if (!j)
-                                return log_oom();
-                        unit = j;
-                } else
-                        unit = t->id;
+        for (const TimerInfo *t = timers; t < timers + n_timers; t++) {
+                _cleanup_free_ char *unit = NULL;
 
-                activates = strv_join(t->triggered, ", ");
-                if (!activates)
+                unit = format_unit_id(t->id, t->machine);
+                if (!unit)
                         return log_oom();
 
                 r = table_add_many(table,
                                    TABLE_TIMESTAMP, t->next_elapse,
-                                   TABLE_TIMESTAMP_RELATIVE, t->next_elapse,
+                                   TABLE_TIMESTAMP_LEFT, t->next_elapse,
                                    TABLE_TIMESTAMP, t->last_trigger,
                                    TABLE_TIMESTAMP_RELATIVE, t->last_trigger,
-                                   TABLE_STRING, unit,
-                                   TABLE_STRING, activates);
+                                   TABLE_STRING, unit);
                 if (r < 0)
                         return table_log_add_error(r);
-        }
 
-        if (n > 0) {
-                on = ansi_highlight();
-                off = ansi_normal();
-        } else {
-                on = ansi_highlight_red();
-                off = ansi_normal();
+                r = table_add_triggered(table, t->triggered);
+                if (r < 0)
+                        return table_log_add_error(r);
         }
 
         r = output_table(table);
         if (r < 0)
                 return r;
 
-        if (arg_legend != 0) {
-                printf("\n%s%u timers listed.%s\n", on, n, off);
-                if (!arg_all)
-                        printf("Pass --all to see loaded but inactive timers, too.\n");
-        }
+        if (arg_legend != 0)
+                output_legend("timer", n_timers);
 
         return 0;
 }
 
-usec_t calc_next_elapse(dual_timestamp *nw, dual_timestamp *next) {
+usec_t calc_next_elapse(const dual_timestamp *nw, const dual_timestamp *next) {
         usec_t next_elapse;
 
         assert(nw);
@@ -689,77 +679,499 @@ usec_t calc_next_elapse(dual_timestamp *nw, dual_timestamp *next) {
         return next_elapse;
 }
 
-int list_timers(int argc, char *argv[], void *userdata) {
+static int add_timer_info(
+                sd_bus *bus,
+                const UnitInfo *u,
+                const dual_timestamp *nw,
+                TimerInfo **timers,
+                size_t *n_timers) {
+
+        _cleanup_strv_free_ char **triggered = NULL;
+        dual_timestamp next = DUAL_TIMESTAMP_NULL;
+        usec_t m, last = 0;
+        int r;
+
+        assert(bus);
+        assert(u);
+        assert(nw);
+        assert(timers);
+        assert(n_timers);
+
+        if (!endswith(u->id, ".timer"))
+                return 0;
+
+        r = get_triggered_units(bus, u->unit_path, &triggered);
+        if (r < 0)
+                return r;
+
+        r = get_next_elapse(bus, u->unit_path, &next);
+        if (r < 0)
+                return r;
+
+        r = get_last_trigger(bus, u->unit_path, &last);
+        if (r < 0)
+                return r;
+
+        m = calc_next_elapse(nw, &next);
+
+        if (!GREEDY_REALLOC(*timers, *n_timers + 1))
+                return log_oom();
+
+        (*timers)[(*n_timers)++] = (TimerInfo) {
+                .machine = u->machine,
+                .id = u->id,
+                .next_elapse = m,
+                .last_trigger = last,
+                .triggered = TAKE_PTR(triggered),
+        };
+
+        return 0;
+}
+
+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;
-        size_t size = 0;
-        int n, c = 0;
-        dual_timestamp nw;
+        TimerInfo *timers = NULL;
+        size_t n_timers = 0;
         sd_bus *bus;
         int r;
 
+        CLEANUP_ARRAY(timers, n_timers, timer_info_array_free);
+
         r = acquire_bus(BUS_MANAGER, &bus);
         if (r < 0)
                 return r;
 
-        (void) pager_open(arg_pager_flags);
+        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);
+                dual_timestamp nw;
+                int n;
+
+                n = get_unit_list_recursive(bus, timers_with_suffix, &unit_infos, &replies);
                 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;
+                        r = add_timer_info(bus, u, &nw, &timers, &n_timers);
+                        if (r < 0)
+                                return r;
+                }
+        }
 
-                        if (!endswith(u->id, ".timer"))
-                                continue;
+        typesafe_qsort(timers, n_timers, timer_info_compare);
+        output_timers_list(timers, n_timers);
 
-                        r = get_triggered_units(bus, u->unit_path, &triggered);
-                        if (r < 0)
-                                goto cleanup;
+        return 0;
+}
 
-                        r = get_next_elapse(bus, u->unit_path, &next);
-                        if (r < 0)
-                                goto cleanup;
+typedef struct AutomountInfo {
+        const char *machine;
+        const char *id;
+        char *what;
+        char *where;
+        usec_t timeout_idle_usec;
+        bool mounted;
+} AutomountInfo;
+
+static void automount_info_array_free(AutomountInfo *automounts, size_t n_automounts) {
+        assert(automounts || n_automounts == 0);
+
+        for (AutomountInfo *i = automounts; i < automounts + n_automounts; i++) {
+                free(i->what);
+                free(i->where);
+        }
 
-                        get_last_trigger(bus, u->unit_path, &last);
+        free(automounts);
+}
 
-                        if (!GREEDY_REALLOC(timer_infos, size, c+1)) {
-                                r = log_oom();
-                                goto cleanup;
-                        }
+static int automount_info_compare(const AutomountInfo *a, const AutomountInfo *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        r = strcasecmp_ptr(a->machine, b->machine);
+        if (r != 0)
+                return r;
+
+        return path_compare(a->where, b->where);
+}
+
+static int automount_info_add(
+                sd_bus* bus,
+                const UnitInfo *info,
+                AutomountInfo **automounts,
+                size_t *n_automounts) {
+
+        _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;
+        uint64_t timeout_idle_usec;
+        BusLocator locator;
+        int r;
+
+        assert(bus);
+        assert(info);
+        assert(automounts);
+        assert(n_automounts);
+
+        if (!endswith(info->id, ".automount"))
+                return 0;
+
+        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));
+
+        if (!GREEDY_REALLOC(*automounts, *n_automounts + 1))
+                return log_oom();
+
+        (*automounts)[(*n_automounts)++] = (AutomountInfo) {
+                .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(const AutomountInfo *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 (const AutomountInfo *info = infos; info < infos + n_infos; info++) {
+                _cleanup_free_ char *unit = NULL;
+
+                unit = format_unit_id(info->id, info->machine);
+                if (!unit)
+                        return log_oom();
+
+                r = table_add_many(table,
+                                   TABLE_STRING, info->what,
+                                   TABLE_STRING, info->where,
+                                   TABLE_BOOLEAN, info->mounted);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                if (timestamp_is_set(info->timeout_idle_usec))
+                        r = table_add_cell(table, NULL, TABLE_TIMESPAN_MSEC, &info->timeout_idle_usec);
+                else
+                        r = table_add_cell(table, NULL, TABLE_EMPTY, NULL);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                r = table_add_cell(table, NULL, 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 **names = NULL;
+        _cleanup_free_ UnitInfo *unit_infos = NULL;
+        AutomountInfo *automounts = NULL;
+        size_t n_automounts = 0;
+        sd_bus *bus;
+        int r;
+
+        CLEANUP_ARRAY(automounts, n_automounts, automount_info_array_free);
 
-                        m = calc_next_elapse(&nw, &next);
+        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", &names, NULL);
+        if (r < 0)
+                return r;
 
-                        timer_infos[c++] = (struct timer_info) {
-                                .machine = u->machine,
-                                .id = u->id,
-                                .next_elapse = m,
-                                .last_trigger = last,
-                                .triggered = TAKE_PTR(triggered),
-                        };
+        if (argc == 1 || automounts) {
+                int n;
+
+                n = get_unit_list_recursive(bus, names, &unit_infos, &replies);
+                if (n < 0)
+                        return n;
+
+                for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
+                        r = automount_info_add(bus, u, &automounts, &n_automounts);
+                        if (r < 0)
+                                return r;
                 }
 
-                typesafe_qsort(timer_infos, c, timer_info_compare);
         }
 
-        output_timers_list(timer_infos, c);
+        typesafe_qsort(automounts, n_automounts, automount_info_compare);
+        output_automounts_list(automounts, n_automounts);
 
- cleanup:
-        for (struct timer_info *t = timer_infos; t < timer_infos + c; t++)
-                strv_free(t->triggered);
+        return 0;
+}
+
+typedef struct PathInfo {
+        const char *machine;
+        const char *id;
+
+        char *path;
+        char *condition;
+
+        /* 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;
+} PathInfo;
+
+static int path_info_compare(const PathInfo *a, const PathInfo *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        r = strcasecmp_ptr(a->machine, b->machine);
+        if (r != 0)
+                return r;
+
+        r = path_compare(a->path, b->path);
+        if (r != 0)
+                return r;
+
+        r = strcmp(a->condition, b->condition);
+        if (r != 0)
+                return r;
 
-        return r;
+        return strcasecmp_ptr(a->id, b->id);
+}
+
+static void path_info_array_free(PathInfo *paths, size_t n_paths) {
+        assert(paths || n_paths == 0);
+
+        for (PathInfo *p = paths; p < paths + n_paths; p++) {
+                free(p->condition);
+                free(p->path);
+                strv_free(p->triggered);
+        }
+
+        free(paths);
+}
+
+static int path_info_add(
+                sd_bus *bus,
+                const struct UnitInfo *u,
+                PathInfo **paths,
+                size_t *n_paths) {
+
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_strv_free_ char **triggered = NULL;
+        const char *condition, *path;
+        int r;
+
+        assert(bus);
+        assert(u);
+        assert(paths);
+        assert(n_paths);
+
+        if (!endswith(u->id, ".path"))
+                return 0;
+
+        r = get_triggered_units(bus, u->unit_path, &triggered);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_get_property(bus,
+                                "org.freedesktop.systemd1",
+                                u->unit_path,
+                                "org.freedesktop.systemd1.Path",
+                                "Paths",
+                                &error,
+                                &reply,
+                                "a(ss)");
+        if (r < 0)
+                return log_error_errno(r, "Failed to get paths: %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)", &condition, &path)) > 0) {
+                _cleanup_free_ char *condition_dup = NULL, *path_dup = NULL;
+                _cleanup_strv_free_ char **triggered_dup = NULL;
+
+                condition_dup = strdup(condition);
+                if (!condition_dup)
+                        return log_oom();
+
+                path_dup = strdup(path);
+                if (!path_dup)
+                        return log_oom();
+
+                triggered_dup = strv_copy(triggered);
+                if (!triggered_dup)
+                        return log_oom();
+
+                if (!GREEDY_REALLOC(*paths, *n_paths + 1))
+                        return log_oom();
+
+                (*paths)[(*n_paths)++] = (PathInfo) {
+                        .machine = u->machine,
+                        .id = u->id,
+                        .condition = TAKE_PTR(condition_dup),
+                        .path = TAKE_PTR(path_dup),
+                        .triggered = TAKE_PTR(triggered_dup),
+                };
+        }
+        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 0;
+}
+
+static int output_paths_list(const PathInfo *paths, size_t n_paths) {
+        _cleanup_(table_unrefp) Table *table = NULL;
+        int r;
+
+        assert(paths || n_paths == 0);
+
+        table = table_new("path", "condition", "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 (const PathInfo *p = paths; p < paths + n_paths; p++) {
+                _cleanup_free_ char *unit = NULL;
+
+                unit = format_unit_id(p->id, p->machine);
+                if (!unit)
+                        return log_oom();
+
+                r = table_add_many(table,
+                                   TABLE_STRING, p->path,
+                                   TABLE_STRING, p->condition,
+                                   TABLE_STRING, unit);
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                r = table_add_triggered(table, p->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("path", n_paths);
+
+        return 0;
+}
+
+int verb_list_paths(int argc, char *argv[], void *userdata) {
+        _cleanup_(message_set_freep) Set *replies = NULL;
+        _cleanup_strv_free_ char **units = NULL;
+        _cleanup_free_ UnitInfo *unit_infos = NULL;
+        PathInfo *paths = NULL;
+        size_t n_paths = 0;
+        sd_bus *bus;
+        int r;
+
+        CLEANUP_ARRAY(paths, n_paths, path_info_array_free);
+
+        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), ".path", &units, NULL);
+        if (r < 0)
+                return r;
+
+        if (argc == 1 || units) {
+                int n;
+
+                n = get_unit_list_recursive(bus, units, &unit_infos, &replies);
+                if (n < 0)
+                        return n;
+
+                for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
+                        r = path_info_add(bus, u, &paths, &n_paths);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        typesafe_qsort(paths, n_paths, path_info_compare);
+        output_paths_list(paths, n_paths);
+
+        return 0;
 }