]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/cgtop/cgtop.c
cgtop: allow user to force looping behavior even in non-TTY mode
[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 (yr > 0 || yw > 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
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 (arg_batch) {
753 usleep(last_refresh + arg_delay - t);
754 } else {
755 r = read_one_char(stdin, &key,
756 last_refresh + arg_delay - t, NULL);
757 if (r == -ETIMEDOUT)
758 continue;
759 if (r < 0) {
760 log_error_errno(r, "Couldn't read key: %m");
761 goto finish;
762 }
763 }
764
765 fputs("\r \r", stdout);
766 fflush(stdout);
767
768 if (arg_batch)
769 continue;
770
771 switch (key) {
772
773 case ' ':
774 immediate_refresh = true;
775 break;
776
777 case 'q':
778 quit = true;
779 break;
780
781 case 'p':
782 arg_order = ORDER_PATH;
783 break;
784
785 case 't':
786 arg_order = ORDER_TASKS;
787 break;
788
789 case 'c':
790 arg_order = ORDER_CPU;
791 break;
792
793 case 'm':
794 arg_order = ORDER_MEMORY;
795 break;
796
797 case 'i':
798 arg_order = ORDER_IO;
799 break;
800
801 case '%':
802 arg_cpu_type = arg_cpu_type == CPU_TIME ? CPU_PERCENT : CPU_TIME;
803 break;
804
805 case '+':
806 if (arg_delay < USEC_PER_SEC)
807 arg_delay += USEC_PER_MSEC*250;
808 else
809 arg_delay += USEC_PER_SEC;
810
811 fprintf(stdout, "\nIncreased delay to %s.", format_timespan(h, sizeof(h), arg_delay, 0));
812 fflush(stdout);
813 sleep(1);
814 break;
815
816 case '-':
817 if (arg_delay <= USEC_PER_MSEC*500)
818 arg_delay = USEC_PER_MSEC*250;
819 else if (arg_delay < USEC_PER_MSEC*1250)
820 arg_delay -= USEC_PER_MSEC*250;
821 else
822 arg_delay -= USEC_PER_SEC;
823
824 fprintf(stdout, "\nDecreased delay to %s.", format_timespan(h, sizeof(h), arg_delay, 0));
825 fflush(stdout);
826 sleep(1);
827 break;
828
829 case '?':
830 case 'h':
831 fprintf(stdout,
832 "\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"
833 "\t<" ON "+" OFF "> Increase delay; <" ON "-" OFF "> Decrease delay; <" ON "%%" OFF "> Toggle time\n"
834 "\t<" ON "q" OFF "> Quit; <" ON "SPACE" OFF "> Refresh");
835 fflush(stdout);
836 sleep(3);
837 break;
838
839 default:
840 fprintf(stdout, "\nUnknown key '%c'. Ignoring.", key);
841 fflush(stdout);
842 sleep(1);
843 break;
844 }
845 }
846
847 r = 0;
848
849 finish:
850 group_hashmap_free(a);
851 group_hashmap_free(b);
852
853 if (r < 0) {
854 log_error_errno(r, "Exiting with failure: %m");
855 return EXIT_FAILURE;
856 }
857
858 return EXIT_SUCCESS;
859 }