]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/systemctl/systemctl-list-units.c
systemctl: extract output of legend to a function
[thirdparty/systemd.git] / src / systemctl / systemctl-list-units.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include "sd-login.h"
4
5 #include "bus-error.h"
6 #include "format-table.h"
7 #include "locale-util.h"
8 #include "set.h"
9 #include "sort-util.h"
10 #include "systemctl-list-units.h"
11 #include "systemctl-util.h"
12 #include "systemctl.h"
13 #include "terminal-util.h"
14
15 static void message_set_freep(Set **set) {
16 set_free_with_destructor(*set, sd_bus_message_unref);
17 }
18
19 static int get_unit_list_recursive(
20 sd_bus *bus,
21 char **patterns,
22 UnitInfo **ret_unit_infos,
23 Set **ret_replies,
24 char ***ret_machines) {
25
26 _cleanup_free_ UnitInfo *unit_infos = NULL;
27 _cleanup_(message_set_freep) Set *replies = NULL;
28 sd_bus_message *reply;
29 int c, r;
30
31 assert(bus);
32 assert(ret_replies);
33 assert(ret_unit_infos);
34 assert(ret_machines);
35
36 replies = set_new(NULL);
37 if (!replies)
38 return log_oom();
39
40 c = get_unit_list(bus, NULL, patterns, &unit_infos, 0, &reply);
41 if (c < 0)
42 return c;
43
44 r = set_put(replies, reply);
45 if (r < 0) {
46 sd_bus_message_unref(reply);
47 return log_oom();
48 }
49
50 if (arg_recursive) {
51 _cleanup_strv_free_ char **machines = NULL;
52
53 r = sd_get_machine_names(&machines);
54 if (r < 0)
55 return log_error_errno(r, "Failed to get machine names: %m");
56
57 STRV_FOREACH(i, machines) {
58 _cleanup_(sd_bus_flush_close_unrefp) sd_bus *container = NULL;
59 int k;
60
61 r = sd_bus_open_system_machine(&container, *i);
62 if (r < 0) {
63 log_warning_errno(r, "Failed to connect to container %s, ignoring: %m", *i);
64 continue;
65 }
66
67 k = get_unit_list(container, *i, patterns, &unit_infos, c, &reply);
68 if (k < 0)
69 return k;
70
71 c = k;
72
73 r = set_put(replies, reply);
74 if (r < 0) {
75 sd_bus_message_unref(reply);
76 return log_oom();
77 }
78 }
79
80 *ret_machines = TAKE_PTR(machines);
81 } else
82 *ret_machines = NULL;
83
84 *ret_unit_infos = TAKE_PTR(unit_infos);
85 *ret_replies = TAKE_PTR(replies);
86
87 return c;
88 }
89
90 static void output_legend(const char *type, size_t n_items) {
91 const char *on, *off;
92
93 assert(type);
94
95 on = n_items > 0 ? ansi_highlight() : ansi_highlight_red();
96 off = ansi_normal();
97
98 printf("\n%s%zu %ss listed.%s\n", on, n_items, type, off);
99 if (!arg_all)
100 printf("Pass --all to see loaded but inactive %ss, too.\n", type);
101 }
102
103 static int output_units_list(const UnitInfo *unit_infos, size_t c) {
104 _cleanup_(table_unrefp) Table *table = NULL;
105 size_t job_count = 0;
106 int r;
107
108 table = table_new("", "unit", "load", "active", "sub", "job", "description");
109 if (!table)
110 return log_oom();
111
112 table_set_header(table, arg_legend != 0);
113 if (arg_plain) {
114 /* Hide the 'glyph' column when --plain is requested */
115 r = table_hide_column_from_display(table, 0);
116 if (r < 0)
117 return log_error_errno(r, "Failed to hide column: %m");
118 }
119 if (arg_full)
120 table_set_width(table, 0);
121
122 (void) table_set_empty_string(table, "-");
123
124 for (const UnitInfo *u = unit_infos; unit_infos && (size_t) (u - unit_infos) < c; u++) {
125 _cleanup_free_ char *j = NULL;
126 const char *on_underline = "", *on_loaded = "", *on_active = "";
127 const char *on_circle = "", *id;
128 bool circle = false, underline = false;
129
130 if (u + 1 < unit_infos + c &&
131 !streq(unit_type_suffix(u->id), unit_type_suffix((u + 1)->id))) {
132 on_underline = ansi_underline();
133 underline = true;
134 }
135
136 if (STR_IN_SET(u->load_state, "error", "not-found", "bad-setting", "masked") && !arg_plain) {
137 on_circle = underline ? ansi_highlight_yellow_underline() : ansi_highlight_yellow();
138 circle = true;
139 on_loaded = underline ? ansi_highlight_red_underline() : ansi_highlight_red();
140 } else if (streq(u->active_state, "failed") && !arg_plain) {
141 on_circle = underline ? ansi_highlight_red_underline() : ansi_highlight_red();
142 circle = true;
143 on_active = underline ? ansi_highlight_red_underline() : ansi_highlight_red();
144 } else {
145 on_circle = on_underline;
146 on_active = on_underline;
147 on_loaded = on_underline;
148 }
149
150 if (u->machine) {
151 j = strjoin(u->machine, ":", u->id);
152 if (!j)
153 return log_oom();
154
155 id = j;
156 } else
157 id = u->id;
158
159 r = table_add_many(table,
160 TABLE_STRING, circle ? special_glyph(SPECIAL_GLYPH_BLACK_CIRCLE) : " ",
161 TABLE_SET_BOTH_COLORS, on_circle,
162 TABLE_STRING, id,
163 TABLE_SET_BOTH_COLORS, on_active,
164 TABLE_STRING, u->load_state,
165 TABLE_SET_BOTH_COLORS, on_loaded,
166 TABLE_STRING, u->active_state,
167 TABLE_SET_BOTH_COLORS, on_active,
168 TABLE_STRING, u->sub_state,
169 TABLE_SET_BOTH_COLORS, on_active,
170 TABLE_STRING, u->job_id ? u->job_type: "",
171 TABLE_SET_BOTH_COLORS, on_underline,
172 TABLE_STRING, u->description,
173 TABLE_SET_BOTH_COLORS, on_underline);
174 if (r < 0)
175 return table_log_add_error(r);
176
177 if (u->job_id != 0)
178 job_count++;
179 }
180
181 if (job_count == 0) {
182 /* There's no data in the JOB column, so let's hide it */
183 r = table_hide_column_from_display(table, 5);
184 if (r < 0)
185 return log_error_errno(r, "Failed to hide column: %m");
186 }
187
188 r = output_table(table);
189 if (r < 0)
190 return r;
191
192 if (arg_legend != 0) {
193 const char *on, *off;
194 size_t records = table_get_rows(table) - 1;
195
196 if (records > 0) {
197 puts("\n"
198 "LOAD = Reflects whether the unit definition was properly loaded.\n"
199 "ACTIVE = The high-level unit activation state, i.e. generalization of SUB.\n"
200 "SUB = The low-level unit activation state, values depend on unit type.");
201 if (job_count > 0)
202 puts("JOB = Pending job for the unit.\n");
203 on = ansi_highlight();
204 off = ansi_normal();
205 } else {
206 on = ansi_highlight_red();
207 off = ansi_normal();
208 }
209
210 if (arg_all || strv_contains(arg_states, "inactive"))
211 printf("%s%zu loaded units listed.%s\n"
212 "To show all installed unit files use 'systemctl list-unit-files'.\n",
213 on, records, off);
214 else if (!arg_states)
215 printf("%s%zu loaded units listed.%s Pass --all to see loaded but inactive units, too.\n"
216 "To show all installed unit files use 'systemctl list-unit-files'.\n",
217 on, records, off);
218 else
219 printf("%zu loaded units listed.\n", records);
220 }
221
222 return 0;
223 }
224
225 int verb_list_units(int argc, char *argv[], void *userdata) {
226 _cleanup_free_ UnitInfo *unit_infos = NULL;
227 _cleanup_(message_set_freep) Set *replies = NULL;
228 _cleanup_strv_free_ char **machines = NULL;
229 sd_bus *bus;
230 int r;
231
232 r = acquire_bus(BUS_MANAGER, &bus);
233 if (r < 0)
234 return r;
235
236 pager_open(arg_pager_flags);
237
238 if (arg_with_dependencies) {
239 _cleanup_strv_free_ char **names = NULL;
240
241 r = append_unit_dependencies(bus, strv_skip(argv, 1), &names);
242 if (r < 0)
243 return r;
244
245 r = get_unit_list_recursive(bus, names, &unit_infos, &replies, &machines);
246 if (r < 0)
247 return r;
248 } else {
249 r = get_unit_list_recursive(bus, strv_skip(argv, 1), &unit_infos, &replies, &machines);
250 if (r < 0)
251 return r;
252 }
253
254 typesafe_qsort(unit_infos, r, unit_info_compare);
255 return output_units_list(unit_infos, r);
256 }
257
258 static int get_triggered_units(
259 sd_bus *bus,
260 const char* path,
261 char*** ret) {
262
263 _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
264 int r;
265
266 assert(bus);
267 assert(path);
268 assert(ret);
269
270 r = sd_bus_get_property_strv(
271 bus,
272 "org.freedesktop.systemd1",
273 path,
274 "org.freedesktop.systemd1.Unit",
275 "Triggers",
276 &error,
277 ret);
278 if (r < 0)
279 return log_error_errno(r, "Failed to determine triggers: %s", bus_error_message(&error, r));
280
281 return 0;
282 }
283
284 static int get_listening(
285 sd_bus *bus,
286 const char* unit_path,
287 char*** listening) {
288
289 _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
290 _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
291 const char *type, *path;
292 int r, n = 0;
293
294 r = sd_bus_get_property(
295 bus,
296 "org.freedesktop.systemd1",
297 unit_path,
298 "org.freedesktop.systemd1.Socket",
299 "Listen",
300 &error,
301 &reply,
302 "a(ss)");
303 if (r < 0)
304 return log_error_errno(r, "Failed to get list of listening sockets: %s", bus_error_message(&error, r));
305
306 r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ss)");
307 if (r < 0)
308 return bus_log_parse_error(r);
309
310 while ((r = sd_bus_message_read(reply, "(ss)", &type, &path)) > 0) {
311
312 r = strv_extend(listening, type);
313 if (r < 0)
314 return log_oom();
315
316 r = strv_extend(listening, path);
317 if (r < 0)
318 return log_oom();
319
320 n++;
321 }
322 if (r < 0)
323 return bus_log_parse_error(r);
324
325 r = sd_bus_message_exit_container(reply);
326 if (r < 0)
327 return bus_log_parse_error(r);
328
329 return n;
330 }
331
332 struct socket_info {
333 const char *machine;
334 const char* id;
335
336 char* type;
337 char* path;
338
339 /* Note: triggered is a list here, although it almost certainly will always be one
340 * unit. Nevertheless, dbus API allows for multiple values, so let's follow that. */
341 char** triggered;
342
343 /* The strv above is shared. free is set only in the first one. */
344 bool own_triggered;
345 };
346
347 static int socket_info_compare(const struct socket_info *a, const struct socket_info *b) {
348 int r;
349
350 assert(a);
351 assert(b);
352
353 r = strcasecmp_ptr(a->machine, b->machine);
354 if (r != 0)
355 return r;
356
357 r = strcmp(a->path, b->path);
358 if (r != 0)
359 return r;
360
361 return strcmp(a->type, b->type);
362 }
363
364 static int output_sockets_list(struct socket_info *socket_infos, size_t cs) {
365 _cleanup_(table_unrefp) Table *table = NULL;
366 int r;
367
368 assert(socket_infos || cs == 0);
369
370 table = table_new("listen", "type", "unit", "activates");
371 if (!table)
372 return log_oom();
373
374 if (!arg_show_types) {
375 /* Hide the second (TYPE) column */
376 r = table_set_display(table, (size_t) 0, (size_t) 2, (size_t) 3);
377 if (r < 0)
378 return log_error_errno(r, "Failed to set columns to display: %m");
379 }
380
381 table_set_header(table, arg_legend != 0);
382 if (arg_full)
383 table_set_width(table, 0);
384
385 (void) table_set_empty_string(table, "-");
386
387 for (struct socket_info *s = socket_infos; s < socket_infos + cs; s++) {
388 _cleanup_free_ char *j = NULL;
389 const char *path;
390
391 if (s->machine) {
392 j = strjoin(s->machine, ":", s->path);
393 if (!j)
394 return log_oom();
395 path = j;
396 } else
397 path = s->path;
398
399 r = table_add_many(table,
400 TABLE_STRING, path,
401 TABLE_STRING, s->type,
402 TABLE_STRING, s->id);
403 if (r < 0)
404 return table_log_add_error(r);
405
406 if (strv_isempty(s->triggered))
407 r = table_add_cell(table, NULL, TABLE_EMPTY, NULL);
408 else if (strv_length(s->triggered) == 1)
409 r = table_add_cell(table, NULL, TABLE_STRING, s->triggered[0]);
410 else
411 /* This should never happen, currently our socket units can only trigger a
412 * single unit. But let's handle this anyway, who knows what the future
413 * brings? */
414 r = table_add_cell(table, NULL, TABLE_STRV, s->triggered);
415 if (r < 0)
416 return table_log_add_error(r);
417 }
418
419 r = output_table(table);
420 if (r < 0)
421 return r;
422
423 if (arg_legend != 0)
424 output_legend("socket", cs);
425
426 return 0;
427 }
428
429 int verb_list_sockets(int argc, char *argv[], void *userdata) {
430 _cleanup_(message_set_freep) Set *replies = NULL;
431 _cleanup_strv_free_ char **machines = NULL;
432 _cleanup_strv_free_ char **sockets_with_suffix = NULL;
433 _cleanup_free_ UnitInfo *unit_infos = NULL;
434 _cleanup_free_ struct socket_info *socket_infos = NULL;
435 size_t cs = 0;
436 int r, n;
437 sd_bus *bus;
438
439 r = acquire_bus(BUS_MANAGER, &bus);
440 if (r < 0)
441 return r;
442
443 pager_open(arg_pager_flags);
444
445 r = expand_unit_names(bus, strv_skip(argv, 1), ".socket", &sockets_with_suffix, NULL);
446 if (r < 0)
447 return r;
448
449 if (argc == 1 || sockets_with_suffix) {
450 n = get_unit_list_recursive(bus, sockets_with_suffix, &unit_infos, &replies, &machines);
451 if (n < 0)
452 return n;
453
454 for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
455 _cleanup_strv_free_ char **listening = NULL, **triggered = NULL;
456 int c;
457
458 if (!endswith(u->id, ".socket"))
459 continue;
460
461 r = get_triggered_units(bus, u->unit_path, &triggered);
462 if (r < 0)
463 goto cleanup;
464
465 c = get_listening(bus, u->unit_path, &listening);
466 if (c < 0) {
467 r = c;
468 goto cleanup;
469 }
470
471 if (!GREEDY_REALLOC(socket_infos, cs + c)) {
472 r = log_oom();
473 goto cleanup;
474 }
475
476 for (int i = 0; i < c; i++)
477 socket_infos[cs + i] = (struct socket_info) {
478 .machine = u->machine,
479 .id = u->id,
480 .type = listening[i*2],
481 .path = listening[i*2 + 1],
482 .triggered = triggered,
483 .own_triggered = i==0,
484 };
485
486 /* from this point on we will cleanup those socket_infos */
487 cs += c;
488 free(listening);
489 listening = triggered = NULL; /* avoid cleanup */
490 }
491
492 typesafe_qsort(socket_infos, cs, socket_info_compare);
493 }
494
495 output_sockets_list(socket_infos, cs);
496
497 cleanup:
498 assert(cs == 0 || socket_infos);
499 for (struct socket_info *s = socket_infos; s < socket_infos + cs; s++) {
500 free(s->type);
501 free(s->path);
502 if (s->own_triggered)
503 strv_free(s->triggered);
504 }
505
506 return r;
507 }
508
509 static int get_next_elapse(
510 sd_bus *bus,
511 const char *path,
512 dual_timestamp *next) {
513
514 _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
515 dual_timestamp t;
516 int r;
517
518 assert(bus);
519 assert(path);
520 assert(next);
521
522 r = sd_bus_get_property_trivial(
523 bus,
524 "org.freedesktop.systemd1",
525 path,
526 "org.freedesktop.systemd1.Timer",
527 "NextElapseUSecMonotonic",
528 &error,
529 't',
530 &t.monotonic);
531 if (r < 0)
532 return log_error_errno(r, "Failed to get next elapse time: %s", bus_error_message(&error, r));
533
534 r = sd_bus_get_property_trivial(
535 bus,
536 "org.freedesktop.systemd1",
537 path,
538 "org.freedesktop.systemd1.Timer",
539 "NextElapseUSecRealtime",
540 &error,
541 't',
542 &t.realtime);
543 if (r < 0)
544 return log_error_errno(r, "Failed to get next elapse time: %s", bus_error_message(&error, r));
545
546 *next = t;
547 return 0;
548 }
549
550 static int get_last_trigger(
551 sd_bus *bus,
552 const char *path,
553 usec_t *last) {
554
555 _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
556 int r;
557
558 assert(bus);
559 assert(path);
560 assert(last);
561
562 r = sd_bus_get_property_trivial(
563 bus,
564 "org.freedesktop.systemd1",
565 path,
566 "org.freedesktop.systemd1.Timer",
567 "LastTriggerUSec",
568 &error,
569 't',
570 last);
571 if (r < 0)
572 return log_error_errno(r, "Failed to get last trigger time: %s", bus_error_message(&error, r));
573
574 return 0;
575 }
576
577 struct timer_info {
578 const char* machine;
579 const char* id;
580 usec_t next_elapse;
581 usec_t last_trigger;
582 char** triggered;
583 };
584
585 static int timer_info_compare(const struct timer_info *a, const struct timer_info *b) {
586 int r;
587
588 assert(a);
589 assert(b);
590
591 r = strcasecmp_ptr(a->machine, b->machine);
592 if (r != 0)
593 return r;
594
595 r = CMP(a->next_elapse, b->next_elapse);
596 if (r != 0)
597 return r;
598
599 return strcmp(a->id, b->id);
600 }
601
602 static int output_timers_list(struct timer_info *timer_infos, size_t n) {
603 _cleanup_(table_unrefp) Table *table = NULL;
604 int r;
605
606 assert(timer_infos || n == 0);
607
608 table = table_new("next", "left", "last", "passed", "unit", "activates");
609 if (!table)
610 return log_oom();
611
612 table_set_header(table, arg_legend != 0);
613 if (arg_full)
614 table_set_width(table, 0);
615
616 (void) table_set_empty_string(table, "-");
617
618 for (struct timer_info *t = timer_infos; t < timer_infos + n; t++) {
619 _cleanup_free_ char *j = NULL, *activates = NULL;
620 const char *unit;
621
622 if (t->machine) {
623 j = strjoin(t->machine, ":", t->id);
624 if (!j)
625 return log_oom();
626 unit = j;
627 } else
628 unit = t->id;
629
630 activates = strv_join(t->triggered, ", ");
631 if (!activates)
632 return log_oom();
633
634 r = table_add_many(table,
635 TABLE_TIMESTAMP, t->next_elapse,
636 TABLE_TIMESTAMP_RELATIVE, t->next_elapse,
637 TABLE_TIMESTAMP, t->last_trigger,
638 TABLE_TIMESTAMP_RELATIVE, t->last_trigger,
639 TABLE_STRING, unit,
640 TABLE_STRING, activates);
641 if (r < 0)
642 return table_log_add_error(r);
643 }
644
645 r = output_table(table);
646 if (r < 0)
647 return r;
648
649 if (arg_legend != 0)
650 output_legend("timer", n);
651
652 return 0;
653 }
654
655 usec_t calc_next_elapse(dual_timestamp *nw, dual_timestamp *next) {
656 usec_t next_elapse;
657
658 assert(nw);
659 assert(next);
660
661 if (timestamp_is_set(next->monotonic)) {
662 usec_t converted;
663
664 if (next->monotonic > nw->monotonic)
665 converted = nw->realtime + (next->monotonic - nw->monotonic);
666 else
667 converted = nw->realtime - (nw->monotonic - next->monotonic);
668
669 if (timestamp_is_set(next->realtime))
670 next_elapse = MIN(converted, next->realtime);
671 else
672 next_elapse = converted;
673
674 } else
675 next_elapse = next->realtime;
676
677 return next_elapse;
678 }
679
680 int verb_list_timers(int argc, char *argv[], void *userdata) {
681 _cleanup_(message_set_freep) Set *replies = NULL;
682 _cleanup_strv_free_ char **machines = NULL;
683 _cleanup_strv_free_ char **timers_with_suffix = NULL;
684 _cleanup_free_ struct timer_info *timer_infos = NULL;
685 _cleanup_free_ UnitInfo *unit_infos = NULL;
686 dual_timestamp nw;
687 size_t c = 0;
688 sd_bus *bus;
689 int n, r;
690
691 r = acquire_bus(BUS_MANAGER, &bus);
692 if (r < 0)
693 return r;
694
695 pager_open(arg_pager_flags);
696
697 r = expand_unit_names(bus, strv_skip(argv, 1), ".timer", &timers_with_suffix, NULL);
698 if (r < 0)
699 return r;
700
701 if (argc == 1 || timers_with_suffix) {
702 n = get_unit_list_recursive(bus, timers_with_suffix, &unit_infos, &replies, &machines);
703 if (n < 0)
704 return n;
705
706 dual_timestamp_get(&nw);
707
708 for (const UnitInfo *u = unit_infos; u < unit_infos + n; u++) {
709 _cleanup_strv_free_ char **triggered = NULL;
710 dual_timestamp next = DUAL_TIMESTAMP_NULL;
711 usec_t m, last = 0;
712
713 if (!endswith(u->id, ".timer"))
714 continue;
715
716 r = get_triggered_units(bus, u->unit_path, &triggered);
717 if (r < 0)
718 goto cleanup;
719
720 r = get_next_elapse(bus, u->unit_path, &next);
721 if (r < 0)
722 goto cleanup;
723
724 get_last_trigger(bus, u->unit_path, &last);
725
726 if (!GREEDY_REALLOC(timer_infos, c+1)) {
727 r = log_oom();
728 goto cleanup;
729 }
730
731 m = calc_next_elapse(&nw, &next);
732
733 timer_infos[c++] = (struct timer_info) {
734 .machine = u->machine,
735 .id = u->id,
736 .next_elapse = m,
737 .last_trigger = last,
738 .triggered = TAKE_PTR(triggered),
739 };
740 }
741
742 typesafe_qsort(timer_infos, c, timer_info_compare);
743 }
744
745 output_timers_list(timer_infos, c);
746
747 cleanup:
748 for (struct timer_info *t = timer_infos; t < timer_infos + c; t++)
749 strv_free(t->triggered);
750
751 return r;
752 }