diff options
Diffstat (limited to '')
-rw-r--r-- | misc-utils/lsfd.c | 2054 |
1 files changed, 2054 insertions, 0 deletions
diff --git a/misc-utils/lsfd.c b/misc-utils/lsfd.c new file mode 100644 index 0000000..f8537a7 --- /dev/null +++ b/misc-utils/lsfd.c @@ -0,0 +1,2054 @@ +/* + * lsfd(1) - list file descriptors + * + * Copyright (C) 2021 Red Hat, Inc. All rights reserved. + * Written by Masatake YAMATO <yamato@redhat.com> + * Karel Zak <kzak@redhat.com> + * + * Very generally based on lsof(8) by Victor A. Abell <abe@purdue.edu> + * It supports multiple OSes. lsfd specializes to Linux. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it would be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include <stdio.h> +#include <sys/types.h> +#include <inttypes.h> +#include <sys/stat.h> +#include <unistd.h> +#include <getopt.h> +#include <ctype.h> +#include <search.h> + +#include <linux/sched.h> +#include <sys/syscall.h> +#include <linux/kcmp.h> +static int kcmp(pid_t pid1, pid_t pid2, int type, + unsigned long idx1, unsigned long idx2) +{ + return syscall(SYS_kcmp, pid1, pid2, type, idx1, idx2); +} + +/* See proc(5). + * Defined in linux/include/linux/sched.h private header file. */ +#define PF_KTHREAD 0x00200000 /* I am a kernel thread */ + +#include "c.h" +#include "nls.h" +#include "xalloc.h" +#include "list.h" +#include "closestream.h" +#include "strutils.h" +#include "procfs.h" +#include "fileutils.h" +#include "idcache.h" +#include "pathnames.h" + +#include "libsmartcols.h" + +#include "lsfd.h" +#include "lsfd-filter.h" +#include "lsfd-counter.h" + +/* + * /proc/$pid/mountinfo entries + */ +struct nodev { + struct list_head nodevs; + unsigned long minor; + char *filesystem; +}; + +struct nodev_table { +#define NODEV_TABLE_SIZE 97 + struct list_head tables[NODEV_TABLE_SIZE]; +}; +static struct nodev_table nodev_table; + +struct name_manager { + struct idcache *cache; + unsigned long next_id; +}; + +/* + * /proc/devices entries + */ +struct devdrv { + struct list_head devdrvs; + unsigned long major; + char *name; +}; + +static struct list_head chrdrvs; +static struct list_head blkdrvs; + +/* + * IPC table + */ + +#define IPC_TABLE_SIZE 997 +struct ipc_table { + struct list_head tables[IPC_TABLE_SIZE]; +}; + +static struct ipc_table ipc_table; + +/* + * Column related stuffs + */ + +/* column names */ +struct colinfo { + const char *name; + double whint; + int flags; + int json_type; + const char *help; +}; + +/* columns descriptions */ +static const struct colinfo infos[] = { + [COL_AINODECLASS] = { "AINODECLASS", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("class of anonymous inode") }, + [COL_ASSOC] = { "ASSOC", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("association between file and process") }, + [COL_BLKDRV] = { "BLKDRV", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("block device driver name resolved by /proc/devices") }, + [COL_CHRDRV] = { "CHRDRV", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("character device driver name resolved by /proc/devices") }, + [COL_COMMAND] = { "COMMAND", + 0.3, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, + N_("command of the process opening the file") }, + [COL_DELETED] = { "DELETED", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_BOOLEAN, + N_("reachability from the file system") }, + [COL_DEV] = { "DEV", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("ID of device containing file") }, + [COL_DEVTYPE] = { "DEVTYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("device type (blk, char, or nodev)") }, + [COL_ENDPOINTS] = { "ENDPOINTS", + 0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING, + N_("IPC endpoints information communicated with the fd") }, + [COL_FLAGS] = { "FLAGS", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("flags specified when opening the file") }, + [COL_FD] = { "FD", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("file descriptor for the file") }, + [COL_FUID] = { "FUID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("user ID number of the file's owner") }, + [COL_INODE] = { "INODE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("inode number") }, + [COL_INET_LADDR] = { "INET.LADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("local IP address") }, + [COL_INET_RADDR] = { "INET.RADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("remote IP address") }, + [COL_INET6_LADDR] = { "INET6.LADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("local IPv6 address") }, + [COL_INET6_RADDR] = { "INET6.RADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("remote IPv6 address") }, + [COL_KNAME] = { "KNAME", + 0.4, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, + N_("name of the file (raw)") }, + [COL_KTHREAD] = { "KTHREAD", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_BOOLEAN, + N_("opened by a kernel thread") }, + [COL_MAJMIN] = { "MAJ:MIN", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("device ID for special, or ID of device containing file") }, + [COL_MAPLEN] = { "MAPLEN", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("length of file mapping (in page)") }, + [COL_MISCDEV] = { "MISCDEV", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("misc character device name resolved by /proc/misc") }, + [COL_MNT_ID] = { "MNTID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("mount id") }, + [COL_MODE] = { "MODE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("access mode (rwx)") }, + [COL_NAME] = { "NAME", + 0.4, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, + N_("name of the file (cooked)") }, + [COL_NETLINK_GROUPS] = { "NETLINK.GROUPS", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("netlink multicast groups") }, + [COL_NETLINK_LPORT] = { "NETLINK.LPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("netlink local port id") }, + [COL_NETLINK_PROTOCOL] = { "NETLINK.PROTOCOL", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("netlink protocol") }, + [COL_NLINK] = { "NLINK", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("link count") }, + [COL_NS_NAME] = { "NS.NAME", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("name of the namespace (NS.TYPE:[INODE])") }, + [COL_NS_TYPE] = { "NS.TYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("type of the namespace") }, + [COL_OWNER] = { "OWNER", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("owner of the file") }, + [COL_PACKET_IFACE] = { "PACKET.IFACE", + 0, SCOLS_FL_RIGHT,SCOLS_JSON_STRING, + N_("net interface associated with the packet socket") }, + [COL_PACKET_PROTOCOL] = { "PACKET.PROTOCOL", + 0, SCOLS_FL_RIGHT,SCOLS_JSON_STRING, + N_("L3 protocol associated with the packet socket") }, + [COL_PARTITION] = { "PARTITION", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("block device name resolved by /proc/partition") }, + [COL_PID] = { "PID", + 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("PID of the process opening the file") }, + [COL_PIDFD_COMM] = { "PIDFD.COMM", + 0.2, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, + N_("command of the process targeted by the pidfd") }, + [COL_PIDFD_NSPID] = { "PIDFD.NSPID", + 0.2, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, + N_("NSpid field in fdinfo of the pidfd") }, + [COL_PIDFD_PID] = { "PIDFD.PID", + 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("PID of the process targeted by the pidfd") }, + [COL_PING_ID] = { "PING.ID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("ICMP echo request ID") }, + [COL_POS] = { "POS", + 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("file position") }, + [COL_RAW_PROTOCOL] = { "RAW.PROTOCOL", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("protocol number of the raw socket") }, + [COL_RDEV] = { "RDEV", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("device ID (if special file)") }, + [COL_SIZE] = { "SIZE", + 4, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("file size"), }, + [COL_SOCK_LISTENING] = { "SOCK.LISTENING", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_BOOLEAN, + N_("listening socket") }, + [COL_SOCK_NETNS] = { "SOCK.NETNS", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("inode identifying network namespace where the socket belongs to") }, + [COL_SOCK_PROTONAME] = { "SOCK.PROTONAME", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("protocol name") }, + [COL_SOCK_STATE] = { "SOCK.STATE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("State of socket") }, + [COL_SOCK_TYPE] = { "SOCK.TYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("Type of socket") }, + [COL_SOURCE] = { "SOURCE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("file system, partition, or device containing file") }, + [COL_STTYPE] = { "STTYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("file type (raw)") }, + [COL_TCP_LADDR] = { "TCP.LADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("local TCP address (INET address:TCP port)") }, + [COL_TCP_RADDR] = { "TCP.RADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("remote TCP address (INET address:TCP port)") }, + [COL_TCP_LPORT] = { "TCP.LPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("local TCP port") }, + [COL_TCP_RPORT] = { "TCP.RPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("remote TCP port") }, + [COL_TID] = { "TID", + 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("thread ID of the process opening the file") }, + [COL_TYPE] = { "TYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("file type (cooked)") }, + [COL_UDP_LADDR] = { "UDP.LADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("local UDP address (INET address:UDP port)") }, + [COL_UDP_RADDR] = { "UDP.RADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("remote UDP address (INET address:UDP port)") }, + [COL_UDP_LPORT] = { "UDP.LPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("local UDP port") }, + [COL_UDP_RPORT] = { "UDP.RPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("remote UDP port") }, + [COL_UDPLITE_LADDR] = { "UDPLITE.LADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("local UDPLite address (INET address:UDPLite port)") }, + [COL_UDPLITE_RADDR] = { "UDPLITE.RADDR", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("remote UDPLite address (INET address:UDPLite port)") }, + [COL_UDPLITE_LPORT] = { "UDPLITE.LPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("local UDPLite port") }, + [COL_UDPLITE_RPORT] = { "UDPLITE.RPORT", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("remote UDPLite port") }, + [COL_UID] = { "UID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("user ID number of the process") }, + [COL_UNIX_PATH] = { "UNIX.PATH", + 0.4, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, + N_("filesystem pathname for UNIX domain socket") }, + [COL_USER] = { "USER", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("user of the process") }, +}; + +static const int default_columns[] = { + COL_COMMAND, + COL_PID, + COL_USER, + COL_ASSOC, + COL_MODE, + COL_TYPE, + COL_SOURCE, + COL_MNT_ID, + COL_INODE, + COL_NAME, +}; + +static const int default_threads_columns[] = { + COL_COMMAND, + COL_PID, + COL_TID, + COL_USER, + COL_ASSOC, + COL_MODE, + COL_TYPE, + COL_SOURCE, + COL_MNT_ID, + COL_INODE, + COL_NAME, +}; + +static int columns[ARRAY_SIZE(infos) * 2] = {-1}; +static size_t ncolumns; + +static ino_t *mnt_namespaces; +static size_t nspaces; + +struct counter_spec { + struct list_head specs; + const char *name; + const char *expr; +}; + +static const struct counter_spec default_counter_specs[] = { + { + .name = N_("processes"), + .expr = "ASSOC == 'cwd'", + }, + { + .name = N_("root owned processes"), + .expr = "(ASSOC == 'cwd') && (UID == 0)", + }, + { + .name = N_("kernel threads"), + .expr = "(ASSOC == 'cwd') && KTHREAD", + }, + { + .name = N_("open files"), + .expr = "FD >= 0", + }, + { + .name = N_("RO open files"), + .expr = "(FD >= 0) and (MODE == 'r--')", + }, + { + .name = N_("WO open files"), + .expr = "(FD >= 0) and (MODE == '-w-')", + }, + { + .name = N_("shared mappings"), + .expr = "ASSOC == 'shm'", + }, + { + .name = N_("RO shared mappings"), + .expr = "(ASSOC == 'shm') and (MODE == 'r--')", + }, + { + .name = N_("WO shared mappings"), + .expr = "(ASSOC == 'shm') and (MODE == '-w-')", + }, + { + .name = N_("regular files"), + .expr = "(FD >= 0) && (STTYPE == 'REG')", + }, + { + .name = N_("directories"), + .expr = "(FD >= 0) && (STTYPE == 'DIR')", + }, + { + .name = N_("sockets"), + .expr = "(FD >= 0) && (STTYPE == 'SOCK')", + }, + { + .name = N_("fifos/pipes"), + .expr = "(FD >= 0) && (STTYPE == 'FIFO')", + }, + { + .name = N_("character devices"), + .expr = "(FD >= 0) && (STTYPE == 'CHR')", + }, + { + .name = N_("block devices"), + .expr = "(FD >= 0) && (STTYPE == 'BLK')", + }, + { + .name = N_("unknown types"), + .expr = "(FD >= 0) && (STTYPE == 'UNKN')", + } +}; + +struct lsfd_control { + struct libscols_table *tb; /* output */ + struct list_head procs; /* list of all processes */ + + unsigned int noheadings : 1, + raw : 1, + json : 1, + notrunc : 1, + threads : 1, + show_main : 1, /* print main table */ + show_summary : 1, /* print summary/counters */ + sockets_only : 1; /* display only SOCKETS */ + + struct lsfd_filter *filter; + struct lsfd_counter **counters; /* NULL terminated array. */ +}; + +static void *proc_tree; /* for tsearch/tfind */ + +static int proc_tree_compare(const void *a, const void *b) +{ + return ((struct proc *)a)->pid - ((struct proc *)b)->pid; +} + +struct proc *get_proc(pid_t pid) +{ + struct proc key = { .pid = pid }; + struct proc **node = tfind(&key, &proc_tree, proc_tree_compare); + if (node) + return *node; + return NULL; +} + +static int column_name_to_id(const char *name, size_t namesz) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(infos); i++) { + const char *cn = infos[i].name; + + if (!strncasecmp(name, cn, namesz) && !*(cn + namesz)) + return i; + } + warnx(_("unknown column: %s"), name); + + return LSFD_FILTER_UNKNOWN_COL_ID; +} + +static int column_name_to_id_cb(const char *name, void *data __attribute__((__unused__))) +{ + return column_name_to_id(name, strlen(name)); +} + +static int get_column_id(int num) +{ + assert(num >= 0); + assert((size_t) num < ncolumns); + assert(columns[num] < (int) ARRAY_SIZE(infos)); + + return columns[num]; +} + +static const struct colinfo *get_column_info(int num) +{ + return &infos[ get_column_id(num) ]; +} + +static struct libscols_column *add_column(struct libscols_table *tb, const struct colinfo *col) +{ + struct libscols_column *cl; + int flags = col->flags; + + cl = scols_table_new_column(tb, col->name, col->whint, flags); + if (cl) { + scols_column_set_json_type(cl, col->json_type); + if (col->flags & SCOLS_FL_WRAP) { + scols_column_set_wrapfunc(cl, + scols_wrapnl_chunksize, + scols_wrapnl_nextchunk, + NULL); + scols_column_set_safechars(cl, "\n"); + } + } + + return cl; +} + +static struct libscols_column *add_column_by_id_cb(struct libscols_table *tb, int colid, void *data) +{ + struct libscols_column *cl; + + if (ncolumns >= ARRAY_SIZE(columns)) + errx(EXIT_FAILURE, _("too many columns are added via filter expression")); + + assert(colid < LSFD_N_COLS); + + cl = add_column(tb, infos + colid); + if (!cl) + err(EXIT_FAILURE, _("failed to allocate output column")); + columns[ncolumns++] = colid; + + if (colid == COL_TID) { + struct lsfd_control *ctl = data; + ctl->threads = 1; + } + + return cl; +} + +static int has_mnt_ns(ino_t id) +{ + size_t i; + + for (i = 0; i < nspaces; i++) { + if (mnt_namespaces[i] == id) + return 1; + } + return 0; +} + +static void add_mnt_ns(ino_t id) +{ + size_t nmax = 0; + + if (nspaces) + nmax = (nspaces + 16) / 16 * 16; + if (nmax <= nspaces + 1) { + nmax += 16; + mnt_namespaces = xrealloc(mnt_namespaces, + sizeof(ino_t) * nmax); + } + mnt_namespaces[nspaces++] = id; +} + +static const struct file_class *stat2class(struct stat *sb) +{ + dev_t dev; + + assert(sb); + + switch (sb->st_mode & S_IFMT) { + case S_IFCHR: + return &cdev_class; + case S_IFBLK: + return &bdev_class; + case S_IFSOCK: + return &sock_class; + case S_IFIFO: + return &fifo_class; + case S_IFLNK: + case S_IFDIR: + return &file_class; + case S_IFREG: + dev = sb->st_dev; + if (major(dev) != 0) + return &file_class; + + if (is_nsfs_dev(dev)) + return &nsfs_file_class; + + return &file_class; + default: + break; + } + + return &unkn_class; +} + +static struct file *new_file(struct proc *proc, const struct file_class *class) +{ + struct file *file; + + file = xcalloc(1, class->size); + file->proc = proc; + + INIT_LIST_HEAD(&file->files); + list_add_tail(&file->files, &proc->files); + + return file; +} + +static struct file *copy_file(struct file *old) +{ + struct file *file = xcalloc(1, old->class->size); + + INIT_LIST_HEAD(&file->files); + file->proc = old->proc; + list_add_tail(&file->files, &old->proc->files); + + file->class = old->class; + file->association = old->association; + file->name = xstrdup(old->name); + file->stat = old->stat; + + return file; +} + +static void file_set_path(struct file *file, struct stat *sb, const char *name, int association) +{ + const struct file_class *class = stat2class(sb); + + assert(class); + + file->class = class; + file->association = association; + file->name = xstrdup(name); + file->stat = *sb; +} + +static void file_init_content(struct file *file) +{ + if (file->class && file->class->initialize_content) + file->class->initialize_content(file); +} + +static void free_file(struct file *file) +{ + const struct file_class *class = file->class; + + while (class) { + if (class->free_content) + class->free_content(file); + class = class->super; + } + free(file); +} + + +static struct proc *new_process(pid_t pid, struct proc *leader) +{ + struct proc *proc = xcalloc(1, sizeof(*proc)); + + proc->pid = pid; + proc->leader = leader? leader: proc; + proc->command = NULL; + + INIT_LIST_HEAD(&proc->files); + INIT_LIST_HEAD(&proc->procs); + + proc->kthread = 0; + return proc; +} + +static void free_proc(struct proc *proc) +{ + list_free(&proc->files, struct file, files, free_file); + + free(proc->command); + free(proc); +} + +static void read_fdinfo(struct file *file, FILE *fdinfo) +{ + char buf[1024]; + + while (fgets(buf, sizeof(buf), fdinfo)) { + const struct file_class *class; + char *val = strchr(buf, ':'); + + if (!val) + continue; + *val++ = '\0'; /* terminate key */ + + val = (char *) skip_space(val); + rtrim_whitespace((unsigned char *) val); + + class = file->class; + while (class) { + if (class->handle_fdinfo + && class->handle_fdinfo(file, buf, val)) + break; + class = class->super; + } + } +} + +static struct file *collect_file_symlink(struct path_cxt *pc, + struct proc *proc, + const char *name, + int assoc, + bool sockets_only) +{ + char sym[PATH_MAX] = { '\0' }; + struct stat sb; + struct file *f, *prev; + + if (ul_path_readlink(pc, sym, sizeof(sym), name) < 0) + return NULL; + + /* The /proc/#/{fd,ns} often contains the same file (e.g. /dev/tty) + * more than once. Let's try to reuse the previous file if the real + * path is the same to save stat() call. + */ + prev = list_last_entry(&proc->files, struct file, files); + if (prev && prev->name && strcmp(prev->name, sym) == 0) { + f = copy_file(prev); + f->association = assoc; + } else { + const struct file_class *class; + + if (ul_path_stat(pc, &sb, 0, name) < 0) + return NULL; + + class = stat2class(&sb); + if (sockets_only + /* A nsfs is not a socket but the nsfs can be used to + * collect information from other network namespaces. + * Besed on the information, various columns of sockets. + */ + && (class != &sock_class)&& (class != &nsfs_file_class)) + return NULL; + f = new_file(proc, class); + file_set_path(f, &sb, sym, assoc); + } + + file_init_content(f); + + if (is_association(f, NS_MNT)) + proc->ns_mnt = f->stat.st_ino; + else if (is_association(f, NS_NET)) + load_sock_xinfo(pc, name, f->stat.st_ino); + + else if (assoc >= 0) { + /* file-descriptor based association */ + FILE *fdinfo; + + if (ul_path_stat(pc, &sb, AT_SYMLINK_NOFOLLOW, name) == 0) + f->mode = sb.st_mode; + + if (is_nsfs_dev(f->stat.st_dev)) + load_sock_xinfo(pc, name, f->stat.st_ino); + + fdinfo = ul_path_fopenf(pc, "r", "fdinfo/%d", assoc); + if (fdinfo) { + read_fdinfo(f, fdinfo); + fclose(fdinfo); + } + } + + return f; +} + +/* read symlinks from /proc/#/fd + */ +static void collect_fd_files(struct path_cxt *pc, struct proc *proc, + bool sockets_only) +{ + DIR *sub = NULL; + struct dirent *d = NULL; + char path[sizeof("fd/") + sizeof(stringify_value(UINT64_MAX))]; + + while (ul_path_next_dirent(pc, &sub, "fd", &d) == 0) { + uint64_t num; + + if (ul_strtou64(d->d_name, &num, 10) != 0) /* only numbers */ + continue; + + snprintf(path, sizeof(path), "fd/%ju", (uintmax_t) num); + collect_file_symlink(pc, proc, path, num, sockets_only); + } +} + +static void parse_maps_line(struct path_cxt *pc, char *buf, struct proc *proc) +{ + uint64_t start, end, offset, ino; + unsigned long major, minor; + enum association assoc = ASSOC_MEM; + struct stat sb; + struct file *f, *prev; + char *path, modestr[5]; + dev_t devno; + + /* read rest of the map */ + if (sscanf(buf, "%"SCNx64 /* start */ + "-%"SCNx64 /* end */ + " %4[^ ]" /* mode */ + " %"SCNx64 /* offset */ + " %lx:%lx" /* maj:min */ + " %"SCNu64, /* inode */ + + &start, &end, modestr, &offset, + &major, &minor, &ino) != 7) + return; + + /* Skip private anonymous mappings. */ + if (major == 0 && minor == 0 && ino == 0) + return; + + devno = makedev(major, minor); + + if (modestr[3] == 's') + assoc = ASSOC_SHM; + + /* The map usually contains the same file more than once, try to reuse + * the previous file (if devno and ino are the same) to save stat() call. + */ + prev = list_last_entry(&proc->files, struct file, files); + + if (prev && prev->stat.st_dev == devno && prev->stat.st_ino == ino) { + f = copy_file(prev); + f->association = -assoc; + } else if ((path = strchr(buf, '/'))) { + rtrim_whitespace((unsigned char *) path); + if (stat(path, &sb) < 0) + /* If a file is mapped but deleted from the file system, + * "stat by the file name" may not work. In that case, + */ + goto try_map_files; + f = new_file(proc, stat2class(&sb)); + if (!f) + return; + + file_set_path(f, &sb, path, -assoc); + } else { + /* As used in tcpdump, AF_PACKET socket can be mmap'ed. */ + char map_file[sizeof("map_files/0000000000000000-ffffffffffffffff")]; + char sym[PATH_MAX] = { '\0' }; + + try_map_files: + snprintf(map_file, sizeof(map_file), "map_files/%"PRIx64"-%"PRIx64, start, end); + if (ul_path_stat(pc, &sb, 0, map_file) < 0) + return; + if (ul_path_readlink(pc, sym, sizeof(sym), map_file) < 0) + return; + f = new_file(proc, stat2class(&sb)); + if (!f) + return; + + file_set_path(f, &sb, sym, -assoc); + } + + if (modestr[0] == 'r') + f->mode |= S_IRUSR; + if (modestr[1] == 'w') + f->mode |= S_IWUSR; + if (modestr[2] == 'x') + f->mode |= S_IXUSR; + + f->map_start = start; + f->map_end = end; + f->pos = offset; + + file_init_content(f); +} + +static void collect_mem_files(struct path_cxt *pc, struct proc *proc) +{ + FILE *fp; + char buf[BUFSIZ]; + + fp = ul_path_fopen(pc, "r", "maps"); + if (!fp) + return; + + while (fgets(buf, sizeof(buf), fp)) + parse_maps_line(pc, buf, proc); + + fclose(fp); +} + +static void collect_outofbox_files(struct path_cxt *pc, + struct proc *proc, + enum association assocs[], + const char *names[], + size_t count, + bool sockets_only) +{ + size_t i; + + for (i = 0; i < count; i++) + collect_file_symlink(pc, proc, names[assocs[i]], assocs[i] * -1, + sockets_only); +} + +static void collect_execve_file(struct path_cxt *pc, struct proc *proc, + bool sockets_only) +{ + enum association assocs[] = { ASSOC_EXE }; + const char *names[] = { + [ASSOC_EXE] = "exe", + }; + collect_outofbox_files(pc, proc, assocs, names, ARRAY_SIZE(assocs), + sockets_only); +} + +static void collect_fs_files(struct path_cxt *pc, struct proc *proc, + bool sockets_only) +{ + enum association assocs[] = { ASSOC_EXE, ASSOC_CWD, ASSOC_ROOT }; + const char *names[] = { + [ASSOC_CWD] = "cwd", + [ASSOC_ROOT] = "root", + }; + collect_outofbox_files(pc, proc, assocs, names, ARRAY_SIZE(assocs), + sockets_only); +} + +static void collect_namespace_files(struct path_cxt *pc, struct proc *proc) +{ + enum association assocs[] = { + ASSOC_NS_CGROUP, + ASSOC_NS_IPC, + ASSOC_NS_MNT, + ASSOC_NS_NET, + ASSOC_NS_PID, + ASSOC_NS_PID4C, + ASSOC_NS_TIME, + ASSOC_NS_TIME4C, + ASSOC_NS_USER, + ASSOC_NS_UTS, + }; + const char *names[] = { + [ASSOC_NS_CGROUP] = "ns/cgroup", + [ASSOC_NS_IPC] = "ns/ipc", + [ASSOC_NS_MNT] = "ns/mnt", + [ASSOC_NS_NET] = "ns/net", + [ASSOC_NS_PID] = "ns/pid", + [ASSOC_NS_PID4C] = "ns/pid_for_children", + [ASSOC_NS_TIME] = "ns/time", + [ASSOC_NS_TIME4C] = "ns/time_for_children", + [ASSOC_NS_USER] = "ns/user", + [ASSOC_NS_UTS] = "ns/uts", + }; + collect_outofbox_files(pc, proc, assocs, names, ARRAY_SIZE(assocs), + /* Namespace information is alwasys needed. */ + false); +} + +static struct nodev *new_nodev(unsigned long minor, const char *filesystem) +{ + struct nodev *nodev = xcalloc(1, sizeof(*nodev)); + + INIT_LIST_HEAD(&nodev->nodevs); + nodev->minor = minor; + nodev->filesystem = xstrdup(filesystem); + + return nodev; +} + +static void free_nodev(struct nodev *nodev) +{ + free(nodev->filesystem); + free(nodev); +} + +static void initialize_nodevs(void) +{ + int i; + + for (i = 0; i < NODEV_TABLE_SIZE; i++) + INIT_LIST_HEAD(&nodev_table.tables[i]); +} + +static void finalize_nodevs(void) +{ + int i; + + for (i = 0; i < NODEV_TABLE_SIZE; i++) + list_free(&nodev_table.tables[i], struct nodev, nodevs, free_nodev); + + free(mnt_namespaces); +} + +const char *get_nodev_filesystem(unsigned long minor) +{ + struct list_head *n; + int slot = minor % NODEV_TABLE_SIZE; + + list_for_each (n, &nodev_table.tables[slot]) { + struct nodev *nodev = list_entry(n, struct nodev, nodevs); + if (nodev->minor == minor) + return nodev->filesystem; + } + return NULL; +} + +static void add_nodevs(FILE *mnt) +{ + /* This can be very long. A line in mountinfo can have more than 3 + * paths. */ + char line[PATH_MAX * 3 + 256]; + + while (fgets(line, sizeof(line), mnt)) { + unsigned long major, minor; + char filesystem[256]; + struct nodev *nodev; + int slot; + + + /* 23 61 0:22 / /sys rw,nosuid,nodev,noexec,relatime shared:2 - sysfs sysfs rw,seclabel */ + if(sscanf(line, "%*d %*d %lu:%lu %*s %*s %*s %*[^-] - %s %*[^\n]", + &major, &minor, filesystem) != 3) + /* 1600 1458 0:55 / / rw,nodev,relatime - overlay overlay rw,context="s... */ + if (sscanf(line, "%*d %*d %lu:%lu %*s %*s %*s - %s %*[^\n]", + &major, &minor, filesystem) != 3) + continue; + + if (major != 0) + continue; + if (get_nodev_filesystem(minor)) + continue; + + nodev = new_nodev(minor, filesystem); + slot = minor % NODEV_TABLE_SIZE; + + list_add_tail(&nodev->nodevs, &nodev_table.tables[slot]); + } +} + +static void initialize_ipc_table(void) +{ + for (int i = 0; i < IPC_TABLE_SIZE; i++) + INIT_LIST_HEAD(ipc_table.tables + i); +} + +static void free_ipc(struct ipc *ipc) +{ + if (ipc->class->free) + ipc->class->free(ipc); + free(ipc); +} + +static void finalize_ipc_table(void) +{ + for (int i = 0; i < IPC_TABLE_SIZE; i++) + list_free(&ipc_table.tables[i], struct ipc, ipcs, free_ipc); +} + +struct ipc *get_ipc(struct file *file) +{ + int slot; + struct list_head *e; + const struct ipc_class *ipc_class; + + if (!file->class->get_ipc_class) + return NULL; + + ipc_class = file->class->get_ipc_class(file); + if (!ipc_class) + return NULL; + + slot = ipc_class->get_hash(file) % IPC_TABLE_SIZE; + list_for_each (e, &ipc_table.tables[slot]) { + struct ipc *ipc = list_entry(e, struct ipc, ipcs); + if (ipc->class != ipc_class) + continue; + if (ipc_class->is_suitable_ipc(ipc, file)) + return ipc; + } + return NULL; +} + +void add_ipc(struct ipc *ipc, unsigned int hash) +{ + int slot = hash % IPC_TABLE_SIZE; + list_add(&ipc->ipcs, &ipc_table.tables[slot]); +} + +static void fill_column(struct proc *proc, + struct file *file, + struct libscols_line *ln, + int column_id, + size_t column_index) +{ + const struct file_class *class = file->class; + + while (class) { + if (class->fill_column + && class->fill_column(proc, file, ln, + column_id, column_index)) + break; + class = class->super; + } +} + +static void convert_file(struct proc *proc, + struct file *file, + struct libscols_line *ln) + +{ + size_t i; + + for (i = 0; i < ncolumns; i++) + fill_column(proc, file, ln, get_column_id(i), i); +} + +static void convert(struct list_head *procs, struct lsfd_control *ctl) +{ + struct list_head *p; + + list_for_each (p, procs) { + struct proc *proc = list_entry(p, struct proc, procs); + struct list_head *f; + + list_for_each (f, &proc->files) { + struct file *file = list_entry(f, struct file, files); + struct libscols_line *ln = scols_table_new_line(ctl->tb, NULL); + struct lsfd_counter **counter = NULL; + + if (!ln) + err(EXIT_FAILURE, _("failed to allocate output line")); + + convert_file(proc, file, ln); + + if (!lsfd_filter_apply(ctl->filter, ln)) { + scols_table_remove_line(ctl->tb, ln); + continue; + } + + if (!ctl->counters) + continue; + + for (counter = ctl->counters; *counter; counter++) + lsfd_counter_accumulate(*counter, ln); + } + } +} + +static void delete(struct list_head *procs, struct lsfd_control *ctl) +{ + struct list_head *p; + + list_for_each (p, procs) { + struct proc *proc = list_entry(p, struct proc, procs); + tdelete(proc, &proc_tree, proc_tree_compare); + } + list_free(procs, struct proc, procs, free_proc); + + scols_unref_table(ctl->tb); + lsfd_filter_free(ctl->filter); + if (ctl->counters) { + struct lsfd_counter **counter; + for (counter = ctl->counters; *counter; counter++) + lsfd_counter_free(*counter); + free(ctl->counters); + } +} + +static void emit(struct lsfd_control *ctl) +{ + scols_print_table(ctl->tb); +} + + +static void initialize_class(const struct file_class *class) +{ + if (class->initialize_class) + class->initialize_class(); +} + +static void initialize_classes(void) +{ + initialize_class(&file_class); + initialize_class(&cdev_class); + initialize_class(&bdev_class); + initialize_class(&sock_class); + initialize_class(&unkn_class); +} + +static void finalize_class(const struct file_class *class) +{ + if (class->finalize_class) + class->finalize_class(); +} + +static void finalize_classes(void) +{ + finalize_class(&file_class); + finalize_class(&cdev_class); + finalize_class(&bdev_class); + finalize_class(&sock_class); + finalize_class(&unkn_class); +} + +static struct devdrv *new_devdrv(unsigned long major, const char *name) +{ + struct devdrv *devdrv = xcalloc(1, sizeof(*devdrv)); + + INIT_LIST_HEAD(&devdrv->devdrvs); + + devdrv->major = major; + devdrv->name = xstrdup(name); + + return devdrv; +} + +static void free_devdrv(struct devdrv *devdrv) +{ + free(devdrv->name); + free(devdrv); +} + +#define READ_DEVICES_LINE_LEN 256 +static struct devdrv *read_devdrv(const char *line) +{ + unsigned long major; + char name[READ_DEVICES_LINE_LEN]; + + if (sscanf(line, "%lu %s", &major, name) != 2) + return NULL; + + return new_devdrv(major, name); +} + +static void read_devices(struct list_head *chrdrvs_list, + struct list_head *blkdrvs_list, FILE *devices_fp) +{ + char line[READ_DEVICES_LINE_LEN]; + + /* Skip to the line "Character devices:". */ + while (fgets(line, sizeof(line), devices_fp)) { + if (line[0] == 'C') + break; + continue; + } + + while (fgets(line, sizeof(line), devices_fp)) { + /* Find the blank line before "Block devices:" line. */ + if (line[0] == '\n') + break; + + /* Read the character device drivers */ + struct devdrv *devdrv = read_devdrv(line); + if (devdrv) + list_add_tail(&devdrv->devdrvs, chrdrvs_list); + } + + /* Skip to the line "Block devices:". */ + while (fgets(line, sizeof(line), devices_fp)) { + if (line[0] == 'B') + break; + continue; + } + + /* Read the block device drivers */ + while (fgets(line, sizeof(line), devices_fp)) { + struct devdrv *devdrv = read_devdrv(line); + if (devdrv) + list_add_tail(&devdrv->devdrvs, blkdrvs_list); + } +} + +static void initialize_devdrvs(void) +{ + FILE *devices_fp; + + INIT_LIST_HEAD(&chrdrvs); + INIT_LIST_HEAD(&blkdrvs); + + devices_fp = fopen("/proc/devices", "r"); + if (devices_fp) { + read_devices(&chrdrvs, &blkdrvs, devices_fp); + fclose(devices_fp); + } +} + +static void finalize_devdrvs(void) +{ + list_free(&blkdrvs, struct devdrv, devdrvs, free_devdrv); + list_free(&chrdrvs, struct devdrv, devdrvs, free_devdrv); +} + +static const char *get_devdrv(struct list_head *devdrvs_list, unsigned long major) +{ + struct list_head *c; + list_for_each(c, devdrvs_list) { + struct devdrv *devdrv = list_entry(c, struct devdrv, devdrvs); + if (devdrv->major == major) + return devdrv->name; + } + return NULL; +} + +const char *get_chrdrv(unsigned long major) +{ + return get_devdrv(&chrdrvs, major); +} + +const char *get_blkdrv(unsigned long major) +{ + return get_devdrv(&blkdrvs, major); +} + +struct name_manager *new_name_manager(void) +{ + struct name_manager *nm = xcalloc(1, sizeof(struct name_manager)); + + nm->cache = new_idcache(); + if (!nm->cache) + err(EXIT_FAILURE, _("failed to allocate an idcache")); + + nm->next_id = 1; /* 0 is never issued as id. */ + return nm; +} + +void free_name_manager(struct name_manager *nm) +{ + free_idcache(nm->cache); + free(nm); +} + +const char *get_name(struct name_manager *nm, unsigned long id) +{ + struct identry *e; + + e = get_id(nm->cache, id); + + return e? e->name: NULL; +} + +unsigned long add_name(struct name_manager *nm, const char *name) +{ + struct identry *e = NULL, *tmp; + + for (tmp = nm->cache->ent; tmp; tmp = tmp->next) { + if (strcmp(tmp->name, name) == 0) { + e = tmp; + break; + } + } + + if (e) + return e->id; + + e = xmalloc(sizeof(struct identry)); + e->name = xstrdup(name); + e->id = nm->next_id++; + e->next = nm->cache->ent; + nm->cache->ent = e; + + return e->id; +} + +static void read_process(struct lsfd_control *ctl, struct path_cxt *pc, + pid_t pid, struct proc *leader) +{ + char buf[BUFSIZ]; + struct proc *proc; + + if (procfs_process_init_path(pc, pid) != 0) + return; + + proc = new_process(pid, leader); + proc->command = procfs_process_get_cmdname(pc, buf, sizeof(buf)) > 0 ? + xstrdup(buf) : xstrdup(_("(unknown)")); + procfs_process_get_uid(pc, &proc->uid); + + if (procfs_process_get_stat(pc, buf, sizeof(buf)) > 0) { + char *p; + unsigned int flags; + char *pat = NULL; + + /* See proc(5) about the column in the line. */ + xstrappend(&pat, "%*d ("); + for (p = proc->command; *p != '\0'; p++) { + if (*p == '%') + xstrappend(&pat, "%%"); + else + xstrputc(&pat, *p); + } + xstrappend(&pat, ") %*c %*d %*d %*d %*d %*d %u %*[^\n]"); + if (sscanf(buf, pat, &flags) == 1) + proc->kthread = !!(flags & PF_KTHREAD); + free(pat); + } + + collect_execve_file(pc, proc, ctl->sockets_only); + + if (proc->pid == proc->leader->pid + || kcmp(proc->leader->pid, proc->pid, KCMP_FS, 0, 0) != 0) + collect_fs_files(pc, proc, ctl->sockets_only); + + if (proc->ns_mnt == 0 || !has_mnt_ns(proc->ns_mnt)) { + FILE *mnt = ul_path_fopen(pc, "r", "mountinfo"); + if (mnt) { + add_nodevs(mnt); + if (proc->ns_mnt) + add_mnt_ns(proc->ns_mnt); + fclose(mnt); + } + } + + collect_namespace_files(pc, proc); + + /* If kcmp is not available, + * there is no way to no whether threads share resources. + * In such cases, we must pay the costs: call collect_mem_files() + * and collect_fd_files(). + */ + if ((!ctl->sockets_only) + && (proc->pid == proc->leader->pid + || kcmp(proc->leader->pid, proc->pid, KCMP_VM, 0, 0) != 0)) + collect_mem_files(pc, proc); + + if (proc->pid == proc->leader->pid + || kcmp(proc->leader->pid, proc->pid, KCMP_FILES, 0, 0) != 0) + collect_fd_files(pc, proc, ctl->sockets_only); + + list_add_tail(&proc->procs, &ctl->procs); + if (tsearch(proc, &proc_tree, proc_tree_compare) == NULL) + errx(EXIT_FAILURE, _("failed to allocate memory")); + + /* The tasks collecting overwrites @pc by /proc/<task-pid>/. Keep it as + * the last path based operation in read_process() + */ + if (ctl->threads && leader == NULL) { + DIR *sub = NULL; + pid_t tid = 0; + + while (procfs_process_next_tid(pc, &sub, &tid) == 0) { + if (tid == pid) + continue; + read_process(ctl, pc, tid, proc); + } + } + + /* Let's be careful with number of open files */ + ul_path_close_dirfd(pc); +} + +static void parse_pids(const char *str, pid_t **pids, int *count) +{ + long v; + char *next = NULL; + + if (*str == '\0') + return; + + errno = 0; + v = strtol(str, &next, 10); + if (errno) + err(EXIT_FAILURE, _("unexpected value for pid specification: %s"), str); + if (next == str) + errx(EXIT_FAILURE, _("garbage at the end of pid specification: %s"), str); + if (v < 0) + errx(EXIT_FAILURE, _("out of range value for pid specification: %ld"), v); + + (*count)++; + *pids = xrealloc(*pids, (*count) * sizeof(**pids)); + (*pids)[*count - 1] = (pid_t)v; + + while (next && *next != '\0' + && (isspace((unsigned char)*next) || *next == ',')) + next++; + if (*next != '\0') + parse_pids(next, pids, count); +} + +static int pidcmp(const void *a, const void *b) +{ + pid_t pa = *(pid_t *)a; + pid_t pb = *(pid_t *)b; + + if (pa < pb) + return -1; + else if (pa == pb) + return 0; + else + return 1; +} + +static void sort_pids(pid_t pids[], const int count) +{ + qsort(pids, count, sizeof(pid_t), pidcmp); +} + +static bool member_pids(const pid_t pid, const pid_t pids[], const int count) +{ + return bsearch(&pid, pids, count, sizeof(pid_t), pidcmp)? true: false; +} + +static void collect_processes(struct lsfd_control *ctl, const pid_t pids[], int n_pids) +{ + DIR *dir; + struct dirent *d; + struct path_cxt *pc = NULL; + + pc = ul_new_path(NULL); + if (!pc) + err(EXIT_FAILURE, _("failed to alloc procfs handler")); + + dir = opendir(_PATH_PROC); + if (!dir) + err(EXIT_FAILURE, _("failed to open /proc")); + + while ((d = readdir(dir))) { + pid_t pid; + + if (procfs_dirent_get_pid(d, &pid) != 0) + continue; + if (n_pids == 0 || member_pids(pid, pids, n_pids)) + read_process(ctl, pc, pid, 0); + } + + closedir(dir); + ul_unref_path(pc); +} + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + size_t i; + + fputs(USAGE_HEADER, out); + fprintf(out, _(" %s [options]\n"), program_invocation_short_name); + + fputs(USAGE_OPTIONS, out); + fputs(_(" -l, --threads list in threads level\n"), out); + fputs(_(" -J, --json use JSON output format\n"), out); + fputs(_(" -n, --noheadings don't print headings\n"), out); + fputs(_(" -o, --output <list> output columns\n"), out); + fputs(_(" -r, --raw use raw output format\n"), out); + fputs(_(" -u, --notruncate don't truncate text in columns\n"), out); + fputs(_(" -p, --pid <pid(s)> collect information only specified processes\n"), out); + fputs(_(" -i[4|6], --inet[=4|6] list only IPv4 and/or IPv6 sockets\n"), out); + fputs(_(" -Q, --filter <expr> apply display filter\n"), out); + fputs(_(" --debug-filter dump the internal data structure of filter and exit\n"), out); + fputs(_(" -C, --counter <name>:<expr>\n" + " define custom counter for --summary output\n"), out); + fputs(_(" --dump-counters dump counter definitions\n"), out); + fputs(_(" --summary[=<when>] print summary information (only, append, or never)\n"), out); + + fputs(USAGE_SEPARATOR, out); + printf(USAGE_HELP_OPTIONS(30)); + + fprintf(out, USAGE_COLUMNS); + + for (i = 0; i < ARRAY_SIZE(infos); i++) + fprintf(out, " %16s %-10s%s\n", infos[i].name, + infos[i].json_type == SCOLS_JSON_STRING? "<string>": + infos[i].json_type == SCOLS_JSON_ARRAY_STRING? "<string>": + infos[i].json_type == SCOLS_JSON_NUMBER? "<number>": + "<boolean>", + _(infos[i].help)); + + printf(USAGE_MAN_TAIL("lsfd(1)")); + + exit(EXIT_SUCCESS); +} + +static void append_filter_expr(char **a, const char *b, bool and) +{ + if (*a == NULL) { + *a = xstrdup(b); + return; + } + + char *tmp = *a; + *a = NULL; + + xstrappend(a, "("); + xstrappend(a, tmp); + xstrappend(a, ")"); + if (and) + xstrappend(a, "and("); + else + xstrappend(a, "or("); + xstrappend(a, b); + xstrappend(a, ")"); + + free(tmp); +} + +static struct lsfd_filter *new_filter(const char *expr, bool debug, const char *err_prefix, struct lsfd_control *ctl) +{ + struct lsfd_filter *filter; + const char *errmsg; + + filter = lsfd_filter_new(expr, ctl->tb, + LSFD_N_COLS, + column_name_to_id_cb, + add_column_by_id_cb, ctl); + errmsg = lsfd_filter_get_errmsg(filter); + if (errmsg) + errx(EXIT_FAILURE, "%s%s", err_prefix, errmsg); + if (debug) { + lsfd_filter_dump(filter, stdout); + exit(EXIT_SUCCESS); + } + + return filter; +} + +static struct counter_spec *new_counter_spec(const char *spec_str) +{ + char *sep; + struct counter_spec *spec; + + if (spec_str[0] == '\0') + errx(EXIT_FAILURE, + _("too short counter specification: -C/--counter %s"), + spec_str); + if (spec_str[0] == ':') + errx(EXIT_FAILURE, + _("no name for counter: -C/--counter %s"), + spec_str); + + sep = strchr(spec_str, ':'); + if (sep == NULL) + errx(EXIT_FAILURE, + _("no name for counter: -C/--counter %s"), + spec_str); + if (sep[1] == '\0') + errx(EXIT_FAILURE, + _("empty counter expression given: -C/--counter %s"), + spec_str); + + /* Split the spec_str in to name and expr. */ + *sep = '\0'; + + if (strchr(spec_str, '{')) + errx(EXIT_FAILURE, + _("don't use `{' in the name of a counter: %s"), + spec_str); + + spec = xmalloc(sizeof(struct counter_spec)); + INIT_LIST_HEAD(&spec->specs); + spec->name = spec_str; + spec->expr = sep + 1; + + return spec; +} + +static void free_counter_spec(struct counter_spec *counter_spec) +{ + free(counter_spec); +} + +static struct lsfd_counter *new_counter(const struct counter_spec *spec, struct lsfd_control *ctl) +{ + struct lsfd_filter *filter; + + filter = new_filter(spec->expr, false, + _("failed in making filter for a counter: "), + ctl); + return lsfd_counter_new(spec->name, filter); +} + +static struct lsfd_counter **new_counters(struct list_head *specs, struct lsfd_control *ctl) +{ + struct lsfd_counter **counters; + size_t len = list_count_entries(specs); + size_t i = 0; + struct list_head *s; + + counters = xcalloc(len + 1, sizeof(struct lsfd_counter *)); + list_for_each(s, specs) { + struct counter_spec *spec = list_entry(s, struct counter_spec, specs); + counters[i++] = new_counter(spec, ctl); + } + assert(counters[len] == NULL); + + return counters; +} + +static struct lsfd_counter **new_default_counters(struct lsfd_control *ctl) +{ + struct lsfd_counter **counters; + size_t len = ARRAY_SIZE(default_counter_specs); + size_t i; + + counters = xcalloc(len + 1, sizeof(struct lsfd_counter *)); + for (i = 0; i < len; i++) { + const struct counter_spec *spec = default_counter_specs + i; + counters[i] = new_counter(spec, ctl); + } + assert(counters[len] == NULL); + + return counters; +} + +static void dump_default_counter_specs(void) +{ + size_t len = ARRAY_SIZE(default_counter_specs); + size_t i; + + puts("default counter specs:"); + for (i = 0; i < len; i++) { + const struct counter_spec *spec = default_counter_specs + i; + printf("\t%s:%s\n", spec->name, spec->expr); + } +} + +static void dump_counter_specs(struct list_head *specs) +{ + struct list_head *s; + + puts("custom counter specs:"); + list_for_each(s, specs) { + struct counter_spec *spec = list_entry(s, struct counter_spec, specs); + printf("\t%s:%s\n", spec->name, spec->expr); + } +} + +static struct libscols_table *new_summary_table(struct lsfd_control *ctl) +{ + struct libscols_table *tb = scols_new_table(); + + struct libscols_column *name_cl, *value_cl; + + if (!tb) + err(EXIT_FAILURE, _("failed to allocate summary table")); + + scols_table_enable_noheadings(tb, ctl->noheadings); + scols_table_enable_raw(tb, ctl->raw); + scols_table_enable_json(tb, ctl->json); + + if(ctl->json) + scols_table_set_name(tb, "lsfd-summary"); + + + value_cl = scols_table_new_column(tb, _("VALUE"), 0, SCOLS_FL_RIGHT); + if (!value_cl) + err(EXIT_FAILURE, _("failed to allocate summary column")); + if (ctl->json) + scols_column_set_json_type(value_cl, SCOLS_JSON_NUMBER); + + name_cl = scols_table_new_column(tb, _("COUNTER"), 0, 0); + if (!name_cl) + err(EXIT_FAILURE, _("failed to allocate summary column")); + if (ctl->json) + scols_column_set_json_type(name_cl, SCOLS_JSON_STRING); + + return tb; +} + +static void fill_summary_line(struct libscols_line *ln, struct lsfd_counter *counter) +{ + char *str = NULL; + + xasprintf(&str, "%llu", (unsigned long long)lsfd_counter_value(counter)); + if (!str) + err(EXIT_FAILURE, _("failed to add summary data")); + if (scols_line_refer_data(ln, 0, str)) + err(EXIT_FAILURE, _("failed to add summary data")); + + if (scols_line_set_data(ln, 1, lsfd_counter_name(counter))) + err(EXIT_FAILURE, _("failed to add summary data")); +} + +static void emit_summary(struct lsfd_control *ctl, struct lsfd_counter **counter) +{ + struct libscols_table *tb = new_summary_table(ctl); + + for (; *counter; counter++) { + struct libscols_line *ln = scols_table_new_line(tb, NULL); + fill_summary_line(ln, *counter); + } + scols_print_table(tb); + + scols_unref_table(tb); +} + +static void attach_xinfos(struct list_head *procs) +{ + struct list_head *p; + + list_for_each (p, procs) { + struct proc *proc = list_entry(p, struct proc, procs); + struct list_head *f; + + list_for_each (f, &proc->files) { + struct file *file = list_entry(f, struct file, files); + if (file->class->attach_xinfo) + file->class->attach_xinfo(file); + } + } +} + +/* Filter expressions for implementing -i option. + * + * To list up the protocol names, use the following command line + * + * cd linux/net; + * find . -type f -exec grep -A 1 --color=auto -nH --null -e 'struct proto .*{' \{\} + + * + */ +#define INET_SUBEXP_BEGIN "(SOCK.PROTONAME =~ \"^(" +#define INET4_REG "TCP|UDP|RAW|PING|UDP-Lite|SCTP|DCCP|L2TP/IP|SMC" +#define INET6_REG "TCPv6|UDPv6|RAWv6|PINGv6|UDPLITEv6|SCTPv6|DCCPv6|L2TP/IPv6|SMC6" +#define INET_SUBEXP_END ")$\")" + +static const char *inet4_subexpr = INET_SUBEXP_BEGIN + INET4_REG + INET_SUBEXP_END; +static const char *inet6_subexpr = INET_SUBEXP_BEGIN + INET6_REG + INET_SUBEXP_END; +static const char *inet46_subexpr = INET_SUBEXP_BEGIN + INET4_REG "|" INET6_REG + INET_SUBEXP_END; + +int main(int argc, char *argv[]) +{ + int c; + size_t i; + char *outarg = NULL; + char *filter_expr = NULL; + bool debug_filter = false; + bool dump_counters = false; + pid_t *pids = NULL; + int n_pids = 0; + struct list_head counter_specs; + + struct lsfd_control ctl = { + .show_main = 1 + }; + + INIT_LIST_HEAD(&counter_specs); + + enum { + OPT_DEBUG_FILTER = CHAR_MAX + 1, + OPT_SUMMARY, + OPT_DUMP_COUNTERS, + }; + static const struct option longopts[] = { + { "noheadings", no_argument, NULL, 'n' }, + { "output", required_argument, NULL, 'o' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + { "json", no_argument, NULL, 'J' }, + { "raw", no_argument, NULL, 'r' }, + { "threads", no_argument, NULL, 'l' }, + { "notruncate", no_argument, NULL, 'u' }, + { "pid", required_argument, NULL, 'p' }, + { "inet", optional_argument, NULL, 'i' }, + { "filter", required_argument, NULL, 'Q' }, + { "debug-filter",no_argument, NULL, OPT_DEBUG_FILTER }, + { "summary", optional_argument, NULL, OPT_SUMMARY }, + { "counter", required_argument, NULL, 'C' }, + { "dump-counters",no_argument, NULL, OPT_DUMP_COUNTERS }, + { NULL, 0, NULL, 0 }, + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + close_stdout_atexit(); + + while ((c = getopt_long(argc, argv, "no:JrVhluQ:p:i::C:s", longopts, NULL)) != -1) { + switch (c) { + case 'n': + ctl.noheadings = 1; + break; + case 'o': + outarg = optarg; + break; + case 'J': + ctl.json = 1; + break; + case 'r': + ctl.raw = 1; + break; + case 'l': + ctl.threads = 1; + break; + case 'u': + ctl.notrunc = 1; + break; + case 'p': + parse_pids(optarg, &pids, &n_pids); + break; + case 'i': { + const char *subexpr = NULL; + + ctl.sockets_only = 1; + if (optarg == NULL) + subexpr = inet46_subexpr; + else if (strcmp(optarg, "4") == 0) + subexpr = inet4_subexpr; + else if (strcmp(optarg, "6") == 0) + subexpr = inet6_subexpr; + else + errx(EXIT_FAILURE, + _("unknown -i/--inet argument: %s"), + optarg); + + append_filter_expr(&filter_expr, subexpr, true); + break; + } + case 'Q': + append_filter_expr(&filter_expr, optarg, true); + break; + case 'C': { + struct counter_spec *c = new_counter_spec(optarg); + list_add_tail(&c->specs, &counter_specs); + break; + } + case OPT_DEBUG_FILTER: + debug_filter = true; + break; + case OPT_SUMMARY: + if (optarg) { + if (strcmp(optarg, "never") == 0) + ctl.show_summary = 0, ctl.show_main = 1; + else if (strcmp(optarg, "only") == 0) + ctl.show_summary = 1, ctl.show_main = 0; + else if (strcmp(optarg, "append") == 0) + ctl.show_summary = 1, ctl.show_main = 1; + else + errx(EXIT_FAILURE, _("unsupported --summary argument")); + } else + ctl.show_summary = 1, ctl.show_main = 0; + break; + case OPT_DUMP_COUNTERS: + dump_counters = true; + break; + case 'V': + print_version(EXIT_SUCCESS); + case 'h': + usage(); + default: + errtryhelp(EXIT_FAILURE); + } + } + if (argv[optind]) + errtryhelp(EXIT_FAILURE); + +#define INITIALIZE_COLUMNS(COLUMN_SPEC) \ + for (i = 0; i < ARRAY_SIZE(COLUMN_SPEC); i++) \ + columns[ncolumns++] = COLUMN_SPEC[i] + if (!ncolumns) { + if (ctl.threads) + INITIALIZE_COLUMNS(default_threads_columns); + else + INITIALIZE_COLUMNS(default_columns); + } + + if (outarg && string_add_to_idarray(outarg, columns, ARRAY_SIZE(columns), + &ncolumns, column_name_to_id) < 0) + return EXIT_FAILURE; + + scols_init_debug(0); + + INIT_LIST_HEAD(&ctl.procs); + + /* inilialize scols table */ + ctl.tb = scols_new_table(); + if (!ctl.tb) + err(EXIT_FAILURE, _("failed to allocate output table")); + + scols_table_enable_noheadings(ctl.tb, ctl.noheadings); + scols_table_enable_raw(ctl.tb, ctl.raw); + scols_table_enable_json(ctl.tb, ctl.json); + if (ctl.json) + scols_table_set_name(ctl.tb, "lsfd"); + + /* create output columns */ + for (i = 0; i < ncolumns; i++) { + const struct colinfo *col = get_column_info(i); + struct libscols_column *cl = add_column(ctl.tb, col); + + if (!cl) + err(EXIT_FAILURE, _("failed to allocate output column")); + + if (ctl.notrunc) { + int flags = scols_column_get_flags(cl); + flags &= ~SCOLS_FL_TRUNC; + scols_column_set_flags(cl, flags); + } + } + + /* make fitler */ + if (filter_expr) { + ctl.filter = new_filter(filter_expr, debug_filter, "", &ctl); + free(filter_expr); + } + + if (dump_counters) { + if (list_empty(&counter_specs)) + dump_default_counter_specs(); + else + dump_counter_specs(&counter_specs); + return 0; + } + + /* make counters */ + if (ctl.show_summary) { + if (list_empty(&counter_specs)) + ctl.counters = new_default_counters(&ctl); + else { + ctl.counters = new_counters(&counter_specs, &ctl); + list_free(&counter_specs, struct counter_spec, specs, + free_counter_spec); + } + } + + if (n_pids > 0) + sort_pids(pids, n_pids); + + /* collect data */ + initialize_nodevs(); + initialize_classes(); + initialize_devdrvs(); + initialize_ipc_table(); + + collect_processes(&ctl, pids, n_pids); + free(pids); + + attach_xinfos(&ctl.procs); + + convert(&ctl.procs, &ctl); + + /* print */ + if (ctl.show_main) + emit(&ctl); + + if (ctl.show_summary && ctl.counters) + emit_summary(&ctl, ctl.counters); + + /* cleanup */ + delete(&ctl.procs, &ctl); + + finalize_ipc_table(); + finalize_devdrvs(); + finalize_classes(); + finalize_nodevs(); + + return 0; +} |