| /* |
| * New Interface to Process Table -- PROCTAB Stream (a la Directory streams) |
| * Copyright (C) 1996 Charles L. Blake. |
| * Copyright (C) 1998 Michael K. Johnson |
| * Copyright 1998-2002 Albert Cahalan |
| * May be distributed under the conditions of the |
| * GNU Library General Public License; a copy is in COPYING |
| */ |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| #include "version.h" |
| #include "readproc.h" |
| #include "alloc.h" |
| #include "pwcache.h" |
| #include "devname.h" |
| #include "procps.h" |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| #include <stdarg.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <signal.h> |
| #include <fcntl.h> |
| #include <sys/dir.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| |
| #ifdef FLASK_LINUX |
| #include <fs_secure.h> |
| #endif |
| |
| /* initiate a process table scan |
| */ |
| PROCTAB* openproc(int flags, ...) { |
| va_list ap; |
| PROCTAB* PT = xmalloc(sizeof(PROCTAB)); |
| |
| if (flags & PROC_PID) |
| PT->procfs = NULL; |
| else if (!(PT->procfs = opendir("/proc"))) |
| return NULL; |
| PT->flags = flags; |
| va_start(ap, flags); /* Init args list */ |
| if (flags & PROC_PID) |
| PT->pids = va_arg(ap, pid_t*); |
| else if (flags & PROC_UID) { |
| PT->uids = va_arg(ap, uid_t*); |
| PT->nuid = va_arg(ap, int); |
| } |
| va_end(ap); /* Clean up args list */ |
| return PT; |
| } |
| |
| /* terminate a process table scan |
| */ |
| void closeproc(PROCTAB* PT) { |
| if (PT) { |
| if (PT->procfs) closedir(PT->procfs); |
| free(PT); |
| } |
| } |
| |
| /* deallocate the space allocated by readproc if the passed rbuf was NULL |
| */ |
| void freeproc(proc_t* p) { |
| if (!p) /* in case p is NULL */ |
| return; |
| /* ptrs are after strings to avoid copying memory when building them. */ |
| /* so free is called on the address of the address of strvec[0]. */ |
| if (p->cmdline) |
| free((void*)*p->cmdline); |
| if (p->environ) |
| free((void*)*p->environ); |
| free(p); |
| } |
| |
| |
| // 2.5.xx looks like: |
| // |
| // "State:\t%s\n" |
| // "Tgid:\t%d\n" |
| // "Pid:\t%d\n" |
| // "PPid:\t%d\n" |
| // "TracerPid:\t%d\n" |
| |
| static void status2proc(const char *S, proc_t *restrict P) { |
| char* tmp; |
| unsigned i; |
| |
| // The cmd is escaped, with \\ and \n for backslash and newline. |
| // It certainly may contain "VmSize:" and similar crap. |
| if (unlikely(strncmp("Name:\t",S,6))) fprintf(stderr, "Internal error!\n"); |
| S += 6; |
| i = 0; |
| while (i < sizeof P->cmd - 1) { |
| int c = *S++; |
| if (unlikely(c=='\n')) break; |
| if (unlikely(c=='\0')) return; // should never happen |
| if (unlikely(c=='\\')) { |
| c = *S++; |
| if (c=='\n') break; // should never happen |
| if (!c) break; // should never happen |
| if (c=='n') c='\n'; // else we assume it is '\\' |
| } |
| P->cmd[i++] = c; |
| } |
| P->cmd[i] = '\0'; |
| |
| tmp = strstr (S,"State:\t"); |
| if (likely(tmp)) P->state = tmp[7]; |
| else fprintf(stderr, "Internal error!\n"); |
| |
| tmp = strstr (S,"PPid:"); |
| if (likely(tmp)) sscanf (tmp, |
| "PPid:\t%d\n", |
| &P->ppid |
| ); |
| else fprintf(stderr, "Internal error!\n"); |
| |
| tmp = strstr (S,"Uid:"); |
| if (likely(tmp)) sscanf (tmp, |
| "Uid:\t%d\t%d\t%d\t%d", |
| &P->ruid, &P->euid, &P->suid, &P->fuid |
| ); |
| else fprintf(stderr, "Internal error!\n"); |
| |
| tmp = strstr (S,"Gid:"); |
| if (likely(tmp)) sscanf (tmp, |
| "Gid:\t%d\t%d\t%d\t%d", |
| &P->rgid, &P->egid, &P->sgid, &P->fgid |
| ); |
| else fprintf(stderr, "Internal error!\n"); |
| |
| tmp = strstr (S,"VmSize:"); |
| if (likely(tmp)) sscanf (tmp, |
| "VmSize: %lu kB\n" |
| "VmLck: %lu kB\n" |
| "VmRSS: %lu kB\n" |
| "VmData: %lu kB\n" |
| "VmStk: %lu kB\n" |
| "VmExe: %lu kB\n" |
| "VmLib: %lu kB\n", |
| &P->vm_size, &P->vm_lock, &P->vm_rss, &P->vm_data, |
| &P->vm_stack, &P->vm_exe, &P->vm_lib |
| ); |
| else /* looks like an annoying kernel thread */ |
| { |
| P->vm_size = 0; |
| P->vm_lock = 0; |
| P->vm_rss = 0; |
| P->vm_data = 0; |
| P->vm_stack = 0; |
| P->vm_exe = 0; |
| P->vm_lib = 0; |
| } |
| |
| tmp = strstr (S,"SigPnd:"); |
| if (likely(tmp)) sscanf (tmp, |
| #ifdef SIGNAL_STRING |
| "SigPnd: %s SigBlk: %s SigIgn: %s %*s %s", |
| P->signal, P->blocked, P->sigignore, P->sigcatch |
| #else |
| "SigPnd: %Lx SigBlk: %Lx SigIgn: %Lx %*s %Lx", |
| &P->signal, &P->blocked, &P->sigignore, &P->sigcatch |
| #endif |
| ); |
| else fprintf(stderr, "Internal error!\n"); |
| } |
| |
| |
| |
| // Reads /proc/*/stat files, being careful not to trip over processes with |
| // names like ":-) 1 2 3 4 5 6". |
| static void stat2proc(const char* S, proc_t *restrict P) { |
| unsigned num; |
| char* tmp; |
| |
| /* fill in default values for older kernels */ |
| P->exit_signal = SIGCHLD; |
| P->processor = 0; |
| P->rtprio = -1; |
| P->sched = -1; |
| |
| S = strchr(S, '(') + 1; |
| tmp = strrchr(S, ')'); |
| num = tmp - S; |
| if (unlikely(num >= sizeof P->cmd)) num = sizeof P->cmd - 1; |
| memcpy(P->cmd, S, num); |
| P->cmd[num] = '\0'; |
| S = tmp + 2; // skip ") " |
| |
| num = sscanf(S, |
| "%c " |
| "%d %d %d %d %d " |
| "%lu %lu %lu %lu %lu " |
| "%Lu %Lu %Lu %Lu " /* utime stime cutime cstime */ |
| "%ld %ld %ld %ld " |
| "%Lu " /* start_time */ |
| "%lu " |
| "%ld " |
| "%lu %lu %lu %lu %lu %lu " |
| "%*s %*s %*s %*s " /* discard, no RT signals & Linux 2.1 used hex */ |
| "%lu %lu %lu " |
| "%d %d " |
| "%lu %lu", |
| &P->state, |
| &P->ppid, &P->pgrp, &P->session, &P->tty, &P->tpgid, |
| &P->flags, &P->min_flt, &P->cmin_flt, &P->maj_flt, &P->cmaj_flt, |
| &P->utime, &P->stime, &P->cutime, &P->cstime, |
| &P->priority, &P->nice, &P->timeout, &P->it_real_value, |
| &P->start_time, |
| &P->vsize, |
| &P->rss, |
| &P->rss_rlim, &P->start_code, &P->end_code, &P->start_stack, &P->kstk_esp, &P->kstk_eip, |
| /* P->signal, P->blocked, P->sigignore, P->sigcatch, */ /* can't use */ |
| &P->wchan, &P->nswap, &P->cnswap, |
| /* -- Linux 2.0.35 ends here -- */ |
| &P->exit_signal, &P->processor, /* 2.2.1 ends with "exit_signal" */ |
| /* -- Linux 2.2.8 to 2.5.17 end here -- */ |
| &P->rtprio, &P->sched /* both added to 2.5.18 */ |
| ); |
| } |
| |
| static void statm2proc(const char* s, proc_t *restrict P) { |
| int num; |
| num = sscanf(s, "%ld %ld %ld %ld %ld %ld %ld", |
| &P->size, &P->resident, &P->share, |
| &P->trs, &P->lrs, &P->drs, &P->dt); |
| /* fprintf(stderr, "statm2proc converted %d fields.\n",num); */ |
| } |
| |
| static int file2str(const char *directory, const char *what, char *ret, int cap) { |
| static char filename[80]; |
| int fd, num_read; |
| |
| sprintf(filename, "%s/%s", directory, what); |
| fd = open(filename, O_RDONLY, 0); |
| if (unlikely(fd==-1)) return -1; |
| num_read = read(fd, ret, cap - 1); |
| if (unlikely(num_read<=0)) num_read = -1; |
| else ret[num_read] = 0; |
| close(fd); |
| return num_read; |
| } |
| |
| static char** file2strvec(const char* directory, const char* what) { |
| char buf[2048]; /* read buf bytes at a time */ |
| char *p, *rbuf = 0, *endbuf, **q, **ret; |
| int fd, tot = 0, n, c, end_of_file = 0; |
| int align; |
| |
| sprintf(buf, "%s/%s", directory, what); |
| fd = open(buf, O_RDONLY, 0); |
| if (fd==-1) return NULL; |
| |
| /* read whole file into a memory buffer, allocating as we go */ |
| while ((n = read(fd, buf, sizeof buf - 1)) > 0) { |
| if (n < (int)(sizeof buf - 1)) |
| end_of_file = 1; |
| if (n == 0 && rbuf == 0) |
| return NULL; /* process died between our open and read */ |
| if (n < 0) { |
| if (rbuf) |
| free(rbuf); |
| return NULL; /* read error */ |
| } |
| if (end_of_file && buf[n-1]) /* last read char not null */ |
| buf[n++] = '\0'; /* so append null-terminator */ |
| rbuf = xrealloc(rbuf, tot + n); /* allocate more memory */ |
| memcpy(rbuf + tot, buf, n); /* copy buffer into it */ |
| tot += n; /* increment total byte ctr */ |
| if (end_of_file) |
| break; |
| } |
| close(fd); |
| if (n <= 0 && !end_of_file) { |
| if (rbuf) free(rbuf); |
| return NULL; /* read error */ |
| } |
| endbuf = rbuf + tot; /* count space for pointers */ |
| align = (sizeof(char*)-1) - ((tot + sizeof(char*)-1) & (sizeof(char*)-1)); |
| for (c = 0, p = rbuf; p < endbuf; p++) |
| if (!*p) |
| c += sizeof(char*); |
| c += sizeof(char*); /* one extra for NULL term */ |
| |
| rbuf = xrealloc(rbuf, tot + c + align); /* make room for ptrs AT END */ |
| endbuf = rbuf + tot; /* addr just past data buf */ |
| q = ret = (char**) (endbuf+align); /* ==> free(*ret) to dealloc */ |
| *q++ = p = rbuf; /* point ptrs to the strings */ |
| endbuf--; /* do not traverse final NUL */ |
| while (++p < endbuf) |
| if (!*p) /* NUL char implies that */ |
| *q++ = p+1; /* next string -> next char */ |
| |
| *q = 0; /* null ptr list terminator */ |
| return ret; |
| } |
| |
| // warning: interface may change |
| int read_cmdline(char *restrict const dst, unsigned sz, unsigned pid) { |
| char name[32]; |
| int fd; |
| unsigned n = 0; |
| dst[0] = '\0'; |
| snprintf(name, sizeof name, "/proc/%u/cmdline", pid); |
| fd = open(name, O_RDONLY); |
| if (fd==-1) return 0; |
| for (;;) { |
| ssize_t r = read(fd,dst+n,sz-n); |
| if (r==-1) { |
| if (errno==EINTR) continue; |
| break; |
| } |
| n += r; |
| if (n==sz) break; // filled the buffer |
| if (r==0) break; // EOF |
| } |
| if (n) { |
| int i; |
| if (n==sz) n--; |
| dst[n] = '\0'; |
| i=n; |
| while (i--) { |
| int c = dst[i]; |
| if (c<' ' || c>'~') dst[i]=' '; |
| } |
| } |
| return n; |
| } |
| |
| /* These are some nice GNU C expression subscope "inline" functions. |
| * The can be used with arbitrary types and evaluate their arguments |
| * exactly once. |
| */ |
| |
| /* Test if item X of type T is present in the 0 terminated list L */ |
| # define XinL(T, X, L) ( { \ |
| T x = (X), *l = (L); \ |
| while (*l && *l != x) l++; \ |
| *l == x; \ |
| } ) |
| |
| /* Test if item X of type T is present in the list L of length N */ |
| # define XinLN(T, X, L, N) ( { \ |
| T x = (X), *l = (L); \ |
| int i = 0, n = (N); \ |
| while (i < n && l[i] != x) i++; \ |
| i < n && l[i] == x; \ |
| } ) |
| |
| /* readproc: return a pointer to a proc_t filled with requested info about the |
| * next process available matching the restriction set. If no more such |
| * processes are available, return a null pointer (boolean false). Use the |
| * passed buffer instead of allocating space if it is non-NULL. */ |
| |
| /* This is optimized so that if a PID list is given, only those files are |
| * searched for in /proc. If other lists are given in addition to the PID list, |
| * the same logic can follow through as for the no-PID list case. This is |
| * fairly complex, but it does try to not to do any unnecessary work. |
| */ |
| proc_t* readproc(PROCTAB* PT, proc_t* p) { |
| static struct direct *ent; /* dirent handle */ |
| static struct stat sb; /* stat buffer */ |
| static char path[32], sbuf[1024]; /* bufs for stat,statm */ |
| #ifdef FLASK_LINUX |
| security_id_t secsid; |
| #endif |
| pid_t pid; // saved until we have a proc_t allocated for sure |
| |
| /* loop until a proc matching restrictions is found or no more processes */ |
| /* I know this could be a while loop -- this way is easier to indent ;-) */ |
| next_proc: /* get next PID for consideration */ |
| |
| /*printf("PT->flags is 0x%08x\n", PT->flags);*/ |
| #define flags (PT->flags) |
| |
| if (flags & PROC_PID) { |
| pid = *(PT->pids)++; |
| if (unlikely(!pid)) return NULL; |
| snprintf(path, sizeof path, "/proc/%d", pid); |
| } else { /* get next numeric /proc ent */ |
| for (;;) { |
| ent = readdir(PT->procfs); |
| if (unlikely(unlikely(!ent) || unlikely(!ent->d_name))) return NULL; |
| if (likely( likely(*ent->d_name > '0') && likely(*ent->d_name <= '9') )) break; |
| } |
| pid = strtoul(ent->d_name, NULL, 10); |
| memcpy(path, "/proc/", 6); |
| strcpy(path+6, ent->d_name); // trust /proc to not contain evil top-level entries |
| // snprintf(path, sizeof path, "/proc/%s", ent->d_name); |
| } |
| #ifdef FLASK_LINUX |
| if (stat_secure(path, &sb, &secsid) == -1 ) /* no such dirent (anymore) */ |
| #else |
| if (unlikely(stat(path, &sb) == -1)) /* no such dirent (anymore) */ |
| #endif |
| goto next_proc; |
| |
| if ((flags & PROC_UID) && !XinLN(uid_t, sb.st_uid, PT->uids, PT->nuid)) |
| goto next_proc; /* not one of the requested uids */ |
| |
| if (!p) |
| p = xcalloc(p, sizeof *p); /* passed buf or alloced mem */ |
| |
| p->euid = sb.st_uid; /* need a way to get real uid */ |
| #ifdef FLASK_LINUX |
| p->secsid = secsid; |
| #endif |
| p->pid = pid; |
| |
| if (flags & PROC_FILLSTAT) { /* read, parse /proc/#/stat */ |
| if (unlikely( file2str(path, "stat", sbuf, sizeof sbuf) == -1 )) |
| goto next_proc; /* error reading /proc/#/stat */ |
| stat2proc(sbuf, p); /* parse /proc/#/stat */ |
| } |
| |
| if (unlikely(flags & PROC_FILLMEM)) { /* read, parse /proc/#/statm */ |
| if (likely( file2str(path, "statm", sbuf, sizeof sbuf) != -1 )) |
| statm2proc(sbuf, p); /* ignore statm errors here */ |
| } /* statm fields just zero */ |
| |
| if (flags & PROC_FILLSTATUS) { /* read, parse /proc/#/status */ |
| if (likely( file2str(path, "status", sbuf, sizeof sbuf) != -1 )) { |
| status2proc(sbuf, p); |
| } |
| } |
| |
| /* some number->text resolving which is time consuming */ |
| if (flags & PROC_FILLUSR) { |
| strncpy(p->euser, user_from_uid(p->euid), sizeof p->euser); |
| if (flags & PROC_FILLSTATUS) { |
| strncpy(p->ruser, user_from_uid(p->ruid), sizeof p->ruser); |
| strncpy(p->suser, user_from_uid(p->suid), sizeof p->suser); |
| strncpy(p->fuser, user_from_uid(p->fuid), sizeof p->fuser); |
| } |
| } |
| |
| /* some number->text resolving which is time consuming */ |
| if (flags & PROC_FILLGRP) { |
| strncpy(p->egroup, group_from_gid(p->egid), sizeof p->egroup); |
| if (flags & PROC_FILLSTATUS) { |
| strncpy(p->rgroup, group_from_gid(p->rgid), sizeof p->rgroup); |
| strncpy(p->sgroup, group_from_gid(p->sgid), sizeof p->sgroup); |
| strncpy(p->fgroup, group_from_gid(p->fgid), sizeof p->fgroup); |
| } |
| } |
| |
| if ((flags & PROC_FILLCOM) || (flags & PROC_FILLARG)) /* read+parse /proc/#/cmdline */ |
| p->cmdline = file2strvec(path, "cmdline"); |
| else |
| p->cmdline = NULL; |
| |
| if (unlikely(flags & PROC_FILLENV)) /* read+parse /proc/#/environ */ |
| p->environ = file2strvec(path, "environ"); |
| else |
| p->environ = NULL; |
| |
| return p; |
| } |
| #undef flags |
| |
| /* ps_readproc: return a pointer to a proc_t filled with requested info about the |
| * next process available matching the restriction set. If no more such |
| * processes are available, return a null pointer (boolean false). Use the |
| * passed buffer instead of allocating space if it is non-NULL. */ |
| |
| /* This is optimized so that if a PID list is given, only those files are |
| * searched for in /proc. If other lists are given in addition to the PID list, |
| * the same logic can follow through as for the no-PID list case. This is |
| * fairly complex, but it does try to not to do any unnecessary work. |
| */ |
| proc_t* ps_readproc(PROCTAB* PT, proc_t* p) { |
| static struct direct *ent; /* dirent handle */ |
| static struct stat sb; /* stat buffer */ |
| static char path[32], sbuf[1024]; /* bufs for stat,statm */ |
| #ifdef FLASK_LINUX |
| security_id_t secsid; |
| #endif |
| pid_t pid; // saved until we have a proc_t allocated for sure |
| |
| /* loop until a proc matching restrictions is found or no more processes */ |
| /* I know this could be a while loop -- this way is easier to indent ;-) */ |
| next_proc: /* get next PID for consideration */ |
| |
| /*printf("PT->flags is 0x%08x\n", PT->flags);*/ |
| #define flags (PT->flags) |
| |
| for (;;) { |
| ent = readdir(PT->procfs); |
| if (unlikely(unlikely(!ent) || unlikely(!ent->d_name))) return NULL; |
| if (likely( likely(*ent->d_name > '0') && likely(*ent->d_name <= '9') )) break; |
| } |
| pid = strtoul(ent->d_name, NULL, 10); |
| memcpy(path, "/proc/", 6); |
| strcpy(path+6, ent->d_name); // trust /proc to not contain evil top-level entries |
| // snprintf(path, sizeof path, "/proc/%s", ent->d_name); |
| |
| #ifdef FLASK_LINUX |
| if (stat_secure(path, &sb, &secsid) == -1) /* no such dirent (anymore) */ |
| #else |
| if (stat(path, &sb) == -1) /* no such dirent (anymore) */ |
| #endif |
| goto next_proc; |
| |
| if (!p) |
| p = xcalloc(p, sizeof *p); /* passed buf or alloced mem */ |
| |
| p->euid = sb.st_uid; /* need a way to get real uid */ |
| #ifdef FLASK_LINUX |
| p->secsid = secsid; |
| #endif |
| p->pid = pid; |
| |
| if ((file2str(path, "stat", sbuf, sizeof sbuf)) == -1) |
| goto next_proc; /* error reading /proc/#/stat */ |
| stat2proc(sbuf, p); /* parse /proc/#/stat */ |
| |
| if (flags & PROC_FILLMEM) { /* read, parse /proc/#/statm */ |
| if ((file2str(path, "statm", sbuf, sizeof sbuf)) != -1) |
| statm2proc(sbuf, p); /* ignore statm errors here */ |
| } /* statm fields just zero */ |
| |
| /* if (flags & PROC_FILLSTATUS) { */ /* read, parse /proc/#/status */ |
| if ((file2str(path, "status", sbuf, sizeof sbuf)) != -1) { |
| status2proc(sbuf, p); |
| } |
| /* }*/ |
| |
| /* some number->text resolving which is time consuming */ |
| if (flags & PROC_FILLUSR) { |
| strncpy(p->euser, user_from_uid(p->euid), sizeof p->euser); |
| /* if (flags & PROC_FILLSTATUS) { */ |
| strncpy(p->ruser, user_from_uid(p->ruid), sizeof p->ruser); |
| strncpy(p->suser, user_from_uid(p->suid), sizeof p->suser); |
| strncpy(p->fuser, user_from_uid(p->fuid), sizeof p->fuser); |
| /* }*/ |
| } |
| |
| /* some number->text resolving which is time consuming */ |
| if (flags & PROC_FILLGRP) { |
| strncpy(p->egroup, group_from_gid(p->egid), sizeof p->egroup); |
| /* if (flags & PROC_FILLSTATUS) { */ |
| strncpy(p->rgroup, group_from_gid(p->rgid), sizeof p->rgroup); |
| strncpy(p->sgroup, group_from_gid(p->sgid), sizeof p->sgroup); |
| strncpy(p->fgroup, group_from_gid(p->fgid), sizeof p->fgroup); |
| /* }*/ |
| } |
| |
| if ((flags & PROC_FILLCOM) || (flags & PROC_FILLARG)) /* read+parse /proc/#/cmdline */ |
| p->cmdline = file2strvec(path, "cmdline"); |
| else |
| p->cmdline = NULL; |
| |
| if (flags & PROC_FILLENV) /* read+parse /proc/#/environ */ |
| p->environ = file2strvec(path, "environ"); |
| else |
| p->environ = NULL; |
| |
| return p; |
| } |
| #undef flags |
| |
| |
| void look_up_our_self(proc_t *p) { |
| static char path[32], sbuf[1024]; /* bufs for stat,statm */ |
| sprintf(path, "/proc/%d", getpid()); |
| file2str(path, "stat", sbuf, sizeof sbuf); |
| stat2proc(sbuf, p); /* parse /proc/#/stat */ |
| file2str(path, "statm", sbuf, sizeof sbuf); |
| statm2proc(sbuf, p); /* ignore statm errors here */ |
| file2str(path, "status", sbuf, sizeof sbuf); |
| status2proc(sbuf, p); |
| } |
| |
| |
| /* Convenient wrapper around openproc and readproc to slurp in the whole process |
| * table subset satisfying the constraints of flags and the optional PID list. |
| * Free allocated memory with freeproctab(). Access via tab[N]->member. The |
| * pointer list is NULL terminated. |
| */ |
| proc_t** readproctab(int flags, ...) { |
| PROCTAB* PT = NULL; |
| proc_t** tab = NULL; |
| int n = 0; |
| va_list ap; |
| |
| va_start(ap, flags); /* pass through args to openproc */ |
| if (flags & PROC_UID) { |
| /* temporary variables to ensure that va_arg() instances |
| * are called in the right order |
| */ |
| uid_t* u; |
| int i; |
| |
| u = va_arg(ap, uid_t*); |
| i = va_arg(ap, int); |
| PT = openproc(flags, u, i); |
| } |
| else if (flags & PROC_PID) |
| PT = openproc(flags, va_arg(ap, void*)); /* assume ptr sizes same */ |
| else |
| PT = openproc(flags); |
| va_end(ap); |
| do { /* read table: */ |
| tab = xrealloc(tab, (n+1)*sizeof(proc_t*));/* realloc as we go, using */ |
| tab[n] = readproc(PT, NULL); /* final null to terminate */ |
| } while (tab[n++]); /* stop when NULL reached */ |
| closeproc(PT); |
| return tab; |
| } |