# HG changeset patch
# User jfp <jf.pieronne@laposte.net>
# Date 1684916340 -7200
#      Wed May 24 10:19:00 2023 +0200
# Node ID 58e996ba25f8b3f904f2a8fe1cb0339b2b5f4292
# Parent  b3bf2ad04df4b055d21c8bd1f8e22b3655ab70c7
Add supprt to kt_limit, quotas, privileges, nouaf

diff --git a/supervisord.conf_template b/supervisord.conf_template
--- a/supervisord.conf_template
+++ b/supervisord.conf_template
@@ -1,3 +1,7 @@
+[supervisord]
+nodaemon = false
+user = system
+command = dev:[dir]supervisord.com
 [program:firstpgm]
 command=dev:[dir]PGM1.COM
 process_name=PGM1D
@@ -8,6 +12,15 @@
 startretries=3
 stopwaitsecs=10
 user=vmsuser
+kt_limit=1
+# if privileges or quotas are specified nouaf is implicitly define to true
+# else default false
+# nouaf=true
+# cpuplm express in seconds
+# quotas= astlm=300, diolm=250,, cpulm=60,
+#        biolm=20, pgflquota=1000000
+# privileges= SYSNAM, PRMMBX, IMPERSONATE,
+#             WORLD
 autorestart=unexpected
 # SS$_NORMAL, SS$_FORCEX, SS$_EXITFORCED
 exitcodes=1,11228,11220
diff --git a/supervisord.py b/supervisord.py
--- a/supervisord.py
+++ b/supervisord.py
@@ -1,5 +1,5 @@
 #
-# Required  privileges: SYSNAM, PRMMBX, IMPERSONATE, WORLD
+# Required  privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD
 #
 import argparse
 import configparser
@@ -11,7 +11,7 @@
 import queue
 from dataclasses import dataclass, field
 from enum import IntEnum
