/* ps.c - Show running process statistics.
 *
 * Copyright 2013 Sandeep Sharma <sandeep.jack2756@gmail.com>
 * Copyright 2013 Kyungwan Han <asura321@gmail.com>
 * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ps.html

USE_PS(NEWTOY(ps, ">0o*T", TOYFLAG_BIN))

config PS
  bool "ps"
  default n
  help
    usage: ps [-o COL1,COL2=HEADER] [-T]
    
    Show list of processes
    -o COL1,COL2=HEADER Select columns for display
    -T      Show threads
*/
#define FOR_ps
#include "toys.h"

GLOBALS(
  struct arg_list *llist_o;
  unsigned screen_width;
)

#define BUFF_SIZE 1024
struct header_list {
  char *name;
  char *header;
  char *format;
  int width;
  int position;
  struct header_list *next;
};

struct header_list def_header[] = { 
  {"user", "USER", "%-*s ", 8, 0, NULL},
  {"group", "GROUP", "%-*s ", 8, 1, NULL},
  {"comm", "COMMAND", "%-*s ",16, 2, NULL},
  {"args", "COMMAND", "%-*s ",30, 3, NULL},
  {"pid", "PID", "%*s ", 5, 4, NULL},
  {"ppid","PPID", "%*s ", 5, 5, NULL},
  {"pgid", "PGID", "%*s ", 5, 6, NULL},
  {"etime","ELAPSED", "%*s ", 7, 7, NULL},
  {"nice", "NI", "%*s ", 5, 8, NULL},
  {"rgroup","RGROUP", "%-*s ", 8, 9, NULL},
  {"ruser","RUSER", "%-*s ", 8, 10, NULL},
  {"time", "TIME", "%*s ", 6, 11, NULL},
  {"tty", "TT", "%-*s ", 6, 12, NULL},
  {"vsz","VSZ", "%*s ", 7, 13, NULL},
  {"stat", "STAT", "%-*s ", 4, 14, NULL},
  {"rss", "RSS", "%*s ", 4, 15, NULL},
  {NULL, NULL, NULL, 0, 0, NULL},
};

struct header_list *o_list = NULL; //List of Header attributes.

/*
 * create list of header attributes taking care of -o (-o ooid=MOM..)
 * and width of attributes.
 */
static void list_add(struct header_list **list, struct header_list *data, char *c_data)
{
  struct header_list  *temp = *list, *new = xzalloc(sizeof(struct header_list));

  new->name = data->name;   
  if (c_data) new->header = c_data;
  else new->header = xstrdup(data->header);  
  if (c_data && (strlen(c_data) > data->width)) new->width = strlen(c_data);
  else new->width = data->width;
  new->format = data->format;
  new->position = data->position;

  if (temp) {
    while (temp->next != NULL) temp = temp->next;
    temp->next = new;
  } else (*list) = new;
}

//print the default header OR header with -o args
static void print_header(void)
{
  int i = 0;
  char *ptr = NULL, *str, *temp;
  struct arg_list *node = TT.llist_o;

  if (!(toys.optflags & FLAG_o)) {
    xprintf("  PID"" ""USER""      "" TIME"" ""COMMAND");
    list_add(&o_list, &def_header[4], NULL); //pid
    list_add(&o_list, &def_header[0], NULL); //user
    list_add(&o_list, &def_header[11], NULL); //time
    list_add(&o_list, &def_header[3], NULL); //comm
    xputc('\n');
    return ;
  }
  while (node) {
    i = 0;
    str = xstrdup(node->arg);
    while (str) {
      if ((ptr = strsep(&str, ","))) { //seprate list
        if ((temp = strchr(ptr, '='))) { // Handle ppid = MOM
          *temp = 0;
          temp++;
          while (def_header[i].name) {
            if (!(strcmp(def_header[i].name, ptr))) { // search from default header
              if (str) ptr = xmsprintf("%s,%s", temp, str); //handle condition like ppid = M,OM
              else ptr = xmsprintf("%s", temp);
              list_add(&o_list, &def_header[i], ptr);
              break;
            }
            i++; 
          }
          if (!def_header[i].name) perror_exit("Invalid arg for -o option");
          break;
        } else {
          while (def_header[i].name) {
            if (!(strcmp(def_header[i].name, ptr))) {
              list_add(&o_list, &def_header[i], NULL);
              break;
            }
            i++; 
          }
          if (!def_header[i].name) perror_exit("Invalid arg for -o option");
          i = 0;
        }
      }
    }
    node = node->next;
  }
  struct header_list *p = o_list;
  while (p) { //print Header
    printf(p->format , p->width, p->header);
    p = p->next;
  }
  xputc('\n');
}

