# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""An API around ps(1)."""

import os
import sys
import platform

formats = {
    "     UID   PID  PPID  C    STIME TTY      TIME CMD": # Solaris 8, 9
    "XXXXXXXX XXXXX XXXXX XX XXXXXXXX XXXXXXX XXXXX *",
    "     UID   PID  PPID   C    STIME TTY         TIME CMD": # Solaris 10
    "XXXXXXXX XXXXX XXXXX XXX XXXXXXXX XXXXXXX XXXXXXXX *",
    "UID        PID  PPID  C STIME TTY          TIME CMD": # Linux
    "XXXXXXXX XXXXX XXXXX XX XXXXX XXXXXXX XXXXXXXXX *",
    "     UID    PID   PPID   C    STIME    TTY  TIME CMD": # AIX 5.3
    "XXXXXXXX XXXXXX XXXXXX XXX XXXXXXXX XXXXXX XXXXX *",
    "     UID   PID  PPID   C    STIME    TTY  TIME CMD": # AIX 5.2
    "XXXXXXXX XXXXX XXXXX XXX XXXXXXXX XXXXXX XXXXX *",
    "     UID   PID  PPID  C    STIME TTY       TIME COMMAND": # HP-UX
    "XXXXXXXX XXXXX XXXXX XX XXXXXXXX XXXXXXXX XXXXX *",
    "UID      PID  PPID   C STIME                         TTY      TIME COMMAND": # OSX
    "XXXXXX XXXXX XXXXX XXX XXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXX XXXXXXXXX *",

    }

def ps():
    """A generator returning the output of ps -ef or -s -efww.

    The output is parsed into lines and each line is parsed into fields.
    Each item yielded is a list of fields corresponding to one line.
    The first item yielded is the table heading.

    This works for Linux and Solaris.  There's no attempt at further
    generality yet.

    You may assume (but then should assert) that the table heading
    contains these fields, in this order:

        UID PID PPID C STIME TTY TIME CMD

    However the contents of some fields (esp. STIME and TIME) varies
    per OS.

    """
    if os.name != "posix":
        raise RuntimeError("Don't know how to get a process listing on %s" %
                           os.name)
    psbin = "/bin/ps"
    if sys.platform.startswith("linux"):
        # sys.platform is "linux" on Python 3 (was "linux2" on Python 2).
        flags = "-efww"
    elif platform.uname()[0] == "Darwin":
        flags = "-axwwo user=UID,pid,ppid,cpu=C,lstart=STIME,tt=TTY,time,command"
    else:
        flags = "-ef"
    pipe = os.popen("%s %s" % (psbin, flags), "r")
    # Determine the format
    format = None
    for line in pipe: # This "loop" only reads one line
        line = line.rstrip("\n")
        format = formats.get(line)
        break

    if platform.uname()[0] == "Darwin":
        # OSX changes the size of the field seperating whitespace according
        #  to the required number of characters needed for the fields.  For
        #  things like username or runtime that can change with every run
        #  we need to be more adaptable.  So, instead of using the fixed with
        #  formatting, split on space (which is safe in OSX) and construct the
        #  proper output.
        yield line.split()
        for line in pipe:
            s = line.split()
            s[4:9] = [' '.join(s[4:9])]
            s[7:] = [' '.join(s[7:])]
            yield s
    elif format:
        # Recognized fixed-width header (Solaris/AIX/HP-UX/older Linux): parse by
        # the column spans implied by the matching format template.
        # Yield the header line itself
        yield line.split()
        # Now turn the format into a list of column spans
        spans = []
        begin = 0
        for i, c in enumerate(format):
            if c == ' ':
                spans.append((begin, i))
                begin = i+1
        spans.append((begin, -1)) # The -1 nicely chops the trailing \n
        # Now parse the rest of the line using those column spans
        for line in pipe:
            yield [line[begin:end].strip() for begin, end in spans]
    else:
        # Unrecognized header. Modern procps (e.g. Ubuntu/Debian 24.04) emits the
        # standard "UID PID PPID C STIME TTY TIME CMD" columns but with different
        # spacing than the hardcoded formats above, so the exact-string lookup
        # misses. The first seven columns are single whitespace-separated tokens
        # and CMD is the remainder, so split positionally instead of raising
        # "don't understand this ps output".
        yield line.split()
        for line in pipe:
            fields = line.rstrip("\n").split(None, 7)
            if len(fields) >= 8:
                yield fields

    # Now close the pipe
    sts = pipe.close()
    if sts:
        raise RuntimeError("nonzero ps exit status (0x%x)" % sts)

if __name__ == "__main__":
    for record in ps():
        print("|".join(record))
