"""Capture the complete official CLI HTTP server; never call a model/library inference entry point."""
from pathlib import Path
import ctypes, ctypes.wintypes as wt, datetime, hashlib, json, os, socket, subprocess, sys, threading, time
import urllib.error, urllib.parse, urllib.request, importlib.util
ROOT=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('task_runtime_inspector',ROOT/'inspect-runtime.py')
inspector=importlib.util.module_from_spec(spec);spec.loader.exec_module(inspector)
task_env=inspector.task_env
LAB=Path('D:/uAgentKit-lab/rembg-native-2026-10-02')
def now(): return datetime.datetime.now(datetime.timezone.utc).isoformat()
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest()
def save(p,v): p.write_text(json.dumps(v,indent=2)+'\n',encoding='utf-8')
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
kernel=ctypes.WinDLL('kernel32',use_last_error=True); psapi=ctypes.WinDLL('psapi',use_last_error=True)
class ProcessEntry(ctypes.Structure):
    _fields_=[('dwSize',wt.DWORD),('cntUsage',wt.DWORD),('th32ProcessID',wt.DWORD),('th32DefaultHeapID',ctypes.c_size_t),('th32ModuleID',wt.DWORD),('cntThreads',wt.DWORD),('th32ParentProcessID',wt.DWORD),('pcPriClassBase',wt.LONG),('dwFlags',wt.DWORD),('szExeFile',wt.WCHAR*260)]
class Memory(ctypes.Structure):
    _fields_=[('cb',wt.DWORD),('PageFaultCount',wt.DWORD)]+[(k,ctypes.c_size_t) for k in ('PeakWorkingSetSize','WorkingSetSize','QuotaPeakPagedPoolUsage','QuotaPagedPoolUsage','QuotaPeakNonPagedPoolUsage','QuotaNonPagedPoolUsage','PagefileUsage','PeakPagefileUsage','PrivateUsage')]
kernel.CreateToolhelp32Snapshot.argtypes=[wt.DWORD,wt.DWORD];kernel.CreateToolhelp32Snapshot.restype=wt.HANDLE
kernel.Process32FirstW.argtypes=[wt.HANDLE,ctypes.POINTER(ProcessEntry)];kernel.Process32NextW.argtypes=kernel.Process32FirstW.argtypes
kernel.CloseHandle.argtypes=[wt.HANDLE]
kernel.OpenProcess.argtypes=[wt.DWORD,wt.BOOL,wt.DWORD];kernel.OpenProcess.restype=wt.HANDLE
kernel.TerminateProcess.argtypes=[wt.HANDLE,wt.UINT]
psapi.GetProcessMemoryInfo.argtypes=[wt.HANDLE,ctypes.POINTER(Memory),wt.DWORD]
def processes():
    h=kernel.CreateToolhelp32Snapshot(2,0)
    if h==wt.HANDLE(-1).value: return []
    e=ProcessEntry();e.dwSize=ctypes.sizeof(e); result=[]
    try:
        ok=kernel.Process32FirstW(h,ctypes.byref(e))
        while ok:
            result.append({'pid':int(e.th32ProcessID),'parentPid':int(e.th32ParentProcessID),'executable':e.szExeFile,'threads':int(e.cntThreads)})
            ok=kernel.Process32NextW(h,ctypes.byref(e))
    finally:kernel.CloseHandle(h)
    return result
def owned_tree(root):
    allp=processes(); owned={root};changed=True
    while changed:
        changed=False
        for p in allp:
            if p['parentPid'] in owned and p['pid'] not in owned:owned.add(p['pid']);changed=True
    return [p for p in allp if p['pid'] in owned]
def memory(pid):
    h=kernel.OpenProcess(0x0400|0x0010,False,pid)
    if not h:return None
    try:
        m=Memory();m.cb=ctypes.sizeof(m)
        if not psapi.GetProcessMemoryInfo(h,ctypes.byref(m),m.cb):return None
        return {'workingSetBytes':int(m.WorkingSetSize),'peakWorkingSetBytes':int(m.PeakWorkingSetSize),'privateBytes':int(m.PrivateUsage)}
    finally:kernel.CloseHandle(h)

