#!/usr/bin/env python3 """One-action private process supervisor; no credentials, network or runtime policy.""" import argparse import base64 import ctypes import json import os from pathlib import Path import select import signal import stat import subprocess import sys import time import uuid MAX_OUTPUT=65536 ENV={'HOME':'/home/tegwick','PATH':'/usr/bin:/bin','LANG':'C.UTF-8','PYTHONDONTWRITEBYTECODE':'1'} def facts(pid): try: path=Path('/proc')/str(pid);raw=(path/'stat').read_text();v=raw[raw.rfind(')')+2:].split() return {'pid':pid,'ppid':int(v[1]),'pgrp':int(v[2]),'sid':int(v[3]),'start':int(v[19]),'uid':path.stat().st_uid,'state':v[0]} except FileNotFoundError:return None def write(path,value): temp=path.with_name('.'+path.name+'.'+uuid.uuid4().hex) fd=os.open(temp,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) with os.fdopen(fd,'w') as f:json.dump(value,f);f.flush();os.fsync(f.fileno()) temp.replace(path) def dead(fd): p=select.poll();p.register(fd,select.POLLIN);return bool(p.poll(0)) def descendants(parent): rows={} for p in Path('/proc').iterdir(): if p.name.isdigit(): row=facts(int(p.name)) if row:rows[row['pid']]=row selected={parent};changed=True while changed: before=len(selected);selected.update(r['pid'] for r in rows.values() if r['ppid'] in selected) changed=len(selected)!=before return {p:rows[p] for p in selected-{parent} if p in rows} def quiesce(parent,children): """Signal only pidfd-bound actual descendants; subreaper adopts escaped sessions.""" uncertain=False for sig,limit in ((signal.SIGTERM,0.5),(signal.SIGKILL,3.0)): end=time.monotonic()+limit while time.monotonic()MAX_OUTPUT:abort=abort or 'output_bound' except BlockingIOError:pass if dead(job['owner_fd']):abort=abort or 'caller_lost' if time.monotonic()>=job['deadline']:abort=abort or 'timeout' if p.poll() is not None and job['eof'] and not abort: # A returned leader is not evidence that its children exited. own=descendants(os.getpid()) leaders={x['p'].pid for x in jobs.values() if not x['done']} if any(row['sid']==p.pid or (row['ppid']==os.getpid() and row['pid'] not in leaders) for row in own.values()):abort='leader_left_descendants' else: write(directory/('result-'+key+'.json'),{'returncode':p.returncode,'quiescent':True,'status':'completed', 'output_b64':base64.b64encode(job['output']).decode() if job['request']['capture'] else ''}) job['done']=True;os.close(job['owner_fd']) if abort: ok=quiesce(os.getpid(),[x['p'] for x in jobs.values()]) and not uncertain for key,job in jobs.items(): if not job['done']: write(directory/('result-'+key+'.json'),{'returncode':124 if abort=='timeout' else 125, 'quiescent':ok,'status':abort,'output_b64':''});job['done']=True write(directory/'quiescent.json',{'quiescent':ok,'reason':abort,'no_live_or_waitable_descendants':ok, 'stderr_suppressed':True,'process_values_emitted':False}) return 0 if ok else 1 time.sleep(0.01) except BaseException: ok=quiesce(os.getpid(),[x['p'] for x in jobs.values()]) write(directory/'quiescent.json',{'quiescent':False,'reason':'supervisor_exception','best_effort_stopped':ok}) return 1 class Supervisor: def __init__(self,directory): self.directory=Path(directory);self.directory.mkdir(mode=0o700) write(self.directory/'root.json',facts(os.getpid())) fd=os.pidfd_open(os.getpid()) try:self.process=subprocess.Popen([sys.executable,'-I','-B',str(Path(__file__).resolve()),'--watchdog',str(self.directory),'--parent-fd',str(fd)], stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,pass_fds=(fd,),start_new_session=True,env=ENV) finally:os.close(fd) end=time.monotonic()+5 while not (self.directory/'ready.json').exists(): if self.process.poll() is not None or time.monotonic()>end:raise RuntimeError('supervisor_start_failed') time.sleep(0.01) def run(self,argv,timeout,*,capture=False):return request(self.directory,argv,timeout,capture=capture) def close(self): write(self.directory/'shutdown',{}) try:self.process.wait(timeout=8) except subprocess.TimeoutExpired:raise RuntimeError('supervisor_quiescence_unknown') from None report=json.loads((self.directory/'quiescent.json').read_text()) if self.process.returncode or report.get('quiescent') is not True:raise RuntimeError('descendant_quiescence_unverified') return report def request(directory,argv,timeout,*,capture=False): directory=Path(directory);key=uuid.uuid4().hex write(directory/('request-'+key+'.json'),{'owner':facts(os.getpid()),'argv':list(argv),'timeout':timeout,'capture':capture}) end=time.monotonic()+timeout+10 result=directory/('result-'+key+'.json') while not result.exists(): if time.monotonic()>end:raise RuntimeError('supervision_reply_unknown') time.sleep(0.01) report=json.loads(result.read_text()) if report.get('quiescent') is not True:raise RuntimeError('descendant_quiescence_unverified') return report['returncode'],base64.b64decode(report.get('output_b64',''),validate=True) def main(): p=argparse.ArgumentParser();p.add_argument('--watchdog');p.add_argument('--parent-fd',type=int);a=p.parse_args() return watchdog(a.watchdog,a.parent_fd) if __name__=='__main__': try:raise SystemExit(main()) except Exception:raise SystemExit(1) from None