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