assert not (ROOT/'run-receipt.json').exists(), 'This runner permits one captured run only; do not overwrite evidence'
frozen=json.loads((ROOT/'frozen-cases.json').read_text(encoding='utf-8'))
assert sha(ROOT/'frozen-cases.json')=='5569d699440ca57decb275abfe9353842bc564947b5bb153359ccd8c3ca63522'
assert json.loads((ROOT/'preparation'/'runtime-preflight.json').read_text())['modelRootEmptyBeforeInference']
assert not list((LAB/'model-state').rglob('*.onnx'))
for a in frozen['assets']:assert sha(ROOT/a['path'])==a['sha256']
env=task_env(); env.pop('MODEL_CHECKSUM_DISABLED',None)
diagnostic=subprocess.run([sys.executable,'-c','import json,onnxruntime as o; print(json.dumps({"version":o.__version__,"device":o.get_device(),"availableProviders":o.get_available_providers()}))'],env=env,stdout=subprocess.PIPE,stderr=subprocess.PIPE,creationflags=subprocess.CREATE_NO_WINDOW)
assert diagnostic.returncode==0
provider=json.loads(diagnostic.stdout); assert provider['device']=='CPU' and 'CPUExecutionProvider' in provider['availableProviders']
provider.update({'sourceSelection':'Exact installed BaseSession selects CPUExecutionProvider for observed CPU device with no CUDA/ROCM/OpenVINO path; no providers override in frozen extras.','selectedProviderDerivedFromExactNativeSource':'CPUExecutionProvider','actualInnerSessionGetProvidersObserved':False,'limitation':'Native HTTP endpoint does not expose the inner ORT session; no model object is created separately for diagnostics.'})
save(ROOT/'preparation'/'cpu-provider-diagnostic.json',provider)
with socket.socket() as sock:sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
cmd=[str(LAB/'venv'/'Scripts'/'rembg.exe'),'s','--host','127.0.0.1','--port',str(port),'--log_level','info','--threads','1']
request_specs=[]
for short,prefix,case in [('primary','bottle',frozen['cases'][0]),('boundary','mug',frozen['cases'][1])]:
    out=ROOT/short;out.mkdir(exist_ok=True)
    boundary='uagentkit-rembg-native-frozen-'+short
    body=bytearray()
    for name,value in frozen['nativeConfiguration']['form'].items():
        body.extend((f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n').encode())
    filename=prefix+'-input.png';inp=(ROOT/'fixtures'/filename).read_bytes()
    body.extend((f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\nContent-Type: image/png\r\n\r\n').encode());body.extend(inp);body.extend((f'\r\n--{boundary}--\r\n').encode())
    path=out/'request-multipart.bin';path.write_bytes(body)
    url=f'http://127.0.0.1:{port}/api/remove?'+urllib.parse.urlencode(frozen['nativeConfiguration']['query'])
    headers={'Content-Type':'multipart/form-data; boundary='+boundary,'Content-Length':str(len(body)),'User-Agent':'uAgentKit-frozen-native-test/1','Connection':'close'}
    metadata={'caseId':case['id'],'method':'POST','url':url,'form':frozen['nativeConfiguration']['form'],'query':frozen['nativeConfiguration']['query'],'headers':headers,'uploadedFile':{'field':'file','name':filename,'bytes':len(inp),'sha256':hashlib.sha256(inp).hexdigest()},'requestBodyBytes':len(body),'requestBodySha256':sha(path),'referenceMasksUploaded':False,'originalDefinition':case}
    save(out/'request-metadata.json',metadata);request_specs.append((short,bytes(body),url,headers))
started=now(); startup_clock=time.perf_counter();samples=[];stop=threading.Event();phase={'name':'startup'};proc=None;attempts=[];cleanup=[]
save(ROOT/'run-started.json',{'startedAt':started,'command':cmd,'freezeSha256':sha(ROOT/'frozen-cases.json'),'requestsPreparedBeforeInference':2,'modelRootInitiallyEmpty':True,'processLocalEnvironment':{k:env[k] for k in ('REMBG_HOME','U2NET_HOME','TEMP','TMP','GRADIO_TEMP_DIR','HF_HOME','HF_HUB_CACHE','XDG_CACHE_HOME','XDG_DATA_HOME','USERPROFILE','HOME','OMP_NUM_THREADS','OPENBLAS_NUM_THREADS','MKL_NUM_THREADS','NUMBA_NUM_THREADS','BROWSER')},'MODEL_CHECKSUM_DISABLED':'unset','cpuProviderDiagnosticSha256':sha(ROOT/'preparation'/'cpu-provider-diagnostic.json')})
raw=ROOT/'native-console.private.log'
console=raw.open('wb')
try:
    proc=subprocess.Popen(cmd,cwd=LAB,env=env,stdin=subprocess.DEVNULL,stdout=console,stderr=subprocess.STDOUT,creationflags=subprocess.CREATE_NO_WINDOW)
    def monitor():
        while not stop.is_set():
            ps=owned_tree(proc.pid);row={'at':now(),'elapsedSeconds':time.perf_counter()-startup_clock,'phase':phase['name'],'processes':[]}
            for p in ps:
                m=memory(p['pid']);row['processes'].append({**p,'memory':m})
            samples.append(row);stop.wait(.5)
    thread=threading.Thread(target=monitor,daemon=True);thread.start()
    ready=None;readiness_checks=0;startup_errors=[]
    while time.perf_counter()-startup_clock<360:
        if proc.poll() is not None:raise RuntimeError('Native startup exited '+str(proc.returncode))
        try:
            with opener.open(f'http://127.0.0.1:{port}/openapi.json',timeout=2) as response:
                content=response.read();ready={'status':response.status,'headers':dict(response.headers),'elapsedMs':round((time.perf_counter()-startup_clock)*1000),'checks':readiness_checks+1}
                (ROOT/'preparation'/'native-openapi.json').write_bytes(content);break
        except (urllib.error.URLError,TimeoutError) as exc:
            readiness_checks+=1;startup_errors.append(type(exc).__name__);time.sleep(.5)
    assert ready,'Native startup did not become ready within 360 seconds'
    schema=json.loads(content);assert schema['info']['version']=='2.0.85' and 'post' in schema['paths']['/api/remove']
    ready.update(nativeRootPid=proc.pid,ownedProcessTree=owned_tree(proc.pid),openapiSha256=sha(ROOT/'preparation'/'native-openapi.json'),startupFailureTypes=sorted(set(startup_errors)))
    save(ROOT/'preparation'/'native-startup-receipt.json',ready)
    print('Official native server ready on loopback; frozen primary request next.',flush=True)
    for short,body,url,headers in request_specs:
        assert not (ROOT/short/'http-receipt.json').exists()
        phase['name']=short;begin=now();clock=time.perf_counter();status=None;response_headers={};error=None
        req=urllib.request.Request(url,data=body,headers=headers,method='POST')
        try:
            with opener.open(req,timeout=300) as response:
                result=response.read();status=response.status;response_headers=dict(response.headers.items())
        except urllib.error.HTTPError as exc:
            result=exc.read();status=exc.code;response_headers=dict(exc.headers.items());error=type(exc).__name__+': '+str(exc)
        except Exception as exc:
            result=b'';error=type(exc).__name__+': '+str(exc)
        elapsed=round((time.perf_counter()-clock)*1000)
        response_path=ROOT/short/'response.bin';response_path.write_bytes(result)
        if result.startswith(b'\x89PNG\r\n\x1a\n'):(ROOT/short/'response.png').write_bytes(result)
        receipt={'caseId':'rembg-'+short,'startedAt':begin,'completedAt':now(),'durationMs':elapsed,'method':'POST','url':url,'requestCountForOriginalCase':1,'status':status,'responseHeaders':response_headers,'responseBytes':len(result),'responseSha256':sha(response_path),'error':error,'nativeProcessStillRunningAtResponse':proc.poll() is None}
        save(ROOT/short/'http-receipt.json',receipt);attempts.append(receipt)
        print(short,'HTTP',status,'bytes',len(result),'ms',elapsed,flush=True)
    phase['name']='post-inference'
    model=LAB/'model-state'/'models'/'u2netp'/'u2netp.onnx'
    files=list((LAB/'model-state').rglob('*'))
    model_receipt={'expectedOfficialAssetBytes':4574861,'nativeExpectedMd5':'8e83ca70e441ab06c318d82300c84806','initialModelRootEmpty':True,'MODEL_CHECKSUM_DISABLED':'unset','modelFiles':[{'path':str(p.relative_to(LAB/'model-state')),'bytes':p.stat().st_size} for p in files if p.is_file()],'nativeDownloaderSourceByteMatch':True,'officialAssetUrl':'https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2netp.onnx'}
    if model.exists():
        model_receipt.update(path=str(model),bytes=model.stat().st_size,md5=hashlib.md5(model.read_bytes()).hexdigest(),sha256=sha(model))
        assert model_receipt['bytes']==4574861 and model_receipt['md5']==model_receipt['nativeExpectedMd5']
        assert len([p for p in files if p.is_file()])==1
    save(ROOT/'model-receipt.json',model_receipt)
except Exception as exc:
    save(ROOT/'run-blocker.json',{'at':now(),'type':type(exc).__name__,'message':str(exc),'requestsSent':len(attempts),'nativeExitCode':None if proc is None else proc.poll()})
    print('Run blocker:',type(exc).__name__,str(exc),flush=True)
finally:
    phase['name']='cleanup'; stop.set()
    if proc is not None:
        if 'thread' in globals():thread.join(2)
        descendants=owned_tree(proc.pid)
        for p in reversed(descendants):
            if p['pid']==proc.pid:continue
            h=kernel.OpenProcess(0x0001,False,p['pid'])
            if h:
                success=bool(kernel.TerminateProcess(h,0));kernel.CloseHandle(h);cleanup.append({**p,'terminatedOnlyOwnedDescendant':success})
        try:proc.wait(timeout=5)
        except subprocess.TimeoutExpired:proc.terminate();proc.wait(timeout=5);cleanup.append({'pid':proc.pid,'terminatedOnlyOwnedRoot':True})
    console.close()
    save(ROOT/'process-resource-samples.json',{'method':'Windows Toolhelp32 owned descendant tree plus GetProcessMemoryInfo at about 0.5s intervals; non-task processes are never selected or terminated.','samples':samples,'limitations':['Sampled working set/private bytes are not full CPU/memory cost accounting. Transient allocation peaks can exceed interval samples; each native process peakWorkingSetBytes is separately recorded.','The task launcher and actual Python descendant are listed separately. No OS filesystem or whole-host network isolation audit is implied.']})
    save(ROOT/'run-receipt.json',{'startedAt':started,'completedAt':now(),'command':cmd,'productInferenceHttpRequestsSent':len(attempts),'cases':[{'caseId':a['caseId'],'status':a['status'],'durationMs':a['durationMs']} for a in attempts],'nativeRootPid':None if proc is None else proc.pid,'nativeExitCodeAfterTaskStop':None if proc is None else proc.returncode,'cleanup':cleanup,'remainingOwnedProcessTree':[] if proc is None else owned_tree(proc.pid),'rawConsolePrivatePath':raw.name,'freezeSha256After':sha(ROOT/'frozen-cases.json'),'fixtureHashesAfter':[{'path':a['path'],'sha256':sha(ROOT/a['path']),'originalSha256':a['sha256']} for a in frozen['assets']]})
