/* * lslocks(8) - list local system locks * * Copyright (C) 2012 Davidlohr Bueso * * Very generally based on lslk(8) by Victor A. Abell * Since it stopped being maintained over a decade ago, this * program should be considered its replacement. * * 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 #include #include #include #include #include #include #include #include #include #include #include "pathnames.h" #include "canonicalize.h" #include "nls.h" #include "xalloc.h" #include "strutils.h" #include "c.h" #include "list.h" #include "closestream.h" #include "optutils.h" #include "procfs.h" /* column IDs */ enum { COL_SRC = 0, COL_PID, COL_TYPE, COL_SIZE, COL_INODE, COL_MAJMIN, COL_MODE, COL_M, COL_START, COL_END, COL_PATH, COL_BLOCKER }; /* column names */ struct colinfo { const char *name; /* header */ double whint; /* width hint (N < 1 is in percent of termwidth) */ int flags; /* SCOLS_FL_* */ const char *help; }; /* columns descriptions */ static struct colinfo infos[] = { [COL_SRC] = { "COMMAND",15, 0, N_("command of the process holding the lock") }, [COL_PID] = { "PID", 5, SCOLS_FL_RIGHT, N_("PID of the process holding the lock") }, [COL_TYPE] = { "TYPE", 5, SCOLS_FL_RIGHT, N_("kind of lock") }, [COL_SIZE] = { "SIZE", 4, SCOLS_FL_RIGHT, N_("size of the lock") }, [COL_INODE] = { "INODE", 5, SCOLS_FL_RIGHT, N_("inode number") }, [COL_MAJMIN] = { "MAJ:MIN", 6, 0, N_("major:minor device number") }, [COL_MODE] = { "MODE", 5, 0, N_("lock access mode") }, [COL_M] = { "M", 1, 0, N_("mandatory state of the lock: 0 (none), 1 (set)")}, [COL_START] = { "START", 10, SCOLS_FL_RIGHT, N_("relative byte offset of the lock")}, [COL_END] = { "END", 10, SCOLS_FL_RIGHT, N_("ending offset of the lock")}, [COL_PATH] = { "PATH", 0, SCOLS_FL_TRUNC, N_("path of the locked file")}, [COL_BLOCKER] = { "BLOCKER", 0, SCOLS_FL_RIGHT, N_("PID of the process blocking the lock") } }; static int columns[ARRAY_SIZE(infos) * 2]; static size_t ncolumns; static pid_t pid = 0; static struct libmnt_table *tab; /* /proc/self/mountinfo */ /* basic output flags */ static int no_headings; static int no_inaccessible; static int raw; static int json; static int bytes; struct lock { struct list_head locks; char *cmdname; pid_t pid; char *path; char *type; char *mode; off_t start; off_t end; ino_t inode; dev_t dev; unsigned int mandatory :1, blocked :1; uint64_t size; int id; }; static void rem_lock(struct lock *lock) { if (!lock) return; free(lock->path); free(lock->mode); free(lock->cmdname); free(lock->type); list_del(&lock->locks); free(lock); } static void disable_columns_truncate(void) { size_t i; for (i = 0; i < ARRAY_SIZE(infos); i++) infos[i].flags &= ~SCOLS_FL_TRUNC; } /* * Associate the device's mountpoint for a filename */ static char *get_fallback_filename(dev_t dev) { struct libmnt_fs *fs; char *res = NULL; if (!tab) { tab = mnt_new_table_from_file(_PATH_PROC_MOUNTINFO); if (!tab) return NULL; } fs = mnt_table_find_devno(tab, dev, MNT_ITER_BACKWARD); if (!fs) return NULL; xasprintf(&res, "%s...", mnt_fs_get_target(fs)); return res; } /* * Return the absolute path of a file from * a given inode number (and its size) */ static char *get_filename_sz(ino_t inode, pid_t lock_pid, size_t *size) { struct stat sb; struct dirent *dp; DIR *dirp; size_t len; int fd; char path[PATH_MAX], sym[PATH_MAX], *ret = NULL; *size = 0; memset(path, 0, sizeof(path)); memset(sym, 0, sizeof(sym)); /* * We know the pid so we don't have to * iterate the *entire* filesystem searching * for the damn file. */ snprintf(path, sizeof(path), "/proc/%d/fd/", lock_pid); if (!(dirp = opendir(path))) return NULL; if ((len = strlen(path)) >= (sizeof(path) - 2)) goto out; if ((fd = dirfd(dirp)) < 0 ) goto out; while ((dp = readdir(dirp))) { if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue; errno = 0; /* care only for numerical descriptors */ if (!strtol(dp->d_name, (char **) NULL, 10) || errno) continue; if (!fstatat(fd, dp->d_name, &sb, 0) && inode != sb.st_ino) continue; if ((len = readlinkat(fd, dp->d_name, sym, sizeof(sym) - 1)) < 1) goto out; *size = sb.st_size; sym[len] = '\0'; ret = xstrdup(sym); break; } out: closedir(dirp); return ret; } /* * Return the inode number from a string */ static ino_t get_dev_inode(char *str, dev_t *dev) { unsigned int maj = 0, min = 0; ino_t inum = 0; if (sscanf(str, "%x:%x:%ju", &maj, &min, &inum) != 3) errx(EXIT_FAILURE, _("failed to parse '%s'"), str); *dev = (dev_t) makedev(maj, min); return inum; } static int get_local_locks(struct list_head *locks) { int i; FILE *fp; char buf[PATH_MAX], *tok = NULL; size_t sz; struct lock *l; if (!(fp = fopen(_PATH_PROC_LOCKS, "r"))) return -1; while (fgets(buf, sizeof(buf), fp)) { l = xcalloc(1, sizeof(*l)); INIT_LIST_HEAD(&l->locks); for (tok = strtok(buf, " "), i = 0; tok; tok = strtok(NULL, " "), i++) { /* * /proc/locks has *exactly* 8 "blocks" of text * separated by ' ' - check /fs/locks.c */ switch (i) { case 0: /* ID: */ tok[strlen(tok) - 1] = '\0'; l->id = strtos32_or_err(tok, _("failed to parse ID")); break; case 1: /* posix, flock, etc */ if (strcmp(tok, "->") == 0) { /* optional field */ l->blocked = 1; i--; } else l->type = xstrdup(tok); break; case 2: /* is this a mandatory lock? other values are advisory or noinode */ l->mandatory = *tok == 'M' ? 1 : 0; break; case 3: /* lock mode */ l->mode = xstrdup(tok); break; case 4: /* PID */ /* * If user passed a pid we filter it later when adding * to the list, no need to worry now. OFD locks use -1 PID. */ l->pid = strtos32_or_err(tok, _("failed to parse pid")); if (l->pid > 0) { l->cmdname = pid_get_cmdname(l->pid); if (!l->cmdname) l->cmdname = xstrdup(_("(unknown)")); } else l->cmdname = xstrdup(_("(undefined)")); break; case 5: /* device major:minor and inode number */ l->inode = get_dev_inode(tok, &l->dev); break; case 6: /* start */ l->start = !strcmp(tok, "EOF") ? 0 : strtou64_or_err(tok, _("failed to parse start")); break; case 7: /* end */ /* replace '\n' character */ tok[strlen(tok)-1] = '\0'; l->end = !strcmp(tok, "EOF") ? 0 : strtou64_or_err(tok, _("failed to parse end")); break; default: break; } } l->path = get_filename_sz(l->inode, l->pid, &sz); /* no permissions -- ignore */ if (!l->path && no_inaccessible) { rem_lock(l); continue; } if (!l->path) { /* probably no permission to peek into l->pid's path */ l->path = get_fallback_filename(l->dev); l->size = 0; } else l->size = sz; list_add(&l->locks, locks); } fclose(fp); return 0; } static int column_name_to_id(const char *name, size_t namesz) { size_t i; assert(name); 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 -1; } static inline 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 inline struct colinfo *get_column_info(unsigned num) { return &infos[ get_column_id(num) ]; } static pid_t get_blocker(int id, struct list_head *locks) { struct list_head *p; list_for_each(p, locks) { struct lock *l = list_entry(p, struct lock, locks); if (l->id == id && !l->blocked) return l->pid; } return 0; } static void add_scols_line(struct libscols_table *table, struct lock *l, struct list_head *locks) { size_t i; struct libscols_line *line; /* * Whenever cmdname or filename is NULL it is most * likely because there's no read permissions * for the specified process. */ const char *notfnd = ""; assert(l); assert(table); line = scols_table_new_line(table, NULL); if (!line) err(EXIT_FAILURE, _("failed to allocate output line")); for (i = 0; i < ncolumns; i++) { char *str = NULL; switch (get_column_id(i)) { case COL_SRC: xasprintf(&str, "%s", l->cmdname ? l->cmdname : notfnd); break; case COL_PID: xasprintf(&str, "%d", l->pid); break; case COL_TYPE: xasprintf(&str, "%s", l->type); break; case COL_INODE: xasprintf(&str, "%ju", (uintmax_t) l->inode); break; case COL_MAJMIN: if (json || raw) xasprintf(&str, "%u:%u", major(l->dev), minor(l->dev)); else xasprintf(&str, "%3u:%-3u", major(l->dev), minor(l->dev)); break; case COL_SIZE: if (!l->size) break; if (bytes) xasprintf(&str, "%ju", l->size); else str = size_to_human_string(SIZE_SUFFIX_1LETTER, l->size); break; case COL_MODE: xasprintf(&str, "%s%s", l->mode, l->blocked ? "*" : ""); break; case COL_M: xasprintf(&str, "%d", l->mandatory ? 1 : 0); break; case COL_START: xasprintf(&str, "%jd", l->start); break; case COL_END: xasprintf(&str, "%jd", l->end); break; case COL_PATH: xasprintf(&str, "%s", l->path ? l->path : notfnd); break; case COL_BLOCKER: { pid_t bl = l->blocked && l->id ? get_blocker(l->id, locks) : 0; if (bl) xasprintf(&str, "%d", (int) bl); } default: break; } if (str && scols_line_refer_data(line, i, str)) err(EXIT_FAILURE, _("failed to add output data")); } } static int show_locks(struct list_head *locks) { int rc = 0; size_t i; struct list_head *p, *pnext; struct libscols_table *table; table = scols_new_table(); if (!table) err(EXIT_FAILURE, _("failed to allocate output table")); scols_table_enable_raw(table, raw); scols_table_enable_json(table, json); scols_table_enable_noheadings(table, no_headings); if (json) scols_table_set_name(table, "locks"); for (i = 0; i < ncolumns; i++) { struct libscols_column *cl; struct colinfo *col = get_column_info(i); cl = scols_table_new_column(table, col->name, col->whint, col->flags); if (!cl) err(EXIT_FAILURE, _("failed to allocate output column")); if (json) { int id = get_column_id(i); switch (id) { case COL_SIZE: if (!bytes) break; /* fallthrough */ case COL_PID: case COL_START: case COL_END: case COL_BLOCKER: case COL_INODE: scols_column_set_json_type(cl, SCOLS_JSON_NUMBER); break; case COL_M: scols_column_set_json_type(cl, SCOLS_JSON_BOOLEAN); break; default: scols_column_set_json_type(cl, SCOLS_JSON_STRING); break; } } } /* prepare data for output */ list_for_each(p, locks) { struct lock *l = list_entry(p, struct lock, locks); if (pid && pid != l->pid) continue; add_scols_line(table, l, locks); } /* destroy the list */ list_for_each_safe(p, pnext, locks) { struct lock *l = list_entry(p, struct lock, locks); rem_lock(l); } scols_print_table(table); scols_unref_table(table); return rc; } 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_SEPARATOR, out); fputs(_("List local system locks.\n"), out); fputs(USAGE_OPTIONS, out); fputs(_(" -b, --bytes print SIZE in bytes rather than in human readable format\n"), out); fputs(_(" -J, --json use JSON output format\n"), out); fputs(_(" -i, --noinaccessible ignore locks without read permissions\n"), out); fputs(_(" -n, --noheadings don't print headings\n"), out); fputs(_(" -o, --output define which output columns to use\n"), out); fputs(_(" --output-all output all columns\n"), out); fputs(_(" -p, --pid display only locks held by this process\n"), out); fputs(_(" -r, --raw use the raw output format\n"), out); fputs(_(" -u, --notruncate don't truncate text in columns\n"), out); fputs(USAGE_SEPARATOR, out); printf(USAGE_HELP_OPTIONS(24)); fputs(USAGE_COLUMNS, out); for (i = 0; i < ARRAY_SIZE(infos); i++) fprintf(out, " %11s %s\n", infos[i].name, _(infos[i].help)); printf(USAGE_MAN_TAIL("lslocks(8)")); exit(EXIT_SUCCESS); } int main(int argc, char *argv[]) { int c, rc = 0; struct list_head locks; char *outarg = NULL; enum { OPT_OUTPUT_ALL = CHAR_MAX + 1 }; static const struct option long_opts[] = { { "bytes", no_argument, NULL, 'b' }, { "json", no_argument, NULL, 'J' }, { "pid", required_argument, NULL, 'p' }, { "help", no_argument, NULL, 'h' }, { "output", required_argument, NULL, 'o' }, { "output-all", no_argument, NULL, OPT_OUTPUT_ALL }, { "notruncate", no_argument, NULL, 'u' }, { "version", no_argument, NULL, 'V' }, { "noheadings", no_argument, NULL, 'n' }, { "raw", no_argument, NULL, 'r' }, { "noinaccessible", no_argument, NULL, 'i' }, { NULL, 0, NULL, 0 } }; static const ul_excl_t excl[] = { /* rows and cols in ASCII order */ { 'J','r' }, { 0 } }; int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT; setlocale(LC_ALL, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); close_stdout_atexit(); while ((c = getopt_long(argc, argv, "biJp:o:nruhV", long_opts, NULL)) != -1) { err_exclusive_options(c, long_opts, excl, excl_st); switch(c) { case 'b': bytes = 1; break; case 'i': no_inaccessible = 1; break; case 'J': json = 1; break; case 'p': pid = strtos32_or_err(optarg, _("invalid PID argument")); break; case 'o': outarg = optarg; break; case OPT_OUTPUT_ALL: for (ncolumns = 0; ncolumns < ARRAY_SIZE(infos); ncolumns++) columns[ncolumns] = ncolumns; break; case 'n': no_headings = 1; break; case 'r': raw = 1; break; case 'u': disable_columns_truncate(); break; case 'V': print_version(EXIT_SUCCESS); case 'h': usage(); default: errtryhelp(EXIT_FAILURE); } } INIT_LIST_HEAD(&locks); if (!ncolumns) { /* default columns */ columns[ncolumns++] = COL_SRC; columns[ncolumns++] = COL_PID; columns[ncolumns++] = COL_TYPE; columns[ncolumns++] = COL_SIZE; columns[ncolumns++] = COL_MODE; columns[ncolumns++] = COL_M; columns[ncolumns++] = COL_START; columns[ncolumns++] = COL_END; columns[ncolumns++] = COL_PATH; } if (outarg && string_add_to_idarray(outarg, columns, ARRAY_SIZE(columns), &ncolumns, column_name_to_id) < 0) return EXIT_FAILURE; scols_init_debug(0); rc = get_local_locks(&locks); if (!rc && !list_empty(&locks)) rc = show_locks(&locks); mnt_unref_table(tab); return rc; }