Skip to content
Snippets Groups Projects
supervisord.py 33.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • jfp's avatar
    jfp committed
    #
    
    # Required  privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD
    
    jfp's avatar
    jfp committed
    #
    
    jfp's avatar
    jfp committed
    import configparser
    import ctypes
    import datetime
    import json
    import logging
    import logging.handlers
    import queue
    from dataclasses import dataclass, field
    from enum import IntEnum
    
    from typing import Any, Dict, List, Literal, Tuple, Type, NewType
    
    jfp's avatar
    jfp committed
    
    from ovms import (
        accdef,
        cmbdef,
        crtl,
        dvidef,
        iodef,
        iosbdef,
        jpidef,
        lnmdef,
        mbxqio,
    
    jfp's avatar
    jfp committed
        prvdef,
        psldef,
        ssdef,
        starlet,
        stsdef,
    )
    from ovms import ast as vmsast
    from ovms.rtl import lib
    
    logger: logging.Logger
    logger = logging.getLogger('supervisord')
    
    supervisord_table_name = b'SUPERVISORD_TABLE'
    current_tick: int = 0
    timer_queue: queue.PriorityQueue['PrioritizedItem'] = queue.PriorityQueue()
    
    
    jfp's avatar
    jfp committed
    
    
    TRUTHY_STRINGS = ('yes', 'true', 'on', '1')
    FALSY_STRINGS = ('no', 'false', 'off', '0')
    
    
    jfp's avatar
    jfp committed
    timer_delay = starlet.bintim('0 0:00:01.00')[1]
    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,
    }
    
    
    
    jfp's avatar
    jfp committed
    def start_timer():
        global timer_delay, timer_astctx
        timer_astctx.reset()
        starlet.setimr(daytim=timer_delay, reqidt=timer_astctx)
    
    
    
    class RestartWhenExitUnexpected:
        pass
    
    
    class RestartUnconditionally:
        pass
    
    
    def auto_restart(
        value: str,
    ) -> (
        Type[RestartUnconditionally]
        | Type[RestartWhenExitUnexpected]
        | Literal[False]
    ):
        value = str(value.lower())
        computed_value = value
        if value in TRUTHY_STRINGS:
            computed_value = RestartUnconditionally
        elif value in FALSY_STRINGS:
            computed_value = False
        elif value == 'unexpected':
            computed_value = RestartWhenExitUnexpected
        if computed_value not in (
            RestartWhenExitUnexpected,
            RestartUnconditionally,
            False,
        ):
            raise ValueError("invalid 'autorestart' value %r" % value)
        return computed_value
    
    
    
    jfp's avatar
    jfp committed
    class TimerItemType(IntEnum):
        UNDEF = 0
        PROC_STARTING = 1
        PROC_BACKOFF = 2
    
    jfp's avatar
    jfp committed
    
    
    class TimerItem(object):
        typ: TimerItemType
        value: Any
        cancel: bool
        done: bool
    
        def __init__(
            self,
            typ: TimerItemType = TimerItemType.UNDEF,
            value: Any = None,
        ) -> None:
            self.typ = typ
            self.value = value
            self.cancel = False
            self.done = False
    
    
    @dataclass(order=True)
    class PrioritizedItem:
        priority: int
        item: TimerItem = field(compare=False)
    
    
    class AstParamType(IntEnum):
        PROCTERM = 1
        CMD = 2
        TIMER = 3
    
    
    # http://supervisord.org/subprocess.html#process-states
    class ProcessStates(IntEnum):
        STOPPED = 0
        STARTING = 10
        RUNNING = 20
        BACKOFF = 30
        STOPPING = 40
        EXITED = 100
        FATAL = 200
        UNKNOWN = 1000
    
    
    class ProcessInfo(object):
    
    jfp's avatar
    jfp committed
        state: ProcessStates
        finalsts: int | None
        start_time: datetime.datetime | None
        end_time: datetime.datetime | None
    
        def __init__(
            self,
    
            pid: PidType = PidType(0),
    
    jfp's avatar
    jfp committed
            start_time: datetime.datetime | None = None,
            state: ProcessStates = ProcessStates.STOPPED,
        ) -> None:
            self.pid = pid
            self.start_time = start_time
            self.end_time = None
            self.finalsts = None
            self.state = state
    
    
    class Program(object):
        mbxunt = 0
        name: str
    
    jfp's avatar
    jfp committed
        command: bytes
        program_name: bytes
        autostart: bool
    
    jfp's avatar
    jfp committed
        priority: int
    
    jfp's avatar
    jfp committed
        stdout_logfile: bytes
        stderr_logfile: bytes
        startsecs: int
        startretries: int
        stopwaitsecs: int
    
        kt_limit: int
    
        nouaf: bool
        quotas: List[Tuple[int, int]] | None
        prv: PrvMask | None
    
    jfp's avatar
    jfp committed
        user: bytes
        process: ProcessInfo
    
        running_processes: Dict[PidType, 'Program']
    
    jfp's avatar
    jfp committed
        running_processes = dict()
        programs: Dict[str, 'Program']
        programs = dict()
        remain_startretries: int
        timer_item = TimerItem | None
        kill_request: bool
    
        autorestart: (
            Type[RestartUnconditionally]
            | Type[RestartWhenExitUnexpected]
            | Literal[False]
        )
        exitcodes: List[int]
    
    jfp's avatar
    jfp committed
    
        def __init__(
            self,
            name: str,
            user: str,
            command: str,
    
    jfp's avatar
    jfp committed
            process_name: str,
            autostart: bool,
    
    jfp's avatar
    jfp committed
            priority: int,
    
    jfp's avatar
    jfp committed
            stdout_file: str,
            stderr_file: str,
            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]
                | Literal[False]
            ),
            exitcodes: List[int],
    
    jfp's avatar
    jfp committed
        ):
            self.name = name
            self.user = user.encode()
    
    jfp's avatar
    jfp committed
            self.command = command.encode()
            self.process_name = process_name.encode()
            self.autostart = autostart
    
    jfp's avatar
    jfp committed
            self.priority = priority
    
    jfp's avatar
    jfp committed
            self.stdout_file = stdout_file.encode()
            self.stderr_file = stderr_file.encode()
            self.startsecs = startsecs
            self.startretries = startretries
            self.remain_startretries = startretries + 1
            self.stopwaitsecs = stopwaitsecs
            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
    
    jfp's avatar
    jfp committed
    
            self.process = ProcessInfo()
    
    
        def process_exists(self) -> bool:
    
    jfp's avatar
    jfp committed
            return self.process.pid in self.running_processes
    
        def process_is_backoff(self) -> bool:
            return self.process.state == ProcessStates.BACKOFF
    
        def process_is_fatal(self) -> bool:
            return self.process.state == ProcessStates.FATAL
    
    
        def process_is_stopped(self) -> bool:
            return self.process.state == ProcessStates.STOPPED
    
        def process_is_stopping(self) -> bool:
            return self.process.state == ProcessStates.STOPPING
    
    
    jfp's avatar
    jfp committed
        def set_running(self):
            self.process.state = ProcessStates.RUNNING
            self.remain_startretries = self.startretries + 1
    
    
        # def create_process(self, check_finalsts: bool = True):
        def create_process(self, from_terminated: bool):
    
    jfp's avatar
    jfp committed
            global supervisord_table_name
    
    
            if self.kill_request and from_terminated:
    
    jfp's avatar
    jfp committed
                return
    
    
            if from_terminated:
                # program has already been started
                if not self.autorestart and self.process.finalsts is not None:
                    return
    
                if (
                    self.autorestart is RestartWhenExitUnexpected
                    and self.process.finalsts in self.exitcodes
                ):
                    return
    
            if self.process_exists():
    
    jfp's avatar
    jfp committed
                return
    
    jfp's avatar
    jfp committed
            try:
                v = lib.get_logical(
                    self.process_name + b'_PID',
                    supervisord_table_name,
                )[1]
    
                pid = PidType(int(v, 16))
    
    jfp's avatar
    jfp committed
                while True:
                    try:
                        itime, stime = lib.getjpi(jpidef.JPI__LOGINTIM, pid)[2:]
                        break
                    except OSError as e:
                        if e.errno != ssdef.SS__SUSPENDED:
                            raise
                itime = crtl.fix_time(itime)
                assert itime is not None
    
                self.process.pid = PidType(pid)
    
    jfp's avatar
    jfp committed
                self.process.start_time = datetime.datetime.fromtimestamp(itime)
                self.set_running()
    
                Program.running_processes[pid] = self
                return
            except OSError as e:
                if e.errno not in (ssdef.SS__NOLOGNAM, ssdef.SS__NONEXPR):
                    raise
    
            if (
                self.process.state == ProcessStates.BACKOFF
                and self.timer_item
                and not self.timer_item.done
            ):
                return
    
            self.kill_request = False
    
            if self.startretries > 0:
                if self.remain_startretries == 0:
                    self.process.state = ProcessStates.FATAL
                    return
                self.remain_startretries -= 1
    
    
            if self.timer_item:
                self.timer_item.cancel = True
                self.timer_item = None
    
    jfp's avatar
    jfp committed
    
    
            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
    
    jfp's avatar
    jfp committed
            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]
    
            # 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.
    
            error = None
    
            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(
    
                        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:
    
                error = e
    
                self.process.state = ProcessStates.FATAL
                return
            finally:
                starlet.persona_assume(persona_previous_id)
                starlet.persona_delete(persona_id)
    
                if error is not None:
                    logger.warning(
                        f"Can' create process {self.process_name}, error {error}"
    
    jfp's avatar
    jfp committed
                    )
    
    jfp's avatar
    jfp committed
            lib.set_logical(
                self.process_name + b'_PID',
                hex(pid)[2:].upper(),
                supervisord_table_name,
            )
            lib.set_logical(
                self.process_name + b'_RUN',
                b'1',
                supervisord_table_name,
            )
    
    
            self.process.pid = PidType(pid)
    
    jfp's avatar
    jfp committed
            self.process.start_time = datetime.datetime.now()
            if self.startsecs == 0:
                self.set_running()
            else:
                self.process.state = ProcessStates.STARTING
                self.timer_item = TimerItem(TimerItemType.PROC_STARTING, self)
                timer_queue.put(
                    PrioritizedItem(current_tick + self.startsecs, self.timer_item)
                )
    
            Program.running_processes[PidType(pid)] = self
    
    jfp's avatar
    jfp committed
            lib.set_logical(
                self.process_name + b'_BEG',
                str(self.process.start_time)[:19],
                supervisord_table_name,
            )
            try:
                lib.delete_logical(
                    self.process_name + b'_END',
                    supervisord_table_name,
                )
            except OSError:
                pass
    
            logging.info(
                f'Process {self.process_name.decode()} created {hex(pid)[2:].upper()}'
            )
    
        def kill(self):
    
            if (
                self.process_is_stopped()
                or self.process_is_stopping()
                or self.process_is_fatal()
            ):
    
                return
    
            if self.process_is_backoff() or self.process_is_fatal():
                self.process.state = ProcessStates.STOPPED
                self.remain_startretries = self.startretries + 1
    
    jfp's avatar
    jfp committed
                if self.timer_item:
                    self.timer_item.cancel = True
                self.timer_item = None
                return
    
    
            if not self.process_exists() or self.kill_request:
    
    jfp's avatar
    jfp committed
                return
    
            self.kill_request = True
    
            pid = self.process.pid
            if self.process.state == ProcessStates.STOPPING:
                starlet.delprc(self.process.pid)
    
            else:
                starlet.forcex(pid, code=ssdef.SS__FORCEX)
    
    jfp's avatar
    jfp committed
            self.process.state = ProcessStates.STOPPING
    
                self.timer_item.cancel = True
    
            self.timer_item = TimerItem(TimerItemType.PROC_STOPPING, self)
            timer_queue.put(
                PrioritizedItem(current_tick + self.stopwaitsecs, self.timer_item)
            )
    
    jfp's avatar
    jfp committed
    
        def set_terminated(self, finalsts: int, end_time: datetime.datetime):
            global supervisord_table_name
            assert self.process is not None
            pid = self.process.pid
            finalsts = finalsts & 0xEFFFFFFF
            self.process.finalsts = finalsts
            self.process.end_time = end_time
            del Program.running_processes[pid]
            if self.timer_item:
                self.timer_item.cancel = True
                self.timer_item = None
            lib.set_logical(
                self.process_name + b'_RUN',
                b'0',
                supervisord_table_name,
            )
            lib.set_logical(
                self.process_name + b'_END',
                str(end_time)[:22],
                supervisord_table_name,
            )
            if self.process.state == ProcessStates.STARTING:
                self.process.state = ProcessStates.BACKOFF
                self.timer_item = TimerItem(TimerItemType.PROC_BACKOFF, self)
                timer_queue.put(
                    PrioritizedItem(
                        5
                        + current_tick
                        + self.startretries
                        - self.remain_startretries
                        + 1,
                        self.timer_item,
                    )
                )
            else:
                self.process.state = (
                    ProcessStates.STOPPED
                    if self.kill_request
                    else ProcessStates.EXITED
                )
    
    
    def logicals_init():
        global supervisord_table_name
    
    jfp's avatar
    jfp committed
        starlet.crelnt(
    
    jfp's avatar
    jfp committed
            attr=lnmdef.LNM_M_CREATE_IF,
            promsk=0xE000,
            tabnam=supervisord_table_name,
            partab=b'LNM$SYSTEM_DIRECTORY',
            acmode=psldef.PSL_C_SUPER,
        )
    
        lib.set_logical(
            b'LNM$PERMANENT_MAILBOX',
            supervisord_table_name,
            b'LNM$PROCESS_DIRECTORY',
        )
    
    jfp's avatar
    jfp committed
    
    
    
    def mbx_init() -> Tuple[int, int, int]:
    
    jfp's avatar
    jfp committed
        global logger
        logger.info('Creating mailbox')
        s, chan = starlet.crembx(
            prmflg=1,
            lognam='SUPERVISORD_MBX',
            maxmsg=accdef.ACC_K_TERMLEN,
            bufquo=1024 * 64,
            promsk=0x0000FF00,
            acmode=psldef.PSL_C_USER,
            flags=cmbdef.CMB_M_READONLY,
        )
        r = lib.getdvi(dvidef.DVI__UNIT, chan)
        mbxunt = r[1]
    
        s, chancmd = starlet.crembx(
            prmflg=1,
            lognam='SUPERVISORD_CMD',
            maxmsg=2048,
            bufquo=8192,
            promsk=0x0000FF00,
            acmode=psldef.PSL_C_USER,
            flags=cmbdef.CMB_M_READONLY,
        )
        logger.info(f'Mailbox created {mbxunt}')
    
        return chan, mbxunt, chancmd   # , chancmd_r
    
    jfp's avatar
    jfp committed
    
    
    class AstParam(object):
        ptype: AstParamType
        iosb: iosbdef.IOSB_r_io_64 | None
        buff: ctypes.Array[ctypes.c_char] | None
    
        def __init__(
            self,
            ptype: AstParamType,
            iosb: iosbdef.IOSB_r_io_64 | None = None,
            buff: ctypes.Array[ctypes.c_char] | None = None,
        ):
            self.ptype = ptype
            self.iosb = iosb
            self.buff = buff
    
    
    def qio_procterm(fterm: mbxqio.MBXQIO) -> vmsast.AstContext:
        astctxt = vmsast.AstContext(flags=vmsast.M_QUEUE | vmsast.M_WAKE)
        iosb, tbuff = fterm.read_nowait(accdef.ACC_K_TERMLEN, astctxt)
        astctxt.param = AstParam(AstParamType.PROCTERM, iosb, tbuff)
        return astctxt
    
    
    def qio_cmd(fcmd: mbxqio.MBXQIO) -> vmsast.AstContext:
        astctxt = vmsast.AstContext(flags=vmsast.M_QUEUE | vmsast.M_WAKE)
        iosb, cbuff = fcmd.read_nowait(256, astctxt)
        astctxt.param = AstParam(AstParamType.CMD, iosb, cbuff)
        return astctxt
    
    
    
    def send_cmd_reply(mbxreply_name: str, cmdreply: dict):
        with mbxqio.MBXQIO(mbxname=mbxreply_name) as fcmd_r:
            jscmd = json.dumps(cmdreply)
            try:
                fcmd_r.write(
                    jscmd.encode('ascii'),
                    iodef.IO_M_READERCHECK | iodef.IO_M_NOW,
                )
            except OSError:
                pass
    
    def dispatch_cmd(res: bytes):
    
    jfp's avatar
    jfp committed
        jscmd = json.loads(res)
    
        pwd = jscmd['pwd']
        jscmd['pwd'] = '*****'
    
        fcmd_r = jscmd['mbxreply']
    
        if pwd != supervisorctl_pwd:
            logging.info('Invalid password')
            send_cmd_reply(fcmd_r, {'error': 'Invalid password'})
    
            return
    
    jfp's avatar
    jfp committed
        match jscmd['cmd'].lower():
            case 'shutdown':
    
                send_cmd_reply(fcmd_r, {'result': None})
    
    jfp's avatar
    jfp committed
                exit(1)
            case 'start':
                pgmnames = set([name.upper() for name in jscmd['programs']])
                if 'ALL' in pgmnames:
                    for pgm in Program.programs.values():
                        if pgm.process.state == ProcessStates.FATAL:
                            pgm.process.state = ProcessStates.STOPPED
                            pgm.remain_startretries = pgm.startretries + 1
                        pgm.create_process(False)
                else:
                    for pgm in Program.programs.values():
                        if pgm.name in pgmnames:
                            if pgm.process.state == ProcessStates.FATAL:
                                pgm.process.state = ProcessStates.STOPPED
                                pgm.remain_startretries = pgm.startretries + 1
                            pgm.create_process(False)
    
    jfp's avatar
    jfp committed
                send_cmd_reply(fcmd_r, {'result': None})
    
    jfp's avatar
    jfp committed
            case 'stop':
                pgmnames = set([name.upper() for name in jscmd['programs']])
                if 'ALL' in pgmnames:
                    for pgm in Program.programs.values():
                        pgm.kill()
                else:
                    for pgm in Program.programs.values():
                        if pgm.name in pgmnames:
                            pgm.kill()
    
    jfp's avatar
    jfp committed
                send_cmd_reply(fcmd_r, {'result': None})
    
    jfp's avatar
    jfp committed
            case 'status':
                pgmnames = set([name.upper() for name in jscmd['programs']])
                lst = []
                for pgm in Program.programs.values():
                    if pgmnames and pgm.name not in pgmnames:
                        continue
                    lst.append(
                        [
                            pgm.process_name.decode('ascii'),
                            hex(pgm.process.pid)[2:].upper(),
                            str(pgm.process.start_time)[:19],
                            pgm.process.state,
                        ]
                    )
                    if pgm.process.state in (
                        ProcessStates.BACKOFF,
    
    jfp's avatar
    jfp committed
                        ProcessStates.EXITED,
                        ProcessStates.FATAL,
                    ):
                        lst[-1].append(str(pgm.process.end_time)[:19])
                        lst[-1].append(pgm.process.finalsts)
    
    
                send_cmd_reply(fcmd_r, {'result': lst})
    
    jfp's avatar
    jfp committed
    
    
    
    def dispatch_ast(astparam: AstParam):
    
    jfp's avatar
    jfp committed
        iosb: iosbdef.IOSB_r_io_64 | None
    
        iosb = astparam.iosb
        buff = astparam.buff
        match astparam.ptype:
            case AstParamType.PROCTERM:
                assert iosb is not None and buff is not None
                res = buff.raw[: iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_w_bcnt]
                pid = iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_l_dev_depend
                try:
                    pgm = Program.running_processes[pid]
    
    jfp's avatar
    jfp committed
                    logging.info(
                        f'Program {pgm.name} '
                        f'terminated {hex(pid)[2:].upper()}, '
    
    jfp's avatar
    jfp committed
                    )
                    pgm.set_terminated(acc.acc_l_finalsts, acc.acc_q_termtime)
    
                    # if not stsdef.vms_status_success(acc.acc_l_finalsts):
                    pgm.create_process(True)
    
    jfp's avatar
    jfp committed
                except KeyError:
                    pass
            case AstParamType.CMD:
                assert iosb is not None and buff is not None
                res = buff.raw[: iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_w_bcnt]
                pid = iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_l_dev_depend
    
                dispatch_cmd(res)
    
    jfp's avatar
    jfp committed
            case AstParamType.TIMER:
                global current_tick
                current_tick += 1
                while True:
                    itm: PrioritizedItem
                    try:
                        itm = timer_queue.get_nowait()
                    except queue.Empty:
                        return
                    if itm.item.cancel:
                        continue
                    if itm.priority > current_tick:
                        timer_queue.put(itm)
                        return
                    itm.item.done = True
    
                    val: Program = itm.item.value
                    match itm.item.typ:
                        case TimerItemType.PROC_STARTING:
                            val.set_running()
                        case TimerItemType.PROC_BACKOFF:
                            val.create_process(False)
                        case TimerItemType.PROC_STOPPING:
                            val.kill()
    
    jfp's avatar
    jfp committed
    
    
    
    def run(chan: int, chancmd: int):
    
    jfp's avatar
    jfp committed
        global timer_delay, timer_astctx
    
    jfp's avatar
    jfp committed
        astparam: AstParam
        with (
            mbxqio.MBXQIO(channel=chan) as fterm,
            mbxqio.MBXQIO(channel=chancmd) as fcmd,
        ):
            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
            ):
    
    jfp's avatar
    jfp committed
                if pgm.autostart:
    
    jfp's avatar
    jfp committed
    
            timer_astctx.param = AstParam(AstParamType.TIMER)
            starlet.setimr(daytim=timer_delay, reqidt=timer_astctx)
    
    
    jfp's avatar
    jfp committed
            timer_in_progress = True
    
    jfp's avatar
    jfp committed
            while True:
                ret_ast_context: vmsast.AstContext
    
                ret_ast_context = vmsast.get_completed(True)   # type:ignore
                astparam = ret_ast_context.param   # type: ignore
                iosb = astparam.iosb
                if iosb and iosb.iosb_w_status == ssdef.SS__ENDOFFILE:
                    if astparam.ptype == AstParamType.PROCTERM:
                        t_astctxt = qio_procterm(fterm)
                    elif astparam.ptype == AstParamType.CMD:
                        c_astctxt = qio_cmd(fcmd)
                    continue
                if iosb and not stsdef.vms_status_success(iosb.iosb_w_status):
                    raise IOError(iosb.iosb_w_status)
    
                dispatch_ast(astparam)
    
    jfp's avatar
    jfp committed
                if astparam.ptype == AstParamType.PROCTERM:
                    t_astctxt = qio_procterm(fterm)  # noqa: F841
                elif astparam.ptype == AstParamType.CMD:
                    c_astctxt = qio_cmd(fcmd)  # noqa: F841
                elif astparam.ptype == AstParamType.TIMER:
    
    jfp's avatar
    jfp committed
                    timer_in_progress = False
    
                if timer_queue.not_empty and not timer_in_progress:
                    timer_in_progress = True
                    start_timer()
    
    jfp's avatar
    jfp committed
    
    
    
    jfp's avatar
    jfp committed
    def is_deamon() -> bool:
        try:
            pid = lib.get_logical(
                b'SUPERVISORD' + b'_PID',
                supervisord_table_name,
            )[1]
            cpid = lib.getjpi(jpidef.JPI__PID)[3]
            return pid == cpid
        except OSError as e:
            if e.errno != ssdef.SS__NOLOGNAM:
                raise
            return False
    
    
    def daemon_is_running() -> bool:
        try:
            v = lib.get_logical(
                b'SUPERVISORD' + b'_PID',
                supervisord_table_name,
            )[1]
            pid = int(v, 16)
            lib.getjpi(jpidef.JPI__PID, pid)
            return True
        except OSError as e:
            if e.errno == ssdef.SS__SUSPENDED:
                return True
            if e.errno == ssdef.SS__NONEXPR:
                return False
            raise
    
    
    def daemonize(usrdaemon: str, comdaemon: str, stdout_file: str) -> int | None:
        userpro = starlet.create_user_profile(usrnam=usrdaemon)[1]
        persona_id = starlet.persona_create(usrpro=userpro)[1]
        persona_previous_id = starlet.persona_assume(persona_id)[1]
        error = None
        try:
            stsflg = (
                prcdef.PRC_M_IMPERSONATE
                | prcdef.PRC_M_PARSE_EXTENDED
                | prcdef.PRC_M_KT_LIMIT
                | prcdef.PRC_M_HIBER
            )
    
            pid = PidType(
                starlet.creprc(
                    image=b'SYS$SYSTEM:LOGINOUT.EXE',
                    input=comdaemon.encode(),
                    output=stdout_file.encode(),
                    prcnam=b'SUPERVISORD',
                    kt_limit=1,
                    baspri=4,
                    stsflg=stsflg,
                )[1]
            )
            lib.set_logical(
                b'SUPERVISORD' + b'_PID',
                hex(pid)[2:].upper(),
                supervisord_table_name,
            )
            starlet.wake(pid)
            return pid
        except OSError as e:
            error = e
            return None
        finally:
            starlet.persona_assume(persona_previous_id)
            starlet.persona_delete(persona_id)
            if error is not None:
                print(f"Can' create process SUPERVISORD, error {error}")
    
    
    
    jfp's avatar
    jfp committed
    def main():
    
        global logger, supervisorctl_pwd
    
    jfp's avatar
    jfp committed
    
    
        # Required privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD
    
    jfp's avatar
    jfp committed
        starlet.setprv(
            1,
            prvdef.PRV_M_SYSNAM
    
            | prvdef.PRV_M_ALTPRI
    
    jfp's avatar
    jfp committed
            | prvdef.PRV_M_PRMMBX
    
            | prvdef.PRV_M_SETPRV
    
    jfp's avatar
    jfp committed
            | prvdef.PRV_M_IMPERSONATE
            | prvdef.PRV_M_WORLD,
        )
    
    
        parser = argparse.ArgumentParser(description='supervisord')
        parser.add_argument(
            '-c',
            '--configuration',
            type=argparse.FileType('r', encoding='latin1'),
            default='./supervisord.conf',
            help='Configuration file path (default ./supervisord.conf)',
        )
    
        parser.add_argument(
            '-p',
            '--password',
            required=False,
            type=str,
            default='',
    
            help='Password for supervisord',
        )
        parser.add_argument(
            '-n',
            '--nodaemon',
            action='store_true',
            help='Run supervisord in the foreground',
    
    jfp's avatar
    jfp committed
        config = configparser.ConfigParser()
    
        config.read_file(args.configuration)
        args.configuration.close()
    
    
        nodaemon = (
            config['supervisord'].getboolean('nodaemon', False) or args.nodaemon
        )
    
    jfp's avatar
    jfp committed
        if not nodaemon and not is_deamon():
            usrdaemon = config['supervisord'].get('user', 'system')
            stdout_file = config['supervisord'].get('stdout_file', 'NLA0:')
            comdaemon = config['supervisord']['command']
            pid = daemonize(usrdaemon, comdaemon, stdout_file)
            if pid is None:
                print('Unable to start supervisord daemon')
            else:
                print(f'Daemon started {hex(pid)[2:].upper()}')
            exit(0)
    
        logfile = config['supervisord'].get('logfile', 'supervisord.log')
    
        logging_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        logging.basicConfig(
            filename='/dev/null', level=logging.INFO, format=logging_format
        )
        formatter = logging.Formatter(logging_format)
        main_log = logging.getLogger()  # root handler
        main_log.setLevel(logging.INFO)
        logfn = crtl.from_vms(logfile)
        if logfn is None:
            print(f'{repr(logfile)} is invalid')
            crtl.vms_exit(ssdef.SS__INVARG)
        handler = logging.handlers.TimedRotatingFileHandler(
            logfn, 'midnight', backupCount=10
        )
        handler.setFormatter(formatter)
        main_log.addHandler(handler)
    
    
    jfp's avatar
    jfp committed
        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':
                        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: