/* Loosely derived from a simple hack posted to c.o.l
 * -Michael K. Johnson, johnsonm@sunsite.unc.edu
 *
 * Some pieces from Branko Lankester's kmem ps, copyright 1992 Branko Lankester
 *
 * Modified 1994/05/25 Michael Shields <mjshield@nyx.cs.du.edu>
 * xrealloc() added and xmalloc() cleaned up.
 * Made get_process() take a pid_t.
 * Added get_processes().  Split off do_get_process().
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/dir.h>
#include <regex.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include "psdata.h"
#include "ps.h"

/* parse_stat() takes great pains to be able to handle truly arbitrary 
   executable file basenames -- i.e. it's oddly segmented for a *reason* */

void parse_stat(char* S, struct ps_proc* P) {
    int n = 33;
    char* tmp = S + strlen(S);

    /* Split S into "PID (cmd)" and "<rest of entries>" */
    while (n--)               /* tmp -> spc following PID (cmd) */
	while (*--tmp != ' ') /* need to do in reverse for arbitrary cmd's */
	    ;
    *tmp='\0';
    /* Now we can parse these two strings separately */
    sscanf(S, "%d %40c", &P->pid, P->cmd);
    sscanf(tmp+1, "%c %d %d %d %d %d %u %u %u %u %u %d %d %d %d %d %d %u %u "
                  "%d %u %u %u %u %u %u %u %u %d %d %d %d %u",
           &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->counter,
           &P->priority, &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, &P->wchan);

#ifdef TTY_FULL_DEVNO       /* temporary kludge until other tty majors exist */
    if (P->tty)
        P->tty = MINOR(P->tty);
    else
        P->tty = -1;
#endif
}

void parse_stat_m(char* s, struct ps_proc* P) {
    sscanf(s, "%d %d %d %d %d %d %d",
           &P->statm.size, &P->statm.resident, &P->statm.share, &P->statm.trs,
	   &P->statm.lrs, &P->statm.drs, &P->statm.dt);
}

/* note: mycpy eqivalent to nulls2spc(ret, file2str(..., ret, ...)) */
void nulls2spc(char* str, int len) {
    int i;
    for (i=0; i < len; i++)
	if (str[i]==0)
	    str[i]=' ';
}

int file2str(char *directory, char *ret, char *what, int cap)
{
  static char filename[80];
  int fd, num_read;

  sprintf(filename, "/proc/%s/%s", directory, what);
  if ( (fd = open(filename, O_RDONLY, 0)) == -1 )
     return -1;
  if ( (num_read = read(fd, ret, cap-1)) == -1 )
      return -1;
  ret[num_read] = 0;
  close(fd);
  return num_read;
}

struct ps_proc_head *take_snapshot(char a, char u, char x, char m, char r,
				   uid_t uid, int ctty)
{
  DIR *proc;
  static struct direct *ent;
  static char filename[80];
  static char stat_str[4096];
  struct ps_proc_head *ph = NULL;
  struct ps_proc *this = NULL, *that = NULL;
  struct stat sb;
  int cmd_len;

  if ((proc = opendir("/proc")) == NULL) {
      perror("opendir /proc");
      exit(1);
  }
/*re_comp("^[0-9]*$");*/

  ph = (struct ps_proc_head *) xcalloc(ph, sizeof(struct ps_proc_head));
  /* initializes ph->head and ph->count to zero ;-) */
  ph->head = (struct ps_proc *) xcalloc(ph->head, sizeof(struct ps_proc));
  this = ph->head;

  while(ent = readdir(proc)) {
/*  if(!re_exec(ent->d_name)) continue; */
    if(*ent->d_name < '0' || *ent->d_name > '9') continue;
    sprintf(filename, "/proc/%s", ent->d_name);
    stat(filename, &sb);
    if(!a && (sb.st_uid != uid)) continue;
    this->uid = sb.st_uid;
    cmd_len = file2str(ent->d_name, this->cmdline, "cmdline", sizeof(this->cmdline));
    if ( (file2str(ent->d_name, stat_str, "stat", sizeof(stat_str))) == -1 )
	continue;
    parse_stat(stat_str,this);
    nulls2spc(this->cmdline, cmd_len);
    if ((ctty && (ctty != this->tty))
	|| (r && this->state != 'R' && this->state != 'D')
	|| (!x && (this->tty == -1))) {
      this->pid = 0;
      continue;
    }
    /* 0 normally passed, which is never the value given as the tty from the
       proc filesystem, so this only happens if a specific tty was passed. */
    if(m) {
      if((file2str(ent->d_name, stat_str, "statm", sizeof(stat_str))) == -1)
	  continue;
      parse_stat_m(stat_str,this);
    }
    if (this->state == 'Z') strcat(this->cmd," <zombie>");
    dev_to_tty(this->ttyc, this->tty);
    if(u) strncpy(this->user, user_from_uid(this->uid), 9);

    /* update the linked list and increase the count */
    if(this->pid) {
      that = this;
      this->next = (struct ps_proc *) xcalloc(this->next,
					      sizeof(struct ps_proc));
      this = this->next;
      ph->count++;
    }
  } /* end of the while loop */
  closedir(proc);
  if (ph->count != 0)
     if(!this->pid) { /* I beleive this will always be true, because it will try
                         one more readdir, and there will be a hanging entry... 
                         But I make it conditional to be safe */
       that->next = (struct ps_proc *) NULL;
       free (this);
     } else this->next = (struct ps_proc *) NULL;
  return ph;
}

