# # Required privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD # import argparse 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 from ovms import ( accdef, cmbdef, crtl, dvidef, iodef, iosbdef, jpidef, lnmdef, mbxqio, pqldef, prcdef, 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() supervisorctl_pwd = '' TRUTHY_STRINGS = ('yes', 'true', 'on', '1') FALSY_STRINGS = ('no', 'false', 'off', '0') 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, } 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 class TimerItemType(IntEnum): UNDEF = 0 PROC_STARTING = 1 PROC_BACKOFF = 2 PROC_STOPPING = 3 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): pid: PidType state: ProcessStates finalsts: int | None start_time: datetime.datetime | None end_time: datetime.datetime | None def __init__( self, pid: PidType = PidType(0), 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 image: bytes command: bytes program_name: bytes autostart: bool priority: int stdout_logfile: bytes stderr_logfile: bytes startsecs: int startretries: int stopwaitsecs: int kt_limit: int baspri: int nouaf: bool quotas: List[Tuple[int, int]] | None prv: PrvMask | None user: bytes process: ProcessInfo running_processes: Dict[PidType, 'Program'] 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] def __init__( self, name: str, user: str, command: str, image: str, process_name: str, autostart: bool, priority: int, 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, baspri: int, autorestart: ( Type[RestartUnconditionally] | Type[RestartWhenExitUnexpected] | Literal[False] ), exitcodes: List[int], ): self.name = name self.user = user.encode() self.image = image.encode() self.command = command.encode() self.process_name = process_name.encode() self.autostart = autostart self.priority = priority 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 self.baspri = baspri self.exitcodes = exitcodes self.process = ProcessInfo() def process_exists(self) -> bool: 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 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): global supervisord_table_name if self.kill_request and from_terminated: 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(): return try: v = lib.get_logical( self.process_name + b'_PID', supervisord_table_name, )[1] pid = PidType(int(v, 16)) 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) 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 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] # 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( image=self.image, 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, baspri=self.baspri, # 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}" ) 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) 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 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(): return if self.process_is_backoff() or self.process_is_fatal(): self.process.state = ProcessStates.STOPPED self.remain_startretries = self.startretries + 1 if self.timer_item: self.timer_item.cancel = True self.timer_item = None return if not self.process_exists() or self.kill_request: 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) self.process.state = ProcessStates.STOPPING if self.timer_item: self.timer_item.cancel self.timer_item = TimerItem(TimerItemType.PROC_STOPPING, self) timer_queue.put( PrioritizedItem(current_tick + self.stopwaitsecs, self.timer_item) ) 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 starlet.crelnt( 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', ) def mbx_init() -> Tuple[int, int, int]: 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 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): global supervisorctl_pwd 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 match jscmd['cmd'].lower(): case 'shutdown': send_cmd_reply(fcmd_r, {'result': None}) 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) send_cmd_reply(fcmd_r, {'result': None}) 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() send_cmd_reply(fcmd_r, {'result': None}) 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, ProcessStates.STOPPED, 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}) def dispatch_ast(astparam: AstParam): 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] acc = accdef.ACCDEF.parse(res) print(acc) logging.info( f'Program {pgm.name} ' f'terminated {hex(pid)[2:].upper()}, ' f'{acc.acc_l_finalsts}' ) 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) 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) 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() def run(chan: int, chancmd: int): global timer_delay, timer_astctx 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 ): if pgm.autostart: pgm.create_process(False) timer_astctx.param = AstParam(AstParamType.TIMER) starlet.setimr(daytim=timer_delay, reqidt=timer_astctx) timer_in_progress = True 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) 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: timer_in_progress = False if timer_queue.not_empty and not timer_in_progress: timer_in_progress = True start_timer() 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}") def main(): global logger, supervisorctl_pwd # 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, ) 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', ) args = parser.parse_args() supervisorctl_pwd = args.password config = configparser.ConfigParser() config.read_file(args.configuration) args.configuration.close() nodaemon = config['supervisord'].getboolean('nodaemon', False) or args.nodaemon 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) 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: 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].get('process_name', name) autostart = config[sn].getboolean('autostart', True) command = config[sn].get('command', 'NLA0:') image = config[sn].get('image', 'SYS$SYSTEM:LOGINOUT.EXE') stdout_file = config[sn].get('stdout_file', 'NLA0:') stderr_file = config[sn].get('stderr_file', 'NLA0:') priority = config[sn].getint('priority', 999) baspri = config[sn].getint('baspri', 4) 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) autorestart = auto_restart( config[sn].get('autorestart', 'unexpected') ) 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, command=command, image=image, process_name=process_name, autostart=autostart, priority=priority, stdout_file=stdout_file, stderr_file=stderr_file, startsecs=startsecs, startretries=startretries, stopwaitsecs=stopwaitsecs, autorestart=autorestart, nouaf=nouaf, quotas=prcquotas, prv=prv, baspri=baspri, kt_limit=kt_limit, exitcodes=[ int(exitcode) for exitcode in exitcodes if exitcode != '' ], ) Program.programs[name] = p logicals_init() chan, Program.mbxunt, chancmd = mbx_init() run(chan, chancmd) if __name__ == '__main__': main()