//get uid/gid for processes.
static void get_uid_gid(char *p, char *id_str, unsigned *id)
{
  FILE *f; 
  if(!p) return;
  f = xfopen(p, "r");
  while (fgets(toybuf, BUFF_SIZE, f)) {
    if (!strncmp(toybuf, id_str, strlen(id_str))) {
      sscanf(toybuf, "%*s %u", id);
      break;
    }        
  }
  fclose(f);
}

//get etime for processes.
void get_etime(unsigned long s_time)
{
  unsigned long min;
  unsigned sec;
  struct sysinfo info;
  char *temp;
  sysinfo(&info);
  min = s_time/sysconf(_SC_CLK_TCK);
  min = info.uptime - min;
  sec = min % 60;
  min = min / 60;
  temp = xmsprintf("%3lu:%02u", min,sec);
  xprintf("%*.*s",7,7,temp);
  free(temp);
}

//get time attributes for processes.
void get_time(unsigned long s_time, unsigned long u_time)
{        
  unsigned long min;
  unsigned sec;
  char *temp;
  min = (s_time + u_time)/sysconf(_SC_CLK_TCK);
  sec = min % 60;
  min = min / 60;
  temp = xmsprintf("%3lu:%02u", min,sec);
  xprintf("%*.*s",6,6,temp);
  free(temp);
}

/*
 * read command line taking care of in between NUL's
 * in command line
 */
static void read_cmdline(int fd, char *cmd_ptr)
{
  int size = read(fd, cmd_ptr, 1024); //sizeof(cmd_buf)
  cmd_ptr[size] = '\0';
  while (--size > 0 && cmd_ptr[size] == '\0'); //reach to last char

  while (size >= 0) {
    if ((unsigned char)cmd_ptr[size] < ' ') cmd_ptr[size] = ' ';
    size--;
  }
}

/*
 * get the processes stats and print the stats 
 * corresponding to header attributes.
 */
