1 /* SPDX-License-Identifier: LGPL-2.1+ */
3 This file is part of systemd.
5 Copyright 2012 Lennart Poettering
7 systemd is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Lesser General Public License as published by
9 the Free Software Foundation; either version 2.1 of the License, or
10 (at your option) any later version.
12 systemd is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public License
18 along with systemd; If not, see <http://www.gnu.org/licenses/>.
32 #include "alloc-util.h"
33 #include "bus-error.h"
35 #include "cgroup-show.h"
36 #include "cgroup-util.h"
40 #include "parse-util.h"
41 #include "path-util.h"
42 #include "process-util.h"
43 #include "procfs-util.h"
44 #include "stdio-util.h"
46 #include "terminal-util.h"
47 #include "unit-name.h"
50 typedef struct Group
{
60 unsigned cpu_iteration
;
67 unsigned io_iteration
;
68 uint64_t io_input
, io_output
;
70 uint64_t io_input_bps
, io_output_bps
;
73 static unsigned arg_depth
= 3;
74 static unsigned arg_iterations
= (unsigned) -1;
75 static bool arg_batch
= false;
76 static bool arg_raw
= false;
77 static usec_t arg_delay
= 1*USEC_PER_SEC
;
78 static char* arg_machine
= NULL
;
79 static char* arg_root
= NULL
;
80 static bool arg_recursive
= true;
81 static bool arg_recursive_unset
= false;
85 COUNT_USERSPACE_PROCESSES
,
87 } arg_count
= COUNT_PIDS
;
95 } arg_order
= ORDER_CPU
;
100 } arg_cpu_type
= CPU_PERCENT
;
102 static void group_free(Group
*g
) {
109 static void group_hashmap_clear(Hashmap
*h
) {
110 hashmap_clear_with_destructor(h
, group_free
);
113 static void group_hashmap_free(Hashmap
*h
) {
114 group_hashmap_clear(h
);
118 static const char *maybe_format_bytes(char *buf
, size_t l
, bool is_valid
, uint64_t t
) {
122 snprintf(buf
, l
, "%" PRIu64
, t
);
125 return format_bytes(buf
, l
, t
);
129 const char *controller
,
143 all_unified
= cg_all_unified();
147 g
= hashmap_get(a
, path
);
149 g
= hashmap_get(b
, path
);
155 g
->path
= strdup(path
);
161 r
= hashmap_put(a
, g
->path
, g
);
167 r
= hashmap_move_one(a
, b
, path
);
171 g
->cpu_valid
= g
->memory_valid
= g
->io_valid
= g
->n_tasks_valid
= false;
175 if (streq(controller
, SYSTEMD_CGROUP_CONTROLLER
) && IN_SET(arg_count
, COUNT_ALL_PROCESSES
, COUNT_USERSPACE_PROCESSES
)) {
176 _cleanup_fclose_
FILE *f
= NULL
;
179 r
= cg_enumerate_processes(controller
, path
, &f
);
186 while (cg_read_pid(f
, &pid
) > 0) {
188 if (arg_count
== COUNT_USERSPACE_PROCESSES
&& is_kernel_thread(pid
) > 0)
195 g
->n_tasks_valid
= true;
197 } else if (streq(controller
, "pids") && arg_count
== COUNT_PIDS
) {
199 if (isempty(path
) || path_equal(path
, "/")) {
200 r
= procfs_tasks_get_current(&g
->n_tasks
);
204 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
206 r
= cg_get_path(controller
, path
, "pids.current", &p
);
210 r
= read_one_line_file(p
, &v
);
216 r
= safe_atou64(v
, &g
->n_tasks
);
222 g
->n_tasks_valid
= true;
224 } else if (STR_IN_SET(controller
, "cpu", "cpuacct")) {
225 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
230 const char *keys
[] = { "usage_usec", NULL
};
231 _cleanup_free_
char *val
= NULL
;
233 if (!streq(controller
, "cpu"))
236 r
= cg_get_keyed_attribute("cpu", path
, "cpu.stat", keys
, &val
);
242 r
= safe_atou64(val
, &new_usage
);
246 new_usage
*= NSEC_PER_USEC
;
248 if (!streq(controller
, "cpuacct"))
251 r
= cg_get_path(controller
, path
, "cpuacct.usage", &p
);
255 r
= read_one_line_file(p
, &v
);
261 r
= safe_atou64(v
, &new_usage
);
266 timestamp
= now_nsec(CLOCK_MONOTONIC
);
268 if (g
->cpu_iteration
== iteration
- 1 &&
269 (nsec_t
) new_usage
> g
->cpu_usage
) {
273 x
= timestamp
- g
->cpu_timestamp
;
277 y
= (nsec_t
) new_usage
- g
->cpu_usage
;
278 g
->cpu_fraction
= (double) y
/ (double) x
;
282 g
->cpu_usage
= (nsec_t
) new_usage
;
283 g
->cpu_timestamp
= timestamp
;
284 g
->cpu_iteration
= iteration
;
286 } else if (streq(controller
, "memory")) {
287 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
290 r
= cg_get_path(controller
, path
, "memory.current", &p
);
292 r
= cg_get_path(controller
, path
, "memory.usage_in_bytes", &p
);
296 r
= read_one_line_file(p
, &v
);
302 r
= safe_atou64(v
, &g
->memory
);
307 g
->memory_valid
= true;
309 } else if ((streq(controller
, "io") && all_unified
) ||
310 (streq(controller
, "blkio") && !all_unified
)) {
311 _cleanup_fclose_
FILE *f
= NULL
;
312 _cleanup_free_
char *p
= NULL
;
313 uint64_t wr
= 0, rd
= 0;
316 r
= cg_get_path(controller
, path
, all_unified
? "io.stat" : "blkio.io_service_bytes", &p
);
328 char line
[LINE_MAX
], *l
;
331 if (!fgets(line
, sizeof(line
), f
))
334 /* Trim and skip the device */
336 l
+= strcspn(l
, WHITESPACE
);
337 l
+= strspn(l
, WHITESPACE
);
340 while (!isempty(l
)) {
341 if (sscanf(l
, "rbytes=%" SCNu64
, &k
))
343 else if (sscanf(l
, "wbytes=%" SCNu64
, &k
))
346 l
+= strcspn(l
, WHITESPACE
);
347 l
+= strspn(l
, WHITESPACE
);
350 if (first_word(l
, "Read")) {
353 } else if (first_word(l
, "Write")) {
359 l
+= strspn(l
, WHITESPACE
);
360 r
= safe_atou64(l
, &k
);
368 timestamp
= now_nsec(CLOCK_MONOTONIC
);
370 if (g
->io_iteration
== iteration
- 1) {
373 x
= (uint64_t) (timestamp
- g
->io_timestamp
);
377 if (rd
> g
->io_input
)
378 yr
= rd
- g
->io_input
;
382 if (wr
> g
->io_output
)
383 yw
= wr
- g
->io_output
;
387 if (yr
> 0 || yw
> 0) {
388 g
->io_input_bps
= (yr
* 1000000000ULL) / x
;
389 g
->io_output_bps
= (yw
* 1000000000ULL) / x
;
396 g
->io_timestamp
= timestamp
;
397 g
->io_iteration
= iteration
;
406 static int refresh_one(
407 const char *controller
,
415 _cleanup_closedir_
DIR *d
= NULL
;
423 if (depth
> arg_depth
)
426 r
= process(controller
, path
, a
, b
, iteration
, &ours
);
430 r
= cg_enumerate_subgroups(controller
, path
, &d
);
437 _cleanup_free_
char *fn
= NULL
, *p
= NULL
;
440 r
= cg_read_subgroup(d
, &fn
);
446 p
= strjoin(path
, "/", fn
);
450 path_kill_slashes(p
);
452 r
= refresh_one(controller
, p
, a
, b
, iteration
, depth
+ 1, &child
);
457 IN_SET(arg_count
, COUNT_ALL_PROCESSES
, COUNT_USERSPACE_PROCESSES
) &&
459 child
->n_tasks_valid
&&
460 streq(controller
, SYSTEMD_CGROUP_CONTROLLER
)) {
462 /* Recursively sum up processes */
464 if (ours
->n_tasks_valid
)
465 ours
->n_tasks
+= child
->n_tasks
;
467 ours
->n_tasks
= child
->n_tasks
;
468 ours
->n_tasks_valid
= true;
479 static int refresh(const char *root
, Hashmap
*a
, Hashmap
*b
, unsigned iteration
) {
484 r
= refresh_one(SYSTEMD_CGROUP_CONTROLLER
, root
, a
, b
, iteration
, 0, NULL
);
487 r
= refresh_one("cpu", root
, a
, b
, iteration
, 0, NULL
);
490 r
= refresh_one("cpuacct", root
, a
, b
, iteration
, 0, NULL
);
493 r
= refresh_one("memory", root
, a
, b
, iteration
, 0, NULL
);
496 r
= refresh_one("io", root
, a
, b
, iteration
, 0, NULL
);
499 r
= refresh_one("blkio", root
, a
, b
, iteration
, 0, NULL
);
502 r
= refresh_one("pids", root
, a
, b
, iteration
, 0, NULL
);
509 static int group_compare(const void*a
, const void *b
) {
510 const Group
*x
= *(Group
**)a
, *y
= *(Group
**)b
;
512 if (arg_order
!= ORDER_TASKS
|| arg_recursive
) {
513 /* Let's make sure that the parent is always before
514 * the child. Except when ordering by tasks and
515 * recursive summing is off, since that is actually
516 * not accumulative for all children. */
518 if (path_startswith(y
->path
, x
->path
))
520 if (path_startswith(x
->path
, y
->path
))
530 if (arg_cpu_type
== CPU_PERCENT
) {
531 if (x
->cpu_valid
&& y
->cpu_valid
) {
532 if (x
->cpu_fraction
> y
->cpu_fraction
)
534 else if (x
->cpu_fraction
< y
->cpu_fraction
)
536 } else if (x
->cpu_valid
)
538 else if (y
->cpu_valid
)
541 if (x
->cpu_usage
> y
->cpu_usage
)
543 else if (x
->cpu_usage
< y
->cpu_usage
)
550 if (x
->n_tasks_valid
&& y
->n_tasks_valid
) {
551 if (x
->n_tasks
> y
->n_tasks
)
553 else if (x
->n_tasks
< y
->n_tasks
)
555 } else if (x
->n_tasks_valid
)
557 else if (y
->n_tasks_valid
)
563 if (x
->memory_valid
&& y
->memory_valid
) {
564 if (x
->memory
> y
->memory
)
566 else if (x
->memory
< y
->memory
)
568 } else if (x
->memory_valid
)
570 else if (y
->memory_valid
)
576 if (x
->io_valid
&& y
->io_valid
) {
577 if (x
->io_input_bps
+ x
->io_output_bps
> y
->io_input_bps
+ y
->io_output_bps
)
579 else if (x
->io_input_bps
+ x
->io_output_bps
< y
->io_input_bps
+ y
->io_output_bps
)
581 } else if (x
->io_valid
)
583 else if (y
->io_valid
)
587 return path_compare(x
->path
, y
->path
);
590 static void display(Hashmap
*a
) {
595 unsigned rows
, n
= 0, j
, maxtcpu
= 0, maxtpath
= 3; /* 3 for ellipsize() to work properly */
596 char buffer
[MAX3(21, FORMAT_BYTES_MAX
, FORMAT_TIMESPAN_MAX
)];
600 if (!terminal_is_dumb())
601 fputs(ANSI_HOME_CLEAR
, stdout
);
603 array
= alloca(sizeof(Group
*) * hashmap_size(a
));
605 HASHMAP_FOREACH(g
, a
, i
)
606 if (g
->n_tasks_valid
|| g
->cpu_valid
|| g
->memory_valid
|| g
->io_valid
)
609 qsort_safe(array
, n
, sizeof(Group
*), group_compare
);
611 /* Find the longest names in one run */
612 for (j
= 0; j
< n
; j
++) {
613 unsigned cputlen
, pathtlen
;
615 format_timespan(buffer
, sizeof(buffer
), (usec_t
) (array
[j
]->cpu_usage
/ NSEC_PER_USEC
), 0);
616 cputlen
= strlen(buffer
);
617 maxtcpu
= MAX(maxtcpu
, cputlen
);
619 pathtlen
= strlen(array
[j
]->path
);
620 maxtpath
= MAX(maxtpath
, pathtlen
);
623 if (arg_cpu_type
== CPU_PERCENT
)
624 xsprintf(buffer
, "%6s", "%CPU");
626 xsprintf(buffer
, "%*s", maxtcpu
, "CPU Time");
633 const char *on
, *off
;
635 path_columns
= columns() - 36 - strlen(buffer
);
636 if (path_columns
< 10)
639 on
= ansi_highlight_underline();
640 off
= ansi_underline();
642 printf("%s%s%-*s%s %s%7s%s %s%s%s %s%8s%s %s%8s%s %s%8s%s%s\n",
644 arg_order
== ORDER_PATH
? on
: "", path_columns
, "Control Group",
645 arg_order
== ORDER_PATH
? off
: "",
646 arg_order
== ORDER_TASKS
? on
: "", arg_count
== COUNT_PIDS
? "Tasks" : arg_count
== COUNT_USERSPACE_PROCESSES
? "Procs" : "Proc+",
647 arg_order
== ORDER_TASKS
? off
: "",
648 arg_order
== ORDER_CPU
? on
: "", buffer
,
649 arg_order
== ORDER_CPU
? off
: "",
650 arg_order
== ORDER_MEMORY
? on
: "", "Memory",
651 arg_order
== ORDER_MEMORY
? off
: "",
652 arg_order
== ORDER_IO
? on
: "", "Input/s",
653 arg_order
== ORDER_IO
? off
: "",
654 arg_order
== ORDER_IO
? on
: "", "Output/s",
655 arg_order
== ORDER_IO
? off
: "",
658 path_columns
= maxtpath
;
660 for (j
= 0; j
< n
; j
++) {
661 _cleanup_free_
char *ellipsized
= NULL
;
664 if (on_tty() && j
+ 6 > rows
)
669 path
= isempty(g
->path
) ? "/" : g
->path
;
670 ellipsized
= ellipsize(path
, path_columns
, 33);
671 printf("%-*s", path_columns
, ellipsized
?: path
);
673 if (g
->n_tasks_valid
)
674 printf(" %7" PRIu64
, g
->n_tasks
);
678 if (arg_cpu_type
== CPU_PERCENT
) {
680 printf(" %6.1f", g
->cpu_fraction
*100);
684 printf(" %*s", maxtcpu
, format_timespan(buffer
, sizeof(buffer
), (usec_t
) (g
->cpu_usage
/ NSEC_PER_USEC
), 0));
686 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->memory_valid
, g
->memory
));
687 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->io_valid
, g
->io_input_bps
));
688 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->io_valid
, g
->io_output_bps
));
694 static void help(void) {
695 printf("%s [OPTIONS...] [CGROUP]\n\n"
696 "Show top control groups by their resource usage.\n\n"
697 " -h --help Show this help\n"
698 " --version Show package version\n"
699 " -p --order=path Order by path\n"
700 " -t --order=tasks Order by number of tasks/processes\n"
701 " -c --order=cpu Order by CPU load (default)\n"
702 " -m --order=memory Order by memory load\n"
703 " -i --order=io Order by IO load\n"
704 " -r --raw Provide raw (not human-readable) numbers\n"
705 " --cpu=percentage Show CPU usage as percentage (default)\n"
706 " --cpu=time Show CPU usage as time\n"
707 " -P Count userspace processes instead of tasks (excl. kernel)\n"
708 " -k Count all processes instead of tasks (incl. kernel)\n"
709 " --recursive=BOOL Sum up process count recursively\n"
710 " -d --delay=DELAY Delay between updates\n"
711 " -n --iterations=N Run for N iterations before exiting\n"
712 " -b --batch Run in batch mode, accepting no input\n"
713 " --depth=DEPTH Maximum traversal depth (default: %u)\n"
714 " -M --machine= Show container\n"
715 , program_invocation_short_name
, arg_depth
);
718 static int parse_argv(int argc
, char *argv
[]) {
728 static const struct option options
[] = {
729 { "help", no_argument
, NULL
, 'h' },
730 { "version", no_argument
, NULL
, ARG_VERSION
},
731 { "delay", required_argument
, NULL
, 'd' },
732 { "iterations", required_argument
, NULL
, 'n' },
733 { "batch", no_argument
, NULL
, 'b' },
734 { "raw", no_argument
, NULL
, 'r' },
735 { "depth", required_argument
, NULL
, ARG_DEPTH
},
736 { "cpu", optional_argument
, NULL
, ARG_CPU_TYPE
},
737 { "order", required_argument
, NULL
, ARG_ORDER
},
738 { "recursive", required_argument
, NULL
, ARG_RECURSIVE
},
739 { "machine", required_argument
, NULL
, 'M' },
748 while ((c
= getopt_long(argc
, argv
, "hptcmin:brd:kPM:", options
, NULL
)) >= 0)
761 if (streq(optarg
, "time"))
762 arg_cpu_type
= CPU_TIME
;
763 else if (streq(optarg
, "percentage"))
764 arg_cpu_type
= CPU_PERCENT
;
766 log_error("Unknown argument to --cpu=: %s", optarg
);
770 arg_cpu_type
= CPU_TIME
;
775 r
= safe_atou(optarg
, &arg_depth
);
777 log_error("Failed to parse depth parameter.");
784 r
= parse_sec(optarg
, &arg_delay
);
785 if (r
< 0 || arg_delay
<= 0) {
786 log_error("Failed to parse delay parameter.");
793 r
= safe_atou(optarg
, &arg_iterations
);
795 log_error("Failed to parse iterations parameter.");
810 arg_order
= ORDER_PATH
;
814 arg_order
= ORDER_TASKS
;
818 arg_order
= ORDER_CPU
;
822 arg_order
= ORDER_MEMORY
;
826 arg_order
= ORDER_IO
;
830 if (streq(optarg
, "path"))
831 arg_order
= ORDER_PATH
;
832 else if (streq(optarg
, "tasks"))
833 arg_order
= ORDER_TASKS
;
834 else if (streq(optarg
, "cpu"))
835 arg_order
= ORDER_CPU
;
836 else if (streq(optarg
, "memory"))
837 arg_order
= ORDER_MEMORY
;
838 else if (streq(optarg
, "io"))
839 arg_order
= ORDER_IO
;
841 log_error("Invalid argument to --order=: %s", optarg
);
847 arg_count
= COUNT_ALL_PROCESSES
;
851 arg_count
= COUNT_USERSPACE_PROCESSES
;
855 r
= parse_boolean(optarg
);
857 log_error("Failed to parse --recursive= argument: %s", optarg
);
862 arg_recursive_unset
= r
== 0;
866 arg_machine
= optarg
;
873 assert_not_reached("Unhandled option");
876 if (optind
== argc
- 1)
877 arg_root
= argv
[optind
];
878 else if (optind
< argc
) {
879 log_error("Too many arguments.");
886 static const char* counting_what(void) {
887 if (arg_count
== COUNT_PIDS
)
889 else if (arg_count
== COUNT_ALL_PROCESSES
)
890 return "all processes (incl. kernel)";
892 return "userspace processes (excl. kernel)";
895 int main(int argc
, char *argv
[]) {
897 Hashmap
*a
= NULL
, *b
= NULL
;
898 unsigned iteration
= 0;
899 usec_t last_refresh
= 0;
900 bool quit
= false, immediate_refresh
= false;
901 _cleanup_free_
char *root
= NULL
;
904 log_parse_environment();
907 r
= parse_argv(argc
, argv
);
911 r
= cg_mask_supported(&mask
);
913 log_error_errno(r
, "Failed to determine supported controllers: %m");
917 arg_count
= (mask
& CGROUP_MASK_PIDS
) ? COUNT_PIDS
: COUNT_USERSPACE_PROCESSES
;
919 if (arg_recursive_unset
&& arg_count
== COUNT_PIDS
) {
920 log_error("Non-recursive counting is only supported when counting processes, not tasks. Use -P or -k.");
924 r
= show_cgroup_get_path_and_warn(arg_machine
, arg_root
, &root
);
926 log_error_errno(r
, "Failed to get root control group path: %m");
929 log_debug("Cgroup path: %s", root
);
931 a
= hashmap_new(&path_hash_ops
);
932 b
= hashmap_new(&path_hash_ops
);
938 signal(SIGWINCH
, columns_lines_cache_reset
);
940 if (arg_iterations
== (unsigned) -1)
941 arg_iterations
= on_tty() ? 0 : 1;
947 char h
[FORMAT_TIMESPAN_MAX
];
949 t
= now(CLOCK_MONOTONIC
);
951 if (t
>= last_refresh
+ arg_delay
|| immediate_refresh
) {
953 r
= refresh(root
, a
, b
, iteration
++);
955 log_error_errno(r
, "Failed to refresh: %m");
959 group_hashmap_clear(b
);
966 immediate_refresh
= false;
971 if (arg_iterations
&& iteration
>= arg_iterations
)
974 if (!on_tty()) /* non-TTY: Empty newline as delimiter between polls */
979 (void) usleep(last_refresh
+ arg_delay
- t
);
981 r
= read_one_char(stdin
, &key
, last_refresh
+ arg_delay
- t
, NULL
);
985 log_error_errno(r
, "Couldn't read key: %m");
990 if (on_tty()) { /* TTY: Clear any user keystroke */
991 fputs("\r \r", stdout
);
1001 immediate_refresh
= true;
1009 arg_order
= ORDER_PATH
;
1013 arg_order
= ORDER_TASKS
;
1017 arg_order
= ORDER_CPU
;
1021 arg_order
= ORDER_MEMORY
;
1025 arg_order
= ORDER_IO
;
1029 arg_cpu_type
= arg_cpu_type
== CPU_TIME
? CPU_PERCENT
: CPU_TIME
;
1033 arg_count
= arg_count
!= COUNT_ALL_PROCESSES
? COUNT_ALL_PROCESSES
: COUNT_PIDS
;
1034 fprintf(stdout
, "\nCounting: %s.", counting_what());
1040 arg_count
= arg_count
!= COUNT_USERSPACE_PROCESSES
? COUNT_USERSPACE_PROCESSES
: COUNT_PIDS
;
1041 fprintf(stdout
, "\nCounting: %s.", counting_what());
1047 if (arg_count
== COUNT_PIDS
)
1048 fprintf(stdout
, "\n\aCannot toggle recursive counting, not available in task counting mode.");
1050 arg_recursive
= !arg_recursive
;
1051 fprintf(stdout
, "\nRecursive process counting: %s", yes_no(arg_recursive
));
1058 if (arg_delay
< USEC_PER_SEC
)
1059 arg_delay
+= USEC_PER_MSEC
*250;
1061 arg_delay
+= USEC_PER_SEC
;
1063 fprintf(stdout
, "\nIncreased delay to %s.", format_timespan(h
, sizeof(h
), arg_delay
, 0));
1069 if (arg_delay
<= USEC_PER_MSEC
*500)
1070 arg_delay
= USEC_PER_MSEC
*250;
1071 else if (arg_delay
< USEC_PER_MSEC
*1250)
1072 arg_delay
-= USEC_PER_MSEC
*250;
1074 arg_delay
-= USEC_PER_SEC
;
1076 fprintf(stdout
, "\nDecreased delay to %s.", format_timespan(h
, sizeof(h
), arg_delay
, 0));
1084 #define ON ANSI_HIGHLIGHT
1085 #define OFF ANSI_NORMAL
1088 "\t<" ON
"p" OFF
"> By path; <" ON
"t" OFF
"> By tasks/procs; <" ON
"c" OFF
"> By CPU; <" ON
"m" OFF
"> By memory; <" ON
"i" OFF
"> By I/O\n"
1089 "\t<" ON
"+" OFF
"> Inc. delay; <" ON
"-" OFF
"> Dec. delay; <" ON
"%%" OFF
"> Toggle time; <" ON
"SPACE" OFF
"> Refresh\n"
1090 "\t<" ON
"P" OFF
"> Toggle count userspace processes; <" ON
"k" OFF
"> Toggle count all processes\n"
1091 "\t<" ON
"r" OFF
"> Count processes recursively; <" ON
"q" OFF
"> Quit");
1098 fprintf(stdout
, "\nUnknown key '\\x%x'. Ignoring.", key
);
1100 fprintf(stdout
, "\nUnknown key '%c'. Ignoring.", key
);
1110 group_hashmap_free(a
);
1111 group_hashmap_free(b
);
1113 return r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
;