-from typing import Any, Dict, List, Literal, Tuple, Type
+from typing import Any, Dict, List, Literal, Tuple, Type, NewType
 
 from ovms import (
     accdef,
@@ -23,6 +23,8 @@
     jpidef,
     lnmdef,
     mbxqio,
+    pqldef,
+    prcdef,
     prvdef,
     psldef,
     ssdef,
@@ -48,6 +50,73 @@
 timer_astctx = vmsast.AstContext(vmsast.M_WAKE | vmsast.M_QUEUE)
 
 
+# Define a new type called "PrvMask", which is internally an `int`
+PrvMask = NewType('PrvMask', int)
+# Ditto for "PidType"
+PidType = NewType('PidType', int)
+
+privileges: Dict[str, PrvMask] = {
+    'NOSAME': PrvMask(0),
+    'ACNT': PrvMask(prvdef.PRV_M_ACNT),
+    'ALLSPOOL': PrvMask(prvdef.PRV_M_ALLSPOOL),
+    'ALTPRI': PrvMask(prvdef.PRV_M_ALTPRI),
+    'AUDIT': PrvMask(prvdef.PRV_M_AUDIT),
+    'BUGCHK': PrvMask(prvdef.PRV_M_BUGCHK),
+    'BYPASS': PrvMask(prvdef.PRV_M_BYPASS),
+    'CMEXEC': PrvMask(prvdef.PRV_M_CMEXEC),
+    'CMKRNL': PrvMask(prvdef.PRV_M_CMKRNL),
+    'DIAGNOSE': PrvMask(prvdef.PRV_M_DIAGNOSE),
+    'DOWNGRADE': PrvMask(prvdef.PRV_M_DOWNGRADE),
+    'EXQUOTA': PrvMask(prvdef.PRV_M_EXQUOTA),
+    'GROUP': PrvMask(prvdef.PRV_M_GROUP),
+    'GRPNAM': PrvMask(prvdef.PRV_M_GRPNAM),
+    'GRPPRV': PrvMask(prvdef.PRV_M_GRPPRV),
+    'IMPERSONATE': PrvMask(prvdef.PRV_M_IMPERSONATE),
+    'IMPORT': PrvMask(prvdef.PRV_M_IMPORT),
+    'LOG_IO': PrvMask(prvdef.PRV_M_LOG_IO),
+    'MOUNT': PrvMask(prvdef.PRV_M_MOUNT),
+    'NETMBX': PrvMask(prvdef.PRV_M_NETMBX),
+    'OPER': PrvMask(prvdef.PRV_M_OPER),
+    'PFNMAP': PrvMask(prvdef.PRV_M_PFNMAP),
+    'PHY_IO': PrvMask(prvdef.PRV_M_PHY_IO),
+    'PRMCEB': PrvMask(prvdef.PRV_M_PRMCEB),
+    'PRMGBL': PrvMask(prvdef.PRV_M_SYSGBL),
+    'PRMMBX': PrvMask(prvdef.PRV_M_PRMMBX),
+    'PSWAPM': PrvMask(prvdef.PRV_M_PSWAPM),
+    'READALL': PrvMask(prvdef.PRV_M_READALL),
+    'SECURITY': PrvMask(prvdef.PRV_M_SECURITY),
+    'SETPRV': PrvMask(prvdef.PRV_M_SETPRV),
+    'SHARE': PrvMask(prvdef.PRV_M_SHARE),
+    'SHMEM': PrvMask(prvdef.PRV_M_SHMEM),
+    'SYSGBL': PrvMask(prvdef.PRV_M_SYSGBL),
+    'SYSLCK': PrvMask(prvdef.PRV_M_SYSLCK),
+    'SYSNAM': PrvMask(prvdef.PRV_M_SYSNAM),
+    'SYSPRV': PrvMask(prvdef.PRV_M_SYSPRV),
+    'TMPMBX': PrvMask(prvdef.PRV_M_TMPMBX),
+    'UPGRADE': PrvMask(prvdef.PRV_M_UPGRADE),
+    'VOLPRO': PrvMask(prvdef.PRV_M_VOLPRO),
+    'WORLD': PrvMask(prvdef.PRV_M_WORLD),
+}
+
+
+quotas: Dict[str, int] = {
+    'ASTLM': pqldef.PQL__ASTLM,
+    'BIOLM': pqldef.PQL__BIOLM,
+    'BYTLM': pqldef.PQL__BYTLM,
+    'CPULM': pqldef.PQL__CPULM,
+    'DIOLM': pqldef.PQL__DIOLM,
+    'ENQLM': pqldef.PQL__ENQLM,
+    'FILLM': pqldef.PQL__FILLM,
+    'JTQUOTA': pqldef.PQL__JTQUOTA,
+    'PGFLQUOTA': pqldef.PQL__PGFLQUOTA,
+    'PRCLM': pqldef.PQL__PRCLM,
+    'TQELM': pqldef.PQL__TQELM,
+    'WSDEFAULT': pqldef.PQL__WSDEFAULT,
+    'WSEXTENT': pqldef.PQL__WSEXTENT,
+    'WSQUOTA': pqldef.PQL__WSQUOTA,
+}
+
+
 def start_timer():
     global timer_delay, timer_astctx
     timer_astctx.reset()
@@ -135,7 +204,7 @@
 
 
 class ProcessInfo(object):
-    pid: int
+    pid: PidType
     state: ProcessStates
     finalsts: int | None
     start_time: datetime.datetime | None
@@ -143,7 +212,7 @@
 
     def __init__(
         self,
-        pid: int = 0,
+        pid: PidType = PidType(0),
         start_time: datetime.datetime | None = None,
         state: ProcessStates = ProcessStates.STOPPED,
     ) -> None:
@@ -166,9 +235,13 @@
     startsecs: int
     startretries: int
     stopwaitsecs: int
+    kt_limit: int
+    nouaf: bool
+    quotas: List[Tuple[int, int]] | None
+    prv: PrvMask | None
     user: bytes
     process: ProcessInfo
-    running_processes: Dict[int, 'Program']
+    running_processes: Dict[PidType, 'Program']
     running_processes = dict()
     programs: Dict[str, 'Program']
     programs = dict()
@@ -195,6 +268,10 @@
         startsecs: int,
         startretries: int,
         stopwaitsecs: int,
+        nouaf: bool,
+        quotas: List[Tuple[int, int]] | None,
+        prv: PrvMask | None,
+        kt_limit: int,
         autorestart: (
             Type[RestartUnconditionally]
             | Type[RestartWhenExitUnexpected]
@@ -217,6 +294,10 @@
         self.timer_item = None
         self.kill_request = False
         self.autorestart = autorestart
+        self.nouaf = nouaf
+        self.quotas = quotas
+        self.prv = prv
+        self.kt_limit = kt_limit
         self.exitcodes = exitcodes
 
         self.process = ProcessInfo()
@@ -266,7 +347,7 @@
                 self.process_name + b'_PID',
                 supervisord_table_name,
             )[1]
-            pid = int(v, 16)
+            pid = PidType(int(v, 16))
             while True:
                 try:
                     itime, stime = lib.getjpi(jpidef.JPI__LOGINTIM, pid)[2:]
@@ -276,7 +357,7 @@
                         raise
             itime = crtl.fix_time(itime)
             assert itime is not None
-            self.process.pid = pid
+            self.process.pid = PidType(pid)
             self.process.start_time = datetime.datetime.fromtimestamp(itime)
             self.set_running()
 
@@ -305,23 +386,64 @@
                 self.timer_item = None
 
         self.process.finalsts = None
+        # If the image argument specifies the SYS$SYSTEM:LOGINOUT.EXE,
+        #  the UIC of the created process will be the UIC of the caller of $CREPRC,
+        # and the UIC parameter is ignored.
+        # So, we need to use persona
         userpro = starlet.create_user_profile(usrnam=self.user)[1]
         persona_id = starlet.persona_create(usrpro=userpro)[1]
         persona_previous_id = starlet.persona_assume(persona_id)[1]
-        starlet.persona_assume(persona_previous_id)
-        uic = lib.getjpi(jpidef.JPI__UIC)[2]
-        starlet.persona_delete(persona_id)
-        # Create a detach process (uic is specified)
-        s, pid, d = starlet.creprc(
-            uic=uic,
-            image=b'SYS$SYSTEM:LOGINOUT.EXE',
-            input=self.command,
-            output=self.stdout_file,
-            error=self.stderr_file,
-            prcnam=self.process_name,
-            mbxunt=Program.mbxunt,
-        )
+        # Create a detach process (stsflg=prvdef.PRV_M_IMPERSONATE)
+        # From John Gillings:
+        # (https://community.hpe.com/t5/operating-system-openvms/run-uic-vs-sys-creprc/td-p/5171510)
+        #         General comment about PRC$M_NOUAF ...
+        # When you run LOGINOUT in a detached process *WITHOUT* specifying PRC$M_NOUAF
+        # for $CREPRC, or *WITH* /AUTHORIZE on the DCL RUN command, (note that the
+        # default is reversed between the two), many of the process attributes you have
+        # specified with qualifiers or parameters may be overridden in the resulting
+        # process.
+        # That includes UIC, process name, quotas and privileges. Behind the scenes,
+        # the process is created with whatever you specified, but LOGINOUT replaces
+        # them with values from the UAF.
+        #
+        # From https://docs.vmssoftware.com/vsi-openvms-programming-concepts-manual-volume-i/
+        # The SYS$CREPRC system service also does not provide default equivalence names
+        # for the logical names SYS$LOGIN, SYS$LOGIN_DEVICE, and SYS$SCRATCH.
+        # These logical names are available to the created process only when the
+        # specified image is LOGINOUT, and when the PRC$M_NOUAF flag is not set.
+        try:
+            stsflg = (
+                prcdef.PRC_M_IMPERSONATE
+                | prcdef.PRC_M_PARSE_EXTENDED
+                | prcdef.PRC_M_KT_LIMIT
+            )
+            if self.nouaf:
+                stsflg |= prcdef.PRC_M_NOUAF
 
+            pid = PidType(
+                starlet.creprc(
+                    image=b'SYS$SYSTEM:LOGINOUT.EXE',
+                    input=self.command,
+                    output=self.stdout_file,
+                    error=self.stderr_file,
+                    prcnam=self.process_name,
+                    mbxunt=Program.mbxunt,
+                    quota=self.quotas,
+                    prv=self.prv,
+                    kt_limit=self.kt_limit,
+                    # prcdef.PRC_M_DETACH and prcdef.PRC_M_IMPERSONATE are synonyms
+                    stsflg=stsflg,
+                )[1]
+            )
+        except OSError as e:
+            logger.warning(
+                f"Can' create process {self.process_name}, error {e}"
+            )
+            self.process.state = ProcessStates.FATAL
+            return
+        finally:
+            starlet.persona_assume(persona_previous_id)
+            starlet.persona_delete(persona_id)
         lib.set_logical(
             self.process_name + b'_PID',
             hex(pid)[2:].upper(),
@@ -333,7 +455,7 @@
             supervisord_table_name,
         )
 
-        self.process.pid = pid
+        self.process.pid = PidType(pid)
         self.process.start_time = datetime.datetime.now()
         if self.startsecs == 0:
             self.set_running()
@@ -343,7 +465,7 @@
             timer_queue.put(
                 PrioritizedItem(current_tick + self.startsecs, self.timer_item)
             )
-        Program.running_processes[pid] = self
+        Program.running_processes[PidType(pid)] = self
         lib.set_logical(
             self.process_name + b'_BEG',
             str(self.process.start_time)[:19],
@@ -659,7 +781,9 @@
         t_astctxt = qio_procterm(fterm)  # noqa: F841
         c_astctxt = qio_cmd(fcmd)  # noqa: F841
 
-        for pgm in sorted(Program.programs.values(), key=lambda pgm: pgm.priority):
+        for pgm in sorted(
+            Program.programs.values(), key=lambda pgm: pgm.priority
+        ):
             if pgm.autostart:
                 pgm.create_process(False)
 
@@ -710,11 +834,13 @@
     handler.setFormatter(formatter)
     main_log.addHandler(handler)
 
-    # Required privileges: SYSNAM, PRMMBX, IMPERSONATE, WORLD
+    # Required privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD
     starlet.setprv(
         1,
         prvdef.PRV_M_SYSNAM
+        | prvdef.PRV_M_ALTPRI
         | prvdef.PRV_M_PRMMBX
+        | prvdef.PRV_M_SETPRV
         | prvdef.PRV_M_IMPERSONATE
         | prvdef.PRV_M_WORLD,
     )
@@ -744,6 +870,40 @@
 
     for sn in config.sections():
         if sn.startswith('program:'):
+            quotaslst = [
+                quota.strip().upper().split('=')
+                for quota in config[sn].get('quotas', '').split(',')
+                if quota != ''
+            ]
+            prcquotas = []
+            for quotan, quotav in quotaslst:
+                if quotan not in quotas:
+                    print(f'{sn}: {quotan}={quotav} not a valid quota')
+                    crtl.vms_exit(ssdef.SS__INVARG)
+                if quotan == 'CPULM':
+                    print('>>>', repr(quotav))
+                    quotav = int(quotav) * 100
+                prcquotas.append((quotas[quotan], int(quotav)))
+            if prcquotas == []:
+                prcquotas = None
+
+            prvnames = [
+                name.strip().upper()
+                for name in config[sn].get('privileges', '').split(',')
+                if name != ''
+            ]
+            prv: PrvMask | None
+            prv = None
+            for prvnam in prvnames:
+                if prvnam not in privileges:
+                    print(f'{sn}: {prvnam} not a valid privilege')
+                    crtl.vms_exit(ssdef.SS__INVARG)
+                prv = (
+                    privileges[prvnam]
+                    if prv is None
+                    else PrvMask(prv | privileges[prvnam])
+                )
+
             name = sn.split(':')[-1].upper()
             process_name = config[sn]['process_name']
             autostart = config[sn].getboolean('autostart', False)
@@ -751,6 +911,8 @@
             stdout_file = config[sn].get('stdout_file', 'NLA0:')
             stderr_file = config[sn].get('stderr_file', 'NLA0:')
             priority = config[sn].getint('priority', 999)
+            nouaf = config[sn].getboolean('nouaf', False)
+            kt_limit = config[sn].getint('kt_limit', 0)
             startsecs = config[sn].getint('startsecs', 10)
             startretries = config[sn].getint('startretries', 3)
             stopwaitsecs = config[sn].getint('stopwaitsescs', 10)
@@ -759,6 +921,8 @@
             )
             exitcodes = config[sn].get('exitcodes', '1').split(',')
             user = config[sn]['user']
+            if prv is not None or quotaslst is not None:
+                nouaf = True
             p = Program(
                 name=name,
                 user=user,
@@ -772,6 +936,10 @@
                 startretries=startretries,
                 stopwaitsecs=stopwaitsecs,
                 autorestart=autorestart,
+                nouaf=nouaf,
+                quotas=prcquotas,
+                prv=prv,
+                kt_limit=kt_limit,
                 exitcodes=[
                     int(exitcode) for exitcode in exitcodes if exitcode != ''
                 ],