static void do_ps_line(int pid, int tid)
{
  char *stat_buff = toybuf + 1024, *cmd_buff = toybuf + 2048;
  char state[4] = {0,};
  int tty, tty_major, tty_minor, fd, n, nice, width_counter = 0;
  struct stat stats;
  struct passwd *pw;
  struct group *gr;
  char *name, *user, *group, *ruser, *rgroup, *ptr;
  long rss;
  unsigned long stime, utime, start_time, vsz;
  unsigned ppid, ruid, rgid, pgid;
  struct header_list *p = o_list;

  sprintf(stat_buff, "/proc/%d", pid);
  if(stat(stat_buff, &stats)) return;

  if (tid) {
    if (snprintf(stat_buff, BUFF_SIZE, "/proc/%d/task/%d/stat", pid, tid) >= BUFF_SIZE) return;
    if (snprintf(cmd_buff, BUFF_SIZE, "/proc/%d/task/%d/cmdline", pid, tid) >= BUFF_SIZE) return;
  } else {
    if (snprintf(stat_buff, BUFF_SIZE, "/proc/%d/stat", pid) >= BUFF_SIZE) return;
    if (snprintf(cmd_buff, BUFF_SIZE, "/proc/%d/cmdline", pid) >= BUFF_SIZE) return;
  }

  fd = xopen(stat_buff, O_RDONLY);
  n = readall(fd, stat_buff, BUFF_SIZE);
  xclose(fd);
  if (n < 0) return;
  stat_buff[n] = 0; //Null terminate the buffer.
  ptr = strchr(stat_buff, '(');
  ptr++;
  name = ptr;
  ptr = strrchr(stat_buff, ')');
  *ptr = '\0'; //unecessary if?
  name = xmsprintf("[%s]", name);
  ptr += 2; // goto STATE
  n = sscanf(ptr, "%c %u %u %*u %d %*s %*s %*s %*s %*s %*s "
      "%lu %lu %*s %*s %*s %d %*s %*s %lu %lu %ld",            
      &state[0],&ppid, &pgid, &tty, &utime, &stime,
      &nice,&start_time, &vsz,&rss);

  if (tid) pid = tid;
  vsz >>= 10; //Convert into KB
  rss = rss * 4; //Express in pages
  tty_major = (tty >> 8) & 0xfff;
  tty_minor = (tty & 0xff) | ((tty >> 12) & 0xfff00);

  if (vsz == 0 && state[0] != 'Z') state[1] = 'W';
  else state[1] = ' ';
  if (nice < 0 ) state[2] = '<';
  else if (nice) state[2] = 'N';
  else state[2] = ' ';

  if (tid) {
    if (snprintf(stat_buff, BUFF_SIZE, "/proc/%d/task/%d/status", pid, tid) >= BUFF_SIZE)
      goto clean;
  } else {
    if (snprintf(stat_buff, BUFF_SIZE, "/proc/%d/status", pid) >= BUFF_SIZE)
      goto clean;
  }

  fd = -1;
  while (p) {
    int width;
    width = p->width;
    width_counter += (width + 1); //how much screen we hv filled, +1, extra space b/w headers
    switch (p->position) {
      case 0:
        pw = getpwuid(stats.st_uid);
        if (!pw) user = xmsprintf("%d",(int)stats.st_uid);
        else user = xmsprintf("%s", pw->pw_name);
        printf("%-*.*s", width, width, user);
        free(user);
        break;
      case 1:
        gr = getgrgid(stats.st_gid);
        if (!gr) group = xmsprintf("%d",(int)stats.st_gid);
        else group = xmsprintf("%s", gr->gr_name);
        printf("%-*.*s", width, width, group);
        free(group);
        break;
      case 2:
        name[strlen(name) - 1] = '\0';
        printf("%-*.*s", width,width, name + 1);
        name[strlen(name)] = ']'; //Refill it for further process.
        break;
      case 3:
        {
          int j = 0;
          width_counter -= width;
          if(p->next) j = width; //is args is in middle. ( -o pid,args,ppid)
          else j = (TT.screen_width - width_counter % TT.screen_width); //how much screen left.
          if (fd == -1) fd = open(cmd_buff, O_RDONLY); //don't want to die
          else xlseek(fd, 0, SEEK_SET);
          if (fd < 0) cmd_buff[0] = 0;
          else read_cmdline(fd, cmd_buff); 
          if (cmd_buff[0]) printf("%-*.*s", j, j, cmd_buff);
          else printf("%-*.*s", j, j, name);
          width_counter += width;
          break;
        }
      case 4:
        printf("%*d", width, pid);
        break;
      case 5:
        printf("%*d", width, ppid);
        break;
      case 6:
        printf("%*d", width, pgid);
        break;
      case 7:
        get_etime(start_time);
        break;
      case 8:
        printf("%*d", width, nice);
        break;
      case 9:
        get_uid_gid(stat_buff, "Gid:", &rgid);
        gr = getgrgid(rgid);
        if (!gr) rgroup = xmsprintf("%d",(int)stats.st_gid);
        else rgroup = xmsprintf("%s", gr->gr_name);
        printf("%-*.*s", width,width, rgroup);
        free(rgroup);
        break;
      case 10:
        get_uid_gid(stat_buff, "Uid:", &ruid);
        pw = getpwuid(ruid);
        if (!pw) ruser = xmsprintf("%d",(int)stats.st_uid);
        else ruser = xmsprintf("%s", pw->pw_name);
        printf("%-*.*s", width, width, ruser);
        free(ruser);
        break;
      case 11:
        get_time(utime, stime);
        break;
      case 12:
        if (tty_major) {
          char *temp = xmsprintf("%d,%d", tty_major,tty_minor);
          printf("%-*s", width, temp);
          free(temp);
        } else printf("%-*s", width, "?");
        break;
      case 13:
        printf("%*lu", width, vsz);
        break;
      case 14:
        printf("%-*s", width, state);
        break;
      case 15:
        printf("%*lu", width, rss);
        break;
    }
    p = p->next;
    xputc(' '); //space char
  }
  if (fd >= 0) xclose(fd);
  xputc('\n');
clean:
  free(name);
}

//Do stats for threads (for -T option)
void do_ps_threads(int pid)
{       
  DIR *d; 
  int tid;
  struct dirent *de;
  char *tmp = xmsprintf("/proc/%d/task",pid);

  if (!(d = opendir(tmp))) {
    free(tmp);
    return;
  }
  while ((de = readdir(d))) {
    if (isdigit(de->d_name[0])) {
      tid = atoi(de->d_name);
      if (tid == pid) continue;
      do_ps_line(pid, tid);
    }
  }        
  closedir(d); 
  free(tmp);
}

void ps_main(void)
{
  DIR *dp;
  struct dirent *entry;
  int pid;
  
  TT.screen_width = 80; //default width
  terminal_size(&TT.screen_width, NULL);
  print_header();

  if (!(dp = opendir("/proc"))) perror_exit("opendir");
  while ((entry = readdir(dp))) {
    if (isdigit(*entry->d_name)) {
      pid = atoi(entry->d_name);
      do_ps_line(pid, 0);
      if (toys.optflags & FLAG_T)
        do_ps_threads(pid);
    }
  }
  closedir(dp);
  if (CFG_TOYBOX_FREE) {
    struct header_list *temp = o_list;
    while(temp) {
      o_list = o_list->next;
      free(temp->header);
      free(temp);
      temp = o_list;
    }
  }
}