]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/cgtop/cgtop.c
Merge pull request #150 from poettering/journal-no-recv-loop
[thirdparty/systemd.git] / src / cgtop / cgtop.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4 This file is part of systemd.
5
6 Copyright 2012 Lennart Poettering
7
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
12
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #define __STDC_FORMAT_MACROS
23 #include <errno.h>
24 #include <string.h>
25 #include <stdlib.h>
26 #include <stdint.h>
27 #include <unistd.h>
28 #include <alloca.h>
29 #include <getopt.h>
30
31 #include "path-util.h"
32 #include "terminal-util.h"
33 #include "util.h"
34 #include "hashmap.h"
35 #include "cgroup-util.h"
36 #include "build.h"
37 #include "fileio.h"
38
39 typedef struct Group {
40 char *path;
41
42 bool n_tasks_valid:1;
43 bool cpu_valid:1;
44 bool memory_valid:1;
45 bool io_valid:1;
46
47 unsigned n_tasks;
48
49 unsigned cpu_iteration;
50 uint64_t cpu_usage;
51 struct timespec cpu_timestamp;
52 double cpu_fraction;
53
54 uint64_t memory;
55
56 unsigned io_iteration;
57 uint64_t io_input, io_output;
58 struct timespec io_timestamp;
59 uint64_t io_input_bps, io_output_bps;
60 } Group;
61
62 static unsigned arg_depth = 3;
63 static unsigned arg_iterations = (unsigned)-1;
64 static bool arg_batch = false;
65 static bool arg_raw = false;
66 static usec_t arg_delay = 1*USEC_PER_SEC;
67
68 static enum {
69 ORDER_PATH,
70 ORDER_TASKS,
71 ORDER_CPU,
72 ORDER_MEMORY,
73 ORDER_IO
74 } arg_order = ORDER_CPU;
75
76 static enum {
77 CPU_PERCENT,
78 CPU_TIME,
79 } arg_cpu_type = CPU_PERCENT;
80
81 static void group_free(Group *g) {
82 assert(g);
83
84 free(g->path);
85 free(g);
86 }
87
88 static void group_hashmap_clear(Hashmap *h) {
89 Group *g;
90
91 while ((g = hashmap_steal_first(h)))
92 group_free(g);
93 }
94
95 static void group_hashmap_free(Hashmap *h) {
96 group_hashmap_clear(h);
97 hashmap_free(h);
98 }
99
100 static const char *maybe_format_bytes(char *buf, size_t l, bool is_valid, off_t t) {
101 if (!is_valid)
102 return "-";
103 if (arg_raw) {
104 snprintf(buf, l, "%jd", t);
105 return buf;
106 }
107 return format_bytes(buf, l, t);
108 }
109
110 static int process(const char *controller, const char *path, Hashmap *a, Hashmap *b, unsigned iteration) {
111 Group *g;
112 int r;
113 FILE *f = NULL;
114 pid_t pid;
115 unsigned n;
116
117 assert(controller);
118 assert(path);
119 assert(a);
120
121 g = hashmap_get(a, path);
122 if (!g) {
123 g = hashmap_get(b, path);
124 if (!g) {
125 g = new0(Group, 1);
126 if (!g)
127 return -ENOMEM;
128
129 g->path = strdup(path);
130 if (!g->path) {
131 group_free(g);
132 return -ENOMEM;
133 }
134
135 r = hashmap_put(a, g->path, g);
136 if (r < 0) {
137 group_free(g);
138 return r;
139 }
140 } else {
141 r = hashmap_move_one(a, b, path);
142 if (r < 0)
143 return r;
144 g->cpu_valid = g->memory_valid = g->io_valid = g->n_tasks_valid = false;
145 }
146 }
147
148 /* Regardless which controller, let's find the maximum number
149 * of processes in any of it */
150
151 r = cg_enumerate_processes(controller, path, &f);
152 if (r < 0)
153 return r;
154
155 n = 0;
156 while (cg_read_pid(f, &pid) > 0)
157 n++;
158 fclose(f);
159
160 if (n > 0) {
161 if (g->n_tasks_valid)
162 g->n_tasks = MAX(g->n_tasks, n);
163 else
164 g->n_tasks = n;
165
166 g->n_tasks_valid = true;
167 }
168
169 if (streq(controller, "cpuacct")) {
170 uint64_t new_usage;
171 char *p, *v;
172 struct timespec ts;
173
174 r = cg_get_path(controller, path, "cpuacct.usage", &p);
175 if (r < 0)
176 return r;
177
178 r = read_one_line_file(p, &v);
179 free(p);
180 if (r < 0)
181 return r;
182
183 r = safe_atou64(v, &new_usage);
184 free(v);
185 if (r < 0)
186 return r;
187
188 assert_se(clock_gettime(CLOCK_MONOTONIC, &ts) == 0);
189
190 if (g->cpu_iteration == iteration - 1) {
191 uint64_t x, y;
192
193 x = ((uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec) -
194 ((uint64_t) g->cpu_timestamp.tv_sec * 1000000000ULL + (uint64_t) g->cpu_timestamp.tv_nsec);
195
196 y = new_usage - g->cpu_usage;
197
198 if (y > 0) {
199 g->cpu_fraction = (double) y / (double) x;
200 g->cpu_valid = true;
201 }
202 }
203
204 g->cpu_usage = new_usage;
205 g->cpu_timestamp = ts;
206 g->cpu_iteration = iteration;
207
208 } else if (streq(controller, "memory")) {
209 char *p, *v;
210
211 r = cg_get_path(controller, path, "memory.usage_in_bytes", &p);
212 if (r < 0)
213 return r;
214
215 r = read_one_line_file(p, &v);
216 free(p);
217 if (r < 0)
218 return r;
219
220 r = safe_atou64(v, &g->memory);
221 free(v);
222 if (r < 0)
223 return r;
224
225 if (g->memory > 0)
226 g->memory_valid = true;
227
228 } else if (streq(controller, "blkio")) {
229 char *p;
230 uint64_t wr = 0, rd = 0;
231 struct timespec ts;
232
233 r = cg_get_path(controller, path, "blkio.io_service_bytes", &p);
234 if (r < 0)
235 return r;
236
237 f = fopen(p, "re");
238 free(p);
239
240 if (!f)
241 return -errno;
242
243 for (;;) {
244 char line[LINE_MAX], *l;
245 uint64_t k, *q;
246
247 if (!fgets(line, sizeof(line), f))
248 break;
249
250 l = strstrip(line);
251 l += strcspn(l, WHITESPACE);
252 l += strspn(l, WHITESPACE);
253
254 if (first_word(l, "Read")) {
255 l += 4;
256 q = &rd;
257 } else if (first_word(l, "Write")) {
258 l += 5;
259 q = &wr;
260 } else
261 continue;
262
263 l += strspn(l, WHITESPACE);
264 r = safe_atou64(l, &k);
265 if (r < 0)
266 continue;
267
268 *q += k;
269 }
270
271 fclose(f);
272
273 assert_se(clock_gettime(CLOCK_MONOTONIC, &ts) == 0);
274
275 if (g->io_iteration == iteration - 1) {
276 uint64_t x, yr, yw;
277
278 x = ((uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec) -
279 ((uint64_t) g->io_timestamp.tv_sec * 1000000000ULL + (uint64_t) g->io_timestamp.tv_nsec);
280
281 yr = rd - g->io_input;
282 yw = wr - g->io_output;
283
284 if (g->io_input > 0 || g->io_output > 0) {
285 g->io_input_bps = (yr * 1000000000ULL) / x;
286 g->io_output_bps = (yw * 1000000000ULL) / x;
287 g->io_valid = true;
288 }
289 }
290
291 g->io_input = rd;
292 g->io_output = wr;
293 g->io_timestamp = ts;
294 g->io_iteration = iteration;
295 }
296
297 return 0;
298 }
299
300 static int refresh_one(
301 const char *controller,
302 const char *path,
303 Hashmap *a,
304 Hashmap *b,
305 unsigned iteration,
306 unsigned depth) {
307
308 DIR *d = NULL;
309 int r;
310
311 assert(controller);
312 assert(path);
313 assert(a);
314
315 if (depth > arg_depth)
316 return 0;
317
318 r = process(controller, path, a, b, iteration);
319 if (r < 0)
320 return r;
321
322 r = cg_enumerate_subgroups(controller, path, &d);
323 if (r < 0) {
324 if (r == -ENOENT)
325 return 0;
326
327 return r;
328 }
329
330 for (;;) {
331 char *fn, *p;
332
333 r = cg_read_subgroup(d, &fn);
334 if (r <= 0)
335 goto finish;
336
337 p = strjoin(path, "/", fn, NULL);
338 free(fn);
339
340 if (!p) {
341 r = -ENOMEM;
342 goto finish;
343 }
344
345 path_kill_slashes(p);
346
347 r = refresh_one(controller, p, a, b, iteration, depth + 1);
348 free(p);
349
350 if (r < 0)
351 goto finish;
352 }
353
354 finish:
355 if (d)
356 closedir(d);
357
358 return r;
359 }
360
361 static int refresh(Hashmap *a, Hashmap *b, unsigned iteration) {
362 int r;
363
364 assert(a);
365
366 r = refresh_one("name=systemd", "/", a, b, iteration, 0);
367 if (r < 0)
368 if (r != -ENOENT)
369 return r;
370 r = refresh_one("cpuacct", "/", a, b, iteration, 0);
371 if (r < 0)
372 if (r != -ENOENT)
373 return r;
374 r = refresh_one("memory", "/", a, b, iteration, 0);
375 if (r < 0)
376 if (r != -ENOENT)
377 return r;
378
379 r = refresh_one("blkio", "/", a, b, iteration, 0);
380 if (r < 0)
381 if (r != -ENOENT)
382 return r;
383 return 0;
384 }
385
386 static int group_compare(const void*a, const void *b) {
387 const Group *x = *(Group**)a, *y = *(Group**)b;
388
389 if (path_startswith(y->path, x->path))
390 return -1;
391 if (path_startswith(x->path, y->path))
392 return 1;
393
394 if (arg_order == ORDER_CPU) {
395 if (arg_cpu_type == CPU_PERCENT) {
396 if (x->cpu_valid && y->cpu_valid) {
397 if (x->cpu_fraction > y->cpu_fraction)
398 return -1;
399 else if (x->cpu_fraction < y->cpu_fraction)
400 return 1;
401 } else if (x->cpu_valid)
402 return -1;
403 else if (y->cpu_valid)
404 return 1;
405 } else {
406 if (x->cpu_usage > y->cpu_usage)
407 return -1;
408 else if (x->cpu_usage < y->cpu_usage)
409 return 1;
410 }
411 }
412
413 if (arg_order == ORDER_TASKS) {
414
415 if (x->n_tasks_valid && y->n_tasks_valid) {
416 if (x->n_tasks > y->n_tasks)
417 return -1;
418 else if (x->n_tasks < y->n_tasks)
419 return 1;
420 } else if (x->n_tasks_valid)
421 return -1;
422 else if (y->n_tasks_valid)
423 return 1;
424 }
425
426 if (arg_order == ORDER_MEMORY) {
427 if (x->memory_valid && y->memory_valid) {
428 if (x->memory > y->memory)
429 return -1;
430 else if (x->memory < y->memory)
431 return 1;
432 } else if (x->memory_valid)
433 return -1;
434 else if (y->memory_valid)
435 return 1;
436 }
437
438 if (arg_order == ORDER_IO) {
439 if (x->io_valid && y->io_valid) {
440 if (x->io_input_bps + x->io_output_bps > y->io_input_bps + y->io_output_bps)
441 return -1;
442 else if (x->io_input_bps + x->io_output_bps < y->io_input_bps + y->io_output_bps)
443 return 1;
444 } else if (x->io_valid)
445 return -1;
446 else if (y->io_valid)
447 return 1;
448 }
449
450 return strcmp(x->path, y->path);
451 }
452
453 #define ON ANSI_HIGHLIGHT_ON
454 #define OFF ANSI_HIGHLIGHT_OFF
455
456 static int display(Hashmap *a) {
457 Iterator i;
458 Group *g;
459 Group **array;
460 signed path_columns;
461 unsigned rows, n = 0, j, maxtcpu = 0, maxtpath = 3; /* 3 for ellipsize() to work properly */
462 char buffer[MAX3(21, FORMAT_BYTES_MAX, FORMAT_TIMESPAN_MAX)];
463
464 assert(a);
465
466 /* Set cursor to top left corner and clear screen */
467 if (on_tty())
468 fputs("\033[H"
469 "\033[2J", stdout);
470
471 array = alloca(sizeof(Group*) * hashmap_size(a));
472
473 HASHMAP_FOREACH(g, a, i)
474 if (g->n_tasks_valid || g->cpu_valid || g->memory_valid || g->io_valid)
475 array[n++] = g;
476
477 qsort_safe(array, n, sizeof(Group*), group_compare);
478
479 /* Find the longest names in one run */
480 for (j = 0; j < n; j++) {
481 unsigned cputlen, pathtlen;
482
483 format_timespan(buffer, sizeof(buffer), (nsec_t) (array[j]->cpu_usage / NSEC_PER_USEC), 0);
484 cputlen = strlen(buffer);
485 maxtcpu = MAX(maxtcpu, cputlen);
486 pathtlen = strlen(array[j]->path);
487 maxtpath = MAX(maxtpath, pathtlen);
488 }
489
490 if (arg_cpu_type == CPU_PERCENT)
491 snprintf(buffer, sizeof(buffer), "%6s", "%CPU");
492 else
493 snprintf(buffer, sizeof(buffer), "%*s", maxtcpu, "CPU Time");
494
495 rows = lines();
496 if (rows <= 10)
497 rows = 10;
498
499 if (on_tty()) {
500 path_columns = columns() - 36 - strlen(buffer);
501 if (path_columns < 10)
502 path_columns = 10;
503
504 printf("%s%-*s%s %s%7s%s %s%s%s %s%8s%s %s%8s%s %s%8s%s\n\n",
505 arg_order == ORDER_PATH ? ON : "", path_columns, "Path",
506 arg_order == ORDER_PATH ? OFF : "",
507 arg_order == ORDER_TASKS ? ON : "", "Tasks",
508 arg_order == ORDER_TASKS ? OFF : "",
509 arg_order == ORDER_CPU ? ON : "", buffer,
510 arg_order == ORDER_CPU ? OFF : "",
511 arg_order == ORDER_MEMORY ? ON : "", "Memory",
512 arg_order == ORDER_MEMORY ? OFF : "",
513 arg_order == ORDER_IO ? ON : "", "Input/s",
514 arg_order == ORDER_IO ? OFF : "",
515 arg_order == ORDER_IO ? ON : "", "Output/s",
516 arg_order == ORDER_IO ? OFF : "");
517 } else
518 path_columns = maxtpath;
519
520 for (j = 0; j < n; j++) {
521 char *p;
522
523 if (on_tty() && j + 5 > rows)
524 break;
525
526 g = array[j];
527
528 p = ellipsize(g->path, path_columns, 33);
529 printf("%-*s", path_columns, p ? p : g->path);
530 free(p);
531
532 if (g->n_tasks_valid)
533 printf(" %7u", g->n_tasks);
534 else
535 fputs(" -", stdout);
536
537 if (arg_cpu_type == CPU_PERCENT) {
538 if (g->cpu_valid)
539 printf(" %6.1f", g->cpu_fraction*100);
540 else
541 fputs(" -", stdout);
542 } else
543 printf(" %*s", maxtcpu, format_timespan(buffer, sizeof(buffer), (nsec_t) (g->cpu_usage / NSEC_PER_USEC), 0));
544
545 printf(" %8s", maybe_format_bytes(buffer, sizeof(buffer), g->memory_valid, g->memory));
546 printf(" %8s", maybe_format_bytes(buffer, sizeof(buffer), g->io_valid, g->io_input_bps));
547 printf(" %8s", maybe_format_bytes(buffer, sizeof(buffer), g->io_valid, g->io_output_bps));
548
549 putchar('\n');
550 }
551
552 return 0;
553 }
554
555 static void help(void) {
556 printf("%s [OPTIONS...]\n\n"
557 "Show top control groups by their resource usage.\n\n"
558 " -h --help Show this help\n"
559 " --version Print version and exit\n"
560 " -p Order by path\n"
561 " -t Order by number of tasks\n"
562 " -c Order by CPU load\n"
563 " -m Order by memory load\n"
564 " -i Order by IO load\n"
565 " -r --raw Provide raw (not human-readable) numbers\n"
566 " --cpu[=TYPE] Show CPU usage as time or percentage (default)\n"
567 " -d --delay=DELAY Delay between updates\n"
568 " -n --iterations=N Run for N iterations before exiting\n"
569 " -b --batch Run in batch mode, accepting no input\n"
570 " --depth=DEPTH Maximum traversal depth (default: %u)\n"
571 , program_invocation_short_name, arg_depth);
572 }
573
574 static int parse_argv(int argc, char *argv[]) {
575
576 enum {
577 ARG_VERSION = 0x100,
578 ARG_DEPTH,
579 ARG_CPU_TYPE
580 };
581
582 static const struct option options[] = {
583 { "help", no_argument, NULL, 'h' },
584 { "version", no_argument, NULL, ARG_VERSION },
585 { "delay", required_argument, NULL, 'd' },
586 { "iterations", required_argument, NULL, 'n' },
587 { "batch", no_argument, NULL, 'b' },
588 { "raw", no_argument, NULL, 'r' },
589 { "depth", required_argument, NULL, ARG_DEPTH },
590 { "cpu", optional_argument, NULL, ARG_CPU_TYPE},
591 {}
592 };
593
594 int c;
595 int r;
596
597 assert(argc >= 1);
598 assert(argv);
599
600 while ((c = getopt_long(argc, argv, "hptcmin:brd:", options, NULL)) >= 0)
601
602 switch (c) {
603
604 case 'h':
605 help();
606 return 0;
607
608 case ARG_VERSION:
609 puts(PACKAGE_STRING);
610 puts(SYSTEMD_FEATURES);
611 return 0;
612
613 case ARG_CPU_TYPE:
614 if (optarg) {
615 if (strcmp(optarg, "time") == 0)
616 arg_cpu_type = CPU_TIME;
617 else if (strcmp(optarg, "percentage") == 0)
618 arg_cpu_type = CPU_PERCENT;
619 else
620 return -EINVAL;
621 }
622 break;
623
624 case ARG_DEPTH:
625 r = safe_atou(optarg, &arg_depth);
626 if (r < 0) {
627 log_error("Failed to parse depth parameter.");
628 return -EINVAL;
629 }
630
631 break;
632
633 case 'd':
634 r = parse_sec(optarg, &arg_delay);
635 if (r < 0 || arg_delay <= 0) {
636 log_error("Failed to parse delay parameter.");
637 return -EINVAL;
638 }
639
640 break;
641
642 case 'n':
643 r = safe_atou(optarg, &arg_iterations);
644 if (r < 0) {
645 log_error("Failed to parse iterations parameter.");
646 return -EINVAL;
647 }
648
649 break;
650
651 case 'b':
652 arg_batch = true;
653 break;
654
655 case 'r':
656 arg_raw = true;
657 break;
658
659 case 'p':
660 arg_order = ORDER_PATH;
661 break;
662
663 case 't':
664 arg_order = ORDER_TASKS;
665 break;
666
667 case 'c':
668 arg_order = ORDER_CPU;
669 break;
670
671 case 'm':
672 arg_order = ORDER_MEMORY;
673 break;
674
675 case 'i':
676 arg_order = ORDER_IO;
677 break;
678
679 case '?':
680 return -EINVAL;
681
682 default:
683 assert_not_reached("Unhandled option");
684 }
685
686 if (optind < argc) {
687 log_error("Too many arguments.");
688 return -EINVAL;
689 }
690
691 return 1;
692 }
693
694 int main(int argc, char *argv[]) {
695 int r;
696 Hashmap *a = NULL, *b = NULL;
697 unsigned iteration = 0;
698 usec_t last_refresh = 0;
699 bool quit = false, immediate_refresh = false;
700
701 log_parse_environment();
702 log_open();
703
704 r = parse_argv(argc, argv);
705 if (r <= 0)
706 goto finish;
707
708 a = hashmap_new(&string_hash_ops);
709 b = hashmap_new(&string_hash_ops);
710 if (!a || !b) {
711 r = log_oom();
712 goto finish;
713 }
714
715 signal(SIGWINCH, columns_lines_cache_reset);
716
717 if (arg_iterations == (unsigned)-1)
718 arg_iterations = on_tty() ? 0 : 1;
719
720 while (!quit) {
721 Hashmap *c;
722 usec_t t;
723 char key;
724 char h[FORMAT_TIMESPAN_MAX];
725
726 t = now(CLOCK_MONOTONIC);
727
728 if (t >= last_refresh + arg_delay || immediate_refresh) {
729
730 r = refresh(a, b, iteration++);
731 if (r < 0)
732 goto finish;
733
734 group_hashmap_clear(b);
735
736 c = a;
737 a = b;
738 b = c;
739
740 last_refresh = t;
741 immediate_refresh = false;
742 }
743
744 r = display(b);
745 if (r < 0)
746 goto finish;
747
748 if (arg_iterations && iteration >= arg_iterations)
749 break;
750
751 if (!on_tty()) /* non-TTY: Empty newline as delimiter between polls */
752 fputs("\n", stdout);
753 fflush(stdout);
754
755 if (arg_batch) {
756 usleep(last_refresh + arg_delay - t);
757 } else {
758 r = read_one_char(stdin, &key,
759 last_refresh + arg_delay - t, NULL);
760 if (r == -ETIMEDOUT)
761 continue;
762 if (r < 0) {
763 log_error_errno(r, "Couldn't read key: %m");
764 goto finish;
765 }
766 }
767
768 if (on_tty()) { /* TTY: Clear any user keystroke */
769 fputs("\r \r", stdout);
770 fflush(stdout);
771 }
772
773 if (arg_batch)
774 continue;
775
776 switch (key) {
777
778 case ' ':
779 immediate_refresh = true;
780 break;
781
782 case 'q':
783 quit = true;
784 break;
785
786 case 'p':
787 arg_order = ORDER_PATH;
788 break;
789
790 case 't':
791 arg_order = ORDER_TASKS;
792 break;
793
794 case 'c':
795 arg_order = ORDER_CPU;
796 break;
797
798 case 'm':
799 arg_order = ORDER_MEMORY;
800 break;
801
802 case 'i':
803 arg_order = ORDER_IO;
804 break;
805
806 case '%':
807 arg_cpu_type = arg_cpu_type == CPU_TIME ? CPU_PERCENT : CPU_TIME;
808 break;
809
810 case '+':
811 if (arg_delay < USEC_PER_SEC)
812 arg_delay += USEC_PER_MSEC*250;
813 else
814 arg_delay += USEC_PER_SEC;
815
816 fprintf(stdout, "\nIncreased delay to %s.", format_timespan(h, sizeof(h), arg_delay, 0));
817 fflush(stdout);
818 sleep(1);
819 break;
820
821 case '-':
822 if (arg_delay <= USEC_PER_MSEC*500)
823 arg_delay = USEC_PER_MSEC*250;
824 else if (arg_delay < USEC_PER_MSEC*1250)
825 arg_delay -= USEC_PER_MSEC*250;
826 else
827 arg_delay -= USEC_PER_SEC;
828
829 fprintf(stdout, "\nDecreased delay to %s.", format_timespan(h, sizeof(h), arg_delay, 0));
830 fflush(stdout);
831 sleep(1);
832 break;
833
834 case '?':
835 case 'h':
836 fprintf(stdout,
837 "\t<" ON "p" OFF "> By path; <" ON "t" OFF "> By tasks; <" ON "c" OFF "> By CPU; <" ON "m" OFF "> By memory; <" ON "i" OFF "> By I/O\n"
838 "\t<" ON "+" OFF "> Increase delay; <" ON "-" OFF "> Decrease delay; <" ON "%%" OFF "> Toggle time\n"
839 "\t<" ON "q" OFF "> Quit; <" ON "SPACE" OFF "> Refresh");
840 fflush(stdout);
841 sleep(3);
842 break;
843
844 default:
845 fprintf(stdout, "\nUnknown key '%c'. Ignoring.", key);
846 fflush(stdout);
847 sleep(1);
848 break;
849 }
850 }
851
852 r = 0;
853
854 finish:
855 group_hashmap_free(a);
856 group_hashmap_free(b);
857
858 if (r < 0) {
859 log_error_errno(r, "Exiting with failure: %m");
860 return EXIT_FAILURE;
861 }
862
863 return EXIT_SUCCESS;
864 }