/*
 * Fill a struct with information about the given PID.  M means to fill
 * the statm field.  The structure is freshly allocated.  If this fails,
 * it will return NULL.
 */
static struct ps_proc *do_get_process(pid_t pid, int m)
{
  static char stat_str[256];
  static char filename[80];
  char *fn = filename;
  struct ps_proc *ret = NULL;
  struct stat sb;
  int cmd_len;

  ret = (struct ps_proc *) xcalloc(ret, sizeof(struct ps_proc));

  sprintf(fn, "/proc/%u", pid);
  stat(fn, &sb);
  ret->uid = sb.st_uid;
  fn += 6; /* cut "/proc/" out of fn cheaply */
  cmd_len = file2str(fn, ret->cmdline, "cmdline", sizeof(ret->cmdline));
  if((file2str(fn, stat_str, "stat", sizeof(stat_str))) == -1)
    { free(ret); return NULL; }
  parse_stat(stat_str,ret);
  nulls2spc(ret->cmdline, cmd_len);
  if(m) {
    if((file2str(fn, stat_str, "statm", sizeof(stat_str))) == -1)
      { free(ret); return NULL; }
    parse_stat_m(stat_str, ret);
  }
  if (ret->state == 'Z') strcat(ret->cmd," <zombie>");
  dev_to_tty(ret->ttyc, ret->tty);
  strncpy(ret->user, user_from_uid(ret->uid), 9);

  return ret;
}

struct ps_proc_head *get_process(pid_t pid, int m)
{
  struct ps_proc_head *ph = NULL;

  ph = xmalloc(sizeof(struct ps_proc_head));
  ph->head = do_get_process(pid, m);
  ph->count = ph->head ? 1 : 0;
  return ph;
}

/*
 * Return the status of the given processes; PIDS is a zero-terminated
 * list of pids.  M is same as for get_process().
 */
struct ps_proc_head *get_processes(pid_t *pids, int m)
{
    struct ps_proc_head *ph;
    struct ps_proc *this_process;
    struct ps_proc **next_of_last;

    if (!pids)
    	/* Shouldn't happen. */
    	return(NULL);

    ph = xmalloc(sizeof(struct ps_proc_head));
    ph->count = 0;
    next_of_last = &ph->head;

    while (*pids) {
        this_process = do_get_process(*pids, m);
        if (this_process) {
            *next_of_last = this_process;
            next_of_last = &this_process->next;
            ph->count++;
        }
        pids++;
    }

    return ph;
}

