aboutsummaryrefslogtreecommitdiff
path: root/toys/posix/ps.c
diff options
context:
space:
mode:
Diffstat (limited to 'toys/posix/ps.c')
-rw-r--r--toys/posix/ps.c613
1 files changed, 613 insertions, 0 deletions
diff --git a/toys/posix/ps.c b/toys/posix/ps.c
new file mode 100644
index 00000000..0dd4952d
--- /dev/null
+++ b/toys/posix/ps.c
@@ -0,0 +1,613 @@
+/* ps.c - show process list
+ *
+ * Copyright 2015 Rob Landley <rob@landley.net>
+ *
+ * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ps.html
+ * And http://kernel.org/doc/Documentation/filesystems/proc.txt Table 1-4
+ * And linux kernel source fs/proc/array.c function do_task_stat()
+ *
+ * Deviations from posix: no -n because /proc/self/wchan exists.
+ * Posix says default output should have field named "TTY" but if you "-o tty"
+ * the same field should be called "TT" which is _INSANE_ and I'm not doing it.
+ * Similarly -f outputs USER but calls it UID (we call it USER).
+ * It also says that -o "args" and "comm" should behave differently but use
+ * the same title, which is not the same title as the default output. (No.)
+ * Select by session id is -s not -g.
+ *
+ * Posix defines -o ADDR as "The address of the process" but the process
+ * start address is a constant on any elf system with mmu. The procps ADDR
+ * field always prints "-" with an alignment of 1, which is why it has 11
+ * characters left for "cmd" in in 80 column "ps -l" mode. On x86-64 you
+ * need 12 chars, leaving nothing for cmd: I.E. posix 2008 ps -l mode can't
+ * be sanely implemented on 64 bit Linux systems. In procps there's ps -y
+ * which changes -l by removing the "F" column and swapping RSS for ADDR,
+ * leaving 9 chars for cmd, so we're using that as our -l output.
+ *
+ * TODO: ps aux (att & bsd style "ps -ax" vs "ps ax" behavior difference)
+ * TODO: finalize F, remove C
+ * switch -fl to -y, use "string" instead of constants to set, remove C
+ * TODO: --sort -Z
+ * TODO: way too many hardwired constants here, how can I generate them?
+ * TODO: thread support /proc/$d/task/%d/stat (and -o stat has "l")
+ *
+ * Design issue: the -o fields are an ordered array, and the order is
+ * significant. The array index is used in strawberry->which (consumed
+ * in do_ps()) and in the bitmasks enabling default fields in ps_main().
+
+USE_PS(NEWTOY(ps, "aAdeflo*p*s*t*u*U*g*G*w[!ol][+Ae]", TOYFLAG_USR|TOYFLAG_BIN))
+
+config PS
+ bool "ps"
+ default y
+ help
+ usage: ps [-Aadeflw] [-gG GROUP] [-o FIELD] [-p PID] [-t TTY] [-uU USER]
+
+ List processes.
+
+ Which processes to show (selections may be comma separated lists):
+
+ -A All processes
+ -a Processes with terminals that aren't session leaders
+ -d All processes that aren't session leaders
+ -e Same as -A
+ -g belonging to GROUPs
+ -G belonging to real GROUPs (before sgid)
+ -p PIDs
+ -s in session IDs
+ -t attached to selected TTYs
+ -u owned by USERs
+ -U owned by real USERs (before suid)
+ -w Wide output (don't truncate at terminal width)
+
+ Which FIELDs to show. (Default = -o PID,TTY,TIME,CMD)
+
+ -f Full listing (-o USER:8=UID,PID,PPID,C,STIME,TTY,TIME,CMD)
+ -l Long listing (-o F,S,UID,PID,PPID,C,PRI,NI,ADDR,SZ,WCHAN,TTY,TIME,CMD)
+ -o Output the listed FIELDs, each with optional :size and/or =title
+
+ Available -o FIELDs:
+
+ ADDR Instruction pointer
+ CMD Command line
+ ETIME Elapsed time since process start
+ F Process flags (PF_*) from linux source file include/sched.h
+ (in octal rather than hex because posix)
+ GID Group id
+ GROUP Group name
+ MAJFL Major page faults
+ MINFL Minor page faults
+ NI Niceness of process (lower niceness is higher priority)
+ PCPU Percentage of CPU time used
+ PGID Process Group ID
+ PID Process ID
+ PPID Parent Process ID
+ PRI Priority
+ RGID Real (before sgid) group ID
+ RGROUP Real (before sgid) group name
+ RSS Resident Set Size (memory currently used)
+ RUID Real (before suid) user ID
+ RUSER Real (before suid) user name
+ S Process state:
+ R (running) S (sleeping) D (disk sleep) T (stopped) t (traced)
+ Z (zombie) X (dead) x (dead) K (wakekill) W (waking)
+ STAT Process state (S) plus:
+ < high priority N low priority L locked memory
+ s session leader + foreground l multithreaded
+ STIME Start time of process in hh:mm (size :19 shows yyyy-mm-dd hh:mm:ss)
+ SZ Memory Size (4k pages needed to completely swap out process)
+ TIME CPU time consumed
+ TTY Controlling terminal
+ UID User id
+ USER User name
+ VSZ Virtual memory size (1k units)
+ WCHAN Waiting in kernel for
+*/
+
+#define FOR_ps
+#include "toys.h"
+
+GLOBALS(
+ struct arg_list *G;
+ struct arg_list *g;
+ struct arg_list *U;
+ struct arg_list *u;
+ struct arg_list *t;
+ struct arg_list *s;
+ struct arg_list *p;
+ struct arg_list *o;
+
+ struct ptr_len gg, GG, pp, ss, tt, uu, UU, *parsing;
+ unsigned width;
+ dev_t tty;
+ void *fields;
+ long bits;
+ long long ticks;
+)
+
+struct strawberry {
+ struct strawberry *next, *prev;
+ short which, len;
+ char *title;
+ char forever[];
+};
+
+static time_t get_uptime(void)
+{
+ struct sysinfo si;
+
+ sysinfo(&si);
+
+ return si.uptime;
+}
+
+// Return 1 to display, 0 to skip
+static int match_process(long long *slot)
+{
+ struct ptr_len *match[] = {&TT.gg,&TT.GG,&TT.pp,&TT.ss,&TT.tt,&TT.uu,&TT.UU};
+ int i, j, mslot[] = {33, 34, 0, 3, 4, 31, 32};
+ long *ll = 0;
+
+ // Do we have -g -G -p -s -t -u -U options selecting processes?
+ for (i = 0; i < ARRAY_LEN(match); i++) {
+ if (match[i]->len) {
+ ll = match[i]->ptr;
+ for (j = 0; j<match[i]->len; j++) if (ll[j] == slot[mslot[i]]) return 1;
+ }
+ }
+
+ // If we had selections and didn't match them, don't display
+ if (ll) return 0;
+
+ // Filter implicit categories for other display types
+ if ((toys.optflags&(FLAG_a|FLAG_d)) && slot[3]==*slot) return 0;
+ if ((toys.optflags&FLAG_a) && !slot[4]) return 0;
+ if (!(toys.optflags&(FLAG_a|FLAG_d|FLAG_A|FLAG_e)) && TT.tty!=slot[4])
+ return 0;
+
+ return 1;
+}
+
+// dirtree callback.
+// toybuf used as: 1024 /proc/$PID/stat, 1024 slot[], 2048 /proc/$PID/cmdline
+static int do_ps(struct dirtree *new)
+{
+ struct strawberry *field;
+ long long *slot = (void *)(toybuf+1024), ll;
+ char *name, *s, state;
+ int nlen, i, fd, len, width = TT.width;
+
+ if (!new->parent) return DIRTREE_RECURSE|DIRTREE_SHUTUP;
+ if (!(*slot = atol(new->name))) return 0;
+
+ // name field limited to 256 bytes by VFS, plus 40 fields * max 20 chars:
+ // 1000-ish total, but some forced zero so actually there's headroom.
+ sprintf(toybuf, "%lld/stat", *slot);
+ if (!readfileat(dirtree_parentfd(new), toybuf, toybuf, 1024)) return 0;
+
+ // parse oddball fields (name and state)
+ if (!(s = strchr(toybuf, '('))) return 0;
+ for (name = ++s; *s != ')'; s++) if (!*s) return 0;
+ nlen = s++-name;
+ if (1>sscanf(++s, " %c%n", &state, &i)) return 0;
+
+ // parse numeric fields (PID = 0, skip 2, then 4th field goes in slot[1])
+ for (len = 1; len<100; len++)
+ if (1>sscanf(s += i, " %lld%n", slot+len, &i)) break;
+
+ // save uid, ruid, gid, gid, and rgid int slots 31-34 (we don't use sigcatch
+ // or numeric wchan, and the remaining two are always zero).
+ slot[31] = new->st.st_uid;
+ slot[33] = new->st.st_gid;
+
+ // If RGROUP RUSER STAT RUID RGID
+ // Save ruid in slot[34] and rgid in slot[35], which are otherwise zero,
+ // and vmlck into slot[18] (it_real_value, also always zero).
+ if ((TT.bits & 0x38300000) || TT.GG.len || TT.UU.len) {
+ char *out = toybuf+2048;
+
+ sprintf(out, "%lld/status", *slot);
+ if (!readfileat(dirtree_parentfd(new), out, out, 2048)) *out = 0;
+ s = strstr(out, "\nUid:");
+ slot[32] = s ? atol(s+5) : new->st.st_uid;
+ s = strstr(out, "\nGid:");
+ slot[34] = s ? atol(s+5) : new->st.st_gid;
+ s = strstr(out, "\nVmLck:");
+ if (s) slot[18] = atoll(s+5);
+ }
+
+ // skip processes we don't care about.
+ if (!match_process(slot)) return 0;
+
+ // At this point 512 bytes at toybuf+512 are free (already parsed).
+ // Start of toybuf still has name in it.
+
+ // Loop through fields
+ for (field = TT.fields; field; field = field->next) {
+ char *out = toybuf+2048, *scratch = toybuf+512;
+
+ // Default: unsupported (5 "C")
+ sprintf(out, "-");
+
+ // PID, PPID, PRI, NI, ADDR, SZ, RSS, PGID, VSS, MAJFL, MINFL
+ if (-1!=(i = stridx((char[]){3,4,6,7,8,9,24,19,23,25,30,0}, field->which)))
+ {
+ char *fmt = "%lld";
+
+ ll = slot[((char[]){0,1,15,16,27,20,21,2,20,9,7})[i]];
+ if (i==2) ll--;
+ if (i==4) fmt = "%llx";
+ else if (i==5) ll >>= 12;
+ else if (i==6) ll <<= 2;
+ else if (i==8) ll >>= 10;
+ sprintf(out, fmt, ll);
+ // UID USER RUID RUSER GID GROUP RGID RGROUP
+ } else if (-1!=(i = stridx((char[]){2,22,28,21,26,17,29,20}, field->which)))
+ {
+ int id = slot[31+i/2]; // uid, ruid, gid, rgid
+
+ // Even entries are numbers, odd are names
+ sprintf(out, "%d", id);
+ if (i&1) {
+ if (i>3) {
+ struct group *gr = getgrgid(id);
+
+ if (gr) out = gr->gr_name;
+ } else {
+ struct passwd *pw = getpwuid(id);
+
+ if (pw) out = pw->pw_name;
+ }
+ }
+
+ // F (also assignment of i used by later tests)
+ // Posix doesn't specify what flags should say. Man page says
+ // 1 for PF_FORKNOEXEC and 4 for PF_SUPERPRIV from linux/sched.h
+ } else if (!(i = field->which)) sprintf(out, "%llo", (slot[6]>>6)&5);
+ // S STAT
+ else if (i==1 || i==27) {
+ sprintf(out, "%c", state);
+ if (i==27) {
+ // TODO l = multithreaded
+ s = out+1;
+ if (slot[16]<0) *s++ = '<';
+ else if (slot[16]>0) *s++ = 'N';
+ if (slot[3]==*slot) *s++ = 's';
+ if (slot[18]) *s++ = 'L';
+ if (slot[5]==*slot) *s++ = '+';
+ *s = 0;
+ }
+ // WCHAN
+ } else if (i==10) {
+ sprintf(scratch, "%lld/wchan", *slot);
+ readfileat(dirtree_parentfd(new), scratch, out, 2047);
+
+ // STIME
+ } else if (i==11) {
+ time_t t = time(0) - get_uptime() + slot[19]/sysconf(_SC_CLK_TCK);
+
+ // Padding behavior's a bit odd: default field size is just hh:mm.
+ // Increasing stime:size reveals more data at left until full
+ // yyyy-mm-dd hh:mm revealed at :16, then adds :ss at end for :19. But
+ // expanding last field just adds :ss.
+ strftime(scratch, 512, "%F %T", localtime(&t));
+ out = scratch+strlen(scratch)-3-abs(field->len);
+ if (out<scratch) out = scratch;
+
+ // TTY
+ } else if (i==12) {
+
+ // Can we readlink() our way to a name?
+ for (i=0; i<3; i++) {
+ struct stat st;
+
+ sprintf(scratch, "%lld/fd/%i", *slot, i);
+ fd = dirtree_parentfd(new);
+ if (!fstatat(fd, scratch, &st, 0) && S_ISCHR(st.st_mode)
+ && st.st_rdev == slot[4]
+ && 0<(len = readlinkat(fd, scratch, out, 2047)))
+ {
+ out[len] = 0;
+ if (!strncmp(out, "/dev/", 5)) out += 5;
+
+ break;
+ }
+ }
+
+ // Couldn't find it, show major:minor
+ if (i==3) {
+ i = slot[4];
+ sprintf(out, "%d:%d", (i>>8)&0xfff, ((i>>12)&0xfff00)|(i&0xff));
+ }
+
+ // TIME ELAPSED
+ } else if (i==13 || i==16) {
+ int unit = 60*60*24, j = sysconf(_SC_CLK_TCK);
+ time_t seconds = (i==16) ? (get_uptime()*j)-slot[19] : slot[11]+slot[12];
+
+ seconds /= j;
+ for (s = 0, j = 0; j<4; j++) {
+ // TIME has 3 required fields, ETIME has 2. (Posix!)
+ if (!s && (seconds>unit || j == 1+(i==16))) s = out;
+ if (s) {
+ s += sprintf(s, j ? "%02ld": "%2ld", (long)(seconds/unit));
+ if ((*s = "-::"[j])) s++;
+ }
+ seconds %= unit;
+ unit /= j ? 60 : 24;
+ }
+
+ // COMMAND CMD
+ // Command line limited to 2k displayable. We could dynamically malloc, but
+ // it'd almost never get used, querying length of a proc file is awkward,
+ // fixed buffer is nommu friendly... Wait for somebody to complain. :)
+ } else if (i==14 || i==15) {
+ int fd;
+
+ len = 0;
+ sprintf(out, "%lld/cmdline", *slot);
+ fd = openat(dirtree_parentfd(new), out, O_RDONLY);
+
+ if (fd != -1) {
+ if (0<(len = read(fd, out, 2047))) {
+ if (!out[len-1]) len--;
+ else out[len] = 0;
+ for (i = 0; i<len; i++) if (out[i] < ' ') out[i] = ' ';
+ }
+ close(fd);
+ }
+
+ if (len<1) sprintf(out, "[%.*s]", nlen, name);
+ // %CPU
+ } else if (i==18) {
+ ll = (get_uptime()*sysconf(_SC_CLK_TCK)-slot[19]);
+ len = ((slot[11]+slot[12])*1000)/ll;
+ sprintf(out, "%d.%d", len/10, len%10);
+ }
+
+ // Output the field, appropriately padded
+ len = width - (field != TT.fields);
+ if (!field->next && field->len<0) i = 0;
+ else {
+ i = len<abs(field->len) ? len : field->len;
+ len = abs(i);
+ }
+
+ // TODO test utf8 fontmetrics
+ width -= printf(" %*.*s" + (field == TT.fields), i, len, out);
+ if (!width) break;
+ }
+ xputc('\n');
+
+ return 0;
+}
+
+// Traverse arg_list of csv, calling callback on each value
+void comma_args(struct arg_list *al, char *err,
+ char *(*callback)(char *str, int len))
+{
+ char *next, *arg;
+ int len;
+
+ while (al) {
+ arg = al->arg;
+ while ((next = comma_iterate(&arg, &len)))
+ if ((next = callback(next, len)))
+ perror_exit("%s '%s'@%ld", err, al->arg, 1+next-al->arg);
+ al = al->next;
+ }
+}
+
+static char *parse_o(char *type, int length)
+{
+ struct strawberry *field;
+ char *width, *title, *end, *s, *typos[] = {
+ "F", "S", "UID", "PID", "PPID", "C", "PRI", "NI", "ADDR", "SZ",
+ "WCHAN", "STIME", "TTY", "TIME", "CMD", "COMMAND", "ELAPSED", "GROUP",
+ "%CPU", "PGID", "RGROUP", "RUSER", "USER", "VSZ", "RSS", "MAJFL",
+ "GID", "STAT", "RUID", "RGID", "MINFL"
+ };
+ signed char widths[] = {1,-1,5,5,5,2,3,3,4+sizeof(long),5,
+ -6,5,-8,8,-27,-27,11,-8,
+ 4,5,-8,-8,-8,6,5,6,
+ 8,-5,4,4,6};
+ int i, j, k;
+
+ // Get title, length of title, type, end of type, and display width
+
+ // Chip off =name to display
+ if ((end = strchr(type, '=')) && length>(end-type)) {
+ title = end+1;
+ length -= (end-type)+1;
+ } else {
+ end = type+length;
+ title = 0;
+ }
+
+ // Chip off :width to display
+ if ((width = strchr(type, ':')) && width<end) {
+ if (!title) length = width-type;
+ } else width = 0;
+
+ // Allocate structure, copy title
+ field = xzalloc(sizeof(struct strawberry)+(length+1)*!!title);
+ if (title) {
+ memcpy(field->title = field->forever, title, length);
+ field->title[field->len = length] = 0;
+ }
+
+ if (width) {
+ field->len = strtol(++width, &title, 10);
+ if (!isdigit(*width) || title != end) return title;
+ end = --width;
+ }
+
+ // Find type
+ for (i = 0; i<ARRAY_LEN(typos); i++) {
+ field->which = i;
+ for (j = 0; j<2; j++) {
+ if (!j) s = typos[i];
+ // posix requires alternate names for some fields
+ else if (-1==(k = stridx((char []){7,14,15,16,18,23,22,0}, i))) continue;
+ else s = ((char *[]){"NICE","ARGS","COMM","ETIME","PCPU",
+ "VSIZE","UNAME"})[k];
+
+ if (!strncasecmp(type, s, end-type) && strlen(s)==end-type) break;
+ }
+ if (j!=2) break;
+ }
+ if (i==ARRAY_LEN(typos)) return type;
+ if (!field->title) field->title = typos[field->which];
+ if (!field->len) field->len = widths[field->which];
+ else if (widths[field->which]<0) field->len *= -1;
+ dlist_add_nomalloc((void *)&TT.fields, (void *)field);
+
+ // Print padded header.
+ printf(" %*s" + (field == TT.fields), field->len, field->title);
+ TT.bits |= (i = 1<<field->which);
+
+ return 0;
+}
+
+// Parse -p -s -t -u -U -g -G
+static char *parse_rest(char *str, int len)
+{
+ struct ptr_len *pl = TT.parsing;
+ long *ll = pl->ptr;
+ char *end;
+ int num = 0;
+
+ // numeric: -p, -s
+ // gg, GG, pp, ss, tt, uu, UU, *parsing;
+
+ // Allocate next chunk of data
+ if (!(15&pl->len))
+ ll = pl->ptr = xrealloc(pl->ptr, sizeof(long)*(pl->len+16));
+
+ // Parse numerical input
+ if (isdigit(*str)) {
+ ll[pl->len] = xstrtol(str, &end, 10);
+ if (end==(len+str)) num++;
+ }
+
+ if (pl==&TT.pp || pl==&TT.ss) {
+ if (num && ll[pl->len]>0) {
+ pl->len++;
+
+ return 0;
+ }
+ } else if (pl==&TT.tt) {
+ // -t pts = 12,pts/12 tty = /dev/tty2,tty2,S0
+ if (!num) {
+ if (strstart(&str, strcpy(toybuf, "/dev/"))) len -= 5;
+ if (strstart(&str, "pts/")) {
+ len -= 4;
+ num++;
+ } else if (strstart(&str, "tty")) len -= 3;
+ }
+ if (len<256 && (!(end = strchr(str, '/')) || end-str>len)) {
+ struct stat st;
+
+ end = toybuf + sprintf(toybuf, "/dev/%s", num ? "pts/" : "tty");
+ memcpy(end, str, len);
+ end[len] = 0;
+ xstat(toybuf, &st);
+ ll[pl->len++] = st.st_rdev;
+
+ return 0;
+ }
+ } else if (len<255) {
+ char name[256];
+
+ if (num) {
+ pl->len++;
+
+ return 0;
+ }
+
+ memcpy(name, str, len);
+ name[len] = 0;
+ if (pl==&TT.gg || pl==&TT.GG) {
+ struct group *gr = getgrnam(name);
+ if (gr) {
+ ll[pl->len++] = gr->gr_gid;
+
+ return 0;
+ }
+ } else if (pl==&TT.uu || pl==&TT.UU) {
+ struct passwd *pw = getpwnam(name);
+ if (pw) {
+ ll[pl->len++] = pw->pw_uid;
+
+ return 0;
+ }
+ }
+ }
+
+ // Return error
+ return str;
+}
+
+void ps_main(void)
+{
+ int i;
+
+ TT.width = 99999;
+ if (!(toys.optflags&FLAG_w)) terminal_size(&TT.width, 0);
+
+ // find controlling tty, falling back to /dev/tty if none
+ for (i = 0; !TT.tty && i<4; i++) {
+ struct stat st;
+ int fd = i;
+
+ if (i==3 && -1==(fd = open("/dev/tty", O_RDONLY))) break;
+
+ if (isatty(fd) && !fstat(fd, &st)) TT.tty = st.st_rdev;
+ if (i==3) close(fd);
+ }
+
+ // parse command line options other than -o
+ TT.parsing = &TT.pp;
+ comma_args(TT.p, "bad -p", parse_rest);
+ TT.parsing = &TT.tt;
+ comma_args(TT.t, "bad -t", parse_rest);
+ TT.parsing = &TT.ss;
+ comma_args(TT.s, "bad -s", parse_rest);
+ TT.parsing = &TT.uu;
+ comma_args(TT.u, "bad -u", parse_rest);
+ TT.parsing = &TT.UU;
+ comma_args(TT.U, "bad -u", parse_rest);
+ TT.parsing = &TT.gg;
+ comma_args(TT.g, "bad -g", parse_rest);
+ TT.parsing = &TT.GG;
+ comma_args(TT.G, "bad -G", parse_rest);
+
+ // Manual field selection, or default/-f/-l. Also prints header.
+ if (TT.o) comma_args(TT.o, "-o", parse_o);
+ else {
+ struct arg_list al;
+
+ al.next = 0;
+ if (toys.optflags&FLAG_f)
+ al.arg = "USER:8=UID,PID,PPID,C,STIME,TTY,TIME,CMD";
+ else if (toys.optflags&FLAG_l)
+ al.arg = "F,S,UID,PID,PPID,C,PRI,NI,ADDR,SZ,WCHAN,TTY,TIME,CMD";
+ else al.arg = "PID,TTY,TIME,CMD";
+
+ comma_args(&al, 0, parse_o);
+ }
+ dlist_terminate(TT.fields);
+ xputc('\n');
+
+ dirtree_read("/proc", do_ps);
+
+ if (CFG_TOYBOX_FREE) {
+ free(TT.gg.ptr);
+ free(TT.GG.ptr);
+ free(TT.pp.ptr);
+ free(TT.ss.ptr);
+ free(TT.tt.ptr);
+ free(TT.uu.ptr);
+ free(TT.UU.ptr);
+ llist_traverse(TT.fields, free);
+ }
+}