
// Jps.java
// Andrew Davison, ad@coe.psu.ac.th, June 2023

/* A simplified version of ps
   Man page: https://man7.org/linux/man-pages/man1/ps.1.html

   Usage: Jps [-ahlV] [-c=<cmdName>] [-r=<min-max>] [-p=PID[,PID...]]...
   A simple Java version of ps.
     -a, --all                 print info about all processes
     -c, --command=<cmdName>   list only command name processes
     -h, --help                Show this help message and exit.
     -l, --label               print field labels
     -p, --pids=PID[,PID...]   selects the processes in pidlist
     -r, --range=<min-max>     selects pids based on min-max range
     -V, --version             Print version information and exit.

  Picocli features:
    * named options and positional parameters
    * POSIX-style grouped short options 
    * automatic help and version options
    * exit codes
    * Split Regex
    * option labels
    * arity
    * Value Validation
    * use of ParameterException

  Usage examples:
    > compile Jps.java

    > run Jps
    > run Jps -l
    > run Jps -al
    > run Jps -r=10000-15000
    > run Jps -p=11224,11900,11904
    > run Jps -c=note
*/

import java.io.*;
import java.util.*;
import java.time.*;
import java.text.*;
import java.util.concurrent.*;

import picocli.CommandLine;
import picocli.CommandLine.*;
import picocli.CommandLine.Model.CommandSpec;


@Command(name = "Jps", 
         mixinStandardHelpOptions = true,
         version = "Jps 1.0",
         description = "A simple Java version of ps.")
public class Jps implements Callable<Integer>
{

  private static final SimpleDateFormat sdf = 
         new SimpleDateFormat("dd-MM-yyyy;HH:mm:ss");


  @CommandLine.Option(names = {"-a", "--all"},
                      description = "print info on all processes")
  private boolean showAll = false;

  @CommandLine.Option(names = {"-l", "--label"},
                      description = "print field labels")
  private boolean showLabels = false;

  @Option(names = { "-c", "--command" }, 
          description = "list only processes for command name")
  private String cmdName = null;

  @Option(names = { "-p", "--pids" }, 
          split=",", paramLabel = "PID",
          description = "list the processes in pidlist")
  private long[] pidlist;


  private long[] pidRange;
  @Spec CommandSpec spec;

  @Option(names = { "-r", "--range"}, 
          paramLabel = "<min-max>",
          description = "list processes in pid range")
  public void setRange(String vals)  // validation
  // convert string to an array of two pids: [ min, max ]
  {
    String[] valsArr = vals.split("-");
    if (valsArr.length != 2) 
      throw new ParameterException(
                spec.commandLine(), String.format(
                      "Invalid range format %s", vals) );
    else {
      long[] vs = new long[2];
      vs[0] = getLong(valsArr[0]);
      vs[1] = getLong(valsArr[1]);
      if (vs[0] > vs[1])
        throw new ParameterException(
                  spec.commandLine(), String.format(
                           "Wrong range order %s", vals) );
      else
        pidRange = vs;
    }
  }  // end of setRange()


  private String today;
  private String usrName;

  public Integer call()
  { 
    if (showLabels) {
      System.out.printf("%7s %15s %10s %2s %4s %7s %22s %5s\n",
               "PID", "Command", "User", "Al", "Kids", 
               "PPID", center("Start Time", 20), "CPU");
      System.out.print(" " + "-".repeat(80) + "\n");
    }

    usrName = System.getProperty("user.name");
    if (cmdName != null)
      cmdName = cmdName.toLowerCase();

    if (pidlist != null)
      Arrays.sort(pidlist);  // so can use binary search

    SimpleDateFormat dayDF = new SimpleDateFormat("dd-MM-yyyy");
    today = dayDF.format(new Date());  // only store the day part

    ProcessHandle.allProcesses().forEach(
                              ph -> examineProc(ph));
    return 0;
  }  // end of call()


  public static long getLong(String s)
  { 
    long num = 0;
    try {
      num = Long.parseLong(s);
    }
    catch (NumberFormatException ex){ 
      System.out.println("Incorrect format for int " + s); 
    }
    return num;
  }  // end of getLong()



  private void examineProc(ProcessHandle ph)
  // print the details for the ph process
  {
    ProcessHandle.Info pInfo = ph.info();

    if (showAll)
      System.out.println( procDetails(ph, pInfo));
    else if (pidRange != null) {
      if ((ph.pid() >= pidRange[0]) && (ph.pid() <= pidRange[1]))
        System.out.println( procDetails(ph, pInfo));
    }
    else if (pidlist != null) {
      if (Arrays.binarySearch(pidlist, ph.pid()) >= 0)  // found
        System.out.println( procDetails(ph, pInfo));
    }
    else if (cmdName != null) {
      String cmd = optString( pInfo.command()).toLowerCase();
      if (cmd.contains(cmdName))  // allows for a partial match
        System.out.println( procDetails(ph, pInfo));
    }
    else {  // only show user's processes
      String pName = optString( pInfo.user());
      if (pName.contains(usrName))
        System.out.println( procDetails(ph, pInfo));
    }
  }  // end of examineProc()


  private String procDetails(ProcessHandle ph, 
                       ProcessHandle.Info pInfo)
  // format the process details for the ph process
  {
    long duration = pInfo.totalCpuDuration()
                        .orElse(Duration.ZERO)
                        .getSeconds();

    return String.format("%7d %15s %10s %2s %4d %7s %22s %5d",
        ph.pid(), 
        getCommand(optString( pInfo.command())),
        getUser( optString( pInfo.user())),
        ph.isAlive() ? "t" : "F",
        ph.children().count(),
        optString( ph.parent().map(ProcessHandle::pid)),
        center( getStartTime(pInfo), 20), duration
      );
  } /* procDetails */



  private String getStartTime(ProcessHandle.Info pInfo)
  // return "day;time" if the day is not today, or just "time"
  {
    Instant start = pInfo.startInstant()
                         .orElse(Instant.now());
    String startStr = sdf.format( Date.from(start));
    if (startStr.contains(today))  // it is today, so strip the day
      startStr = startStr.substring( startStr.indexOf(";")+1);
    return startStr;
  } // end of getStartTime()


  private String optString(Optional<?> optional)
  // return the data or "-"
  {  return optional.map( Object::toString ).orElse("-");  }


  private String getUser(String user)
  // strip away any path prefix on the user name
  {
    int index = user.lastIndexOf(File.separator);
    return (index == -1) ? user : user.substring(index + 1);
  }

  private String getCommand(String cmdName)
  /* strip away any path prefix on the command name,
     and also ".exe" at the end of the command, and
     truncate the string to <= 15 chars
  */
  {
    int index = cmdName.lastIndexOf(File.separator);
    if (index != -1)
      cmdName = cmdName.substring(index + 1);

    index = cmdName.lastIndexOf(".exe");
    if (index != -1)
      cmdName =  cmdName.substring(0, index);

    if (cmdName.length() > 15)
      return cmdName.substring(0, 12) + "...";
    else
      return cmdName;
  } // end of getCommand()


  private String center(String s, int len) 
  // center the string in len spaces
  {
    StringBuilder sb = new StringBuilder(len);
    sb.setLength((len - s.length())/2);
    sb.append(s);
    sb.setLength(len);
    return sb.toString().replace('\0', ' ');
  }  // end of center()


  // ---------------------------------------------

  public static void main(String[] args)
  {
    int exitCode = new CommandLine(new Jps()).execute(args);
    System.exit(exitCode); 
  }


}  // end of Jps class