struct ps_proc_head *refresh_snapshot(struct ps_proc_head *ph,
				      char a, char u, char x, char m, char r,
				      uid_t uid, int ctty)
{
  DIR *proc;
  static struct direct *ent;
  static char filename[80];
  static char stat_str[4096];
  struct ps_proc *this = NULL, *that = NULL;
  struct stat sb;
  int cmd_len;

  if ((proc = opendir("/proc")) == NULL) {
      perror("opendir /proc");
      exit(1);
  }
/*re_comp("^[0-9]*$"); */

  ph->count = 0;
  this = ph->head;

  while((ent = readdir(proc))) { /* Extra parens to make gcc -Wall happy... */
/*  if(!re_exec(ent->d_name)) continue; */
    if(*ent->d_name < '0' || *ent->d_name > '9') continue;
    sprintf(filename, "/proc/%s", ent->d_name);
    stat(filename, &sb);
    if(!a && (sb.st_uid != uid)) continue;
    this->uid = sb.st_uid;
    cmd_len = file2str(ent->d_name, this->cmdline, "cmdline", sizeof(this->cmdline));
    if((file2str(ent->d_name, stat_str, "stat", sizeof(stat_str))) == -1)
	continue;
    parse_stat(stat_str,this);
    nulls2spc(this->cmdline, cmd_len);
    if ((ctty && (ctty != this->tty))
	|| (r && this->state != 'R' && this->state != 'D')
	|| (!x && (this->tty == -1))) {
      this->pid = 0;
      continue;
    }
    /* 0 normally passed, which is never the value given as the tty from the
       proc filesystem, so this only happens if a specific tty was passed. */
    if(m) {
      if((file2str(ent->d_name, stat_str, "statm", sizeof(stat_str))) == -1)
	  continue;
      parse_stat_m(stat_str,this);
    }
    if (this->state == 'Z') strcat(this->cmd," <zombie>");
    dev_to_tty(this->ttyc, this->tty);
    if(u) strncpy(this->user, user_from_uid(this->uid), 9);

    /* update the linked list and increase the count */
    if(this->pid) {
      that = this;
      if(!this->next)
	this->next = (struct ps_proc *) xcalloc(this->next,
						sizeof(struct ps_proc));
      this = this->next;
      this->pid = 0;
      ph->count++;
    }
  } /* end of the while loop */
  closedir(proc);
  if(!this->pid) { /* if the last slot was not used */
    if(that->next)
      free_psproc(that->next);
    that->next = (struct ps_proc *) NULL;
  } else {
    if (this->next)
      free_psproc(this->next);
    this->next = (struct ps_proc *) NULL;
  }
  return ph;
}

void free_psproc(struct ps_proc * this) {

  struct ps_proc *that;

  for(; this != NULL; this = that) {
    that = this->next;
    free(this);
  }
}

/* The next few functions are modified versions of functions from
   various files in the kmem ps.  They are not as complete at error
   checking, but that's life.  Thanks, Branko.  I had to change them
   not to look at /dev/kmem, and to make my life simpler for a while,
   error checking came out.  It's a stupid move, and I'll regret it,
   but quite a bit of it depends on kmem reading, so I just chopped
   it out...
*/

struct tbl_s vars, fncs;
struct psdb_hdr db_hdr;
int psdb = -1;

void *xmalloc(unsigned int size)
{
    void *p;

    if (size == 0)
        ++size;

    p = malloc(size);
    if (!p) {
	fprintf(stderr, "xmalloc: malloc(%d) failed", size);
	perror(NULL);
	exit(1);
    }
    return(p);
}

void *xrealloc(void *oldp, unsigned int size)
{
    void *p;

    if (size == 0)
        ++size;

    p = realloc(oldp, size);
    if (!p) {
	fprintf(stderr, "xrealloc: realloc(%d) failed", size);
	perror(NULL);
	exit(1);
    }
    return(p);
}

int open_psdb(void)
{

    if ((psdb = open(PSDATABASE, O_RDONLY)) == -1)
	return -1;
    if (read(psdb, (char *) &db_hdr, sizeof(db_hdr)) != sizeof(db_hdr))
	return -1;
    
    if (strncmp(db_hdr.magic, PS_MAGIC, sizeof(db_hdr.magic))) {
	fprintf(stderr, "invalid psdatabase\n");
	return -1;
    }
    
    return(0);
}

void close_psdb(void)
{
    if (psdb != -1)
	close(psdb);
    psdb = -1;
}

int read_tbl(struct dbtbl_s *dbtbl, struct tbl_s *tbl)
{
  lseek(psdb, dbtbl->off, SEEK_SET);
  tbl->tbl = (struct sym_s *) xmalloc(dbtbl->size);
  if (read(psdb, (char *) tbl->tbl, dbtbl->size) != dbtbl->size) {
    perror(PSDATABASE);
    exit(1);
  }
  tbl->nsym = dbtbl->nsym;
  tbl->strings = (char *) (tbl->tbl + tbl->nsym);
  return(0);
}

/*
 * misc stuff needed
 */

char * find_func(unsigned long address)
{
  int n;
  struct sym_s *p;
  char *s;

  if (fncs.tbl == NULL)
    read_tbl(&db_hdr.fncs, &fncs);

  p = fncs.tbl;
  n = fncs.nsym;
  while (n) {
    int i = n / 2;
    if (p[i].addr < address) {
      p = &p[i+1];
      if (p->addr > address) {
	--p;
	break;
      }
      --n;
    }
    n /= 2;
  }
  s = p->name + fncs.strings;
  return(*s == '_' ? s+1 : s);
}

char * wchan(unsigned int address)
{
  static char zero = 0;
  char *p;

  if (address) {
    p = find_func(address);
    
    if (strncmp(p, "sys_", 4) == 0)
      p += 4;
    while (*p == '_' && *p)
      ++p;
  } else { /* 0 address means not in kernel space */
    p = &zero;
  }
  return(p);
}
