Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/test_native_watch_cleanup.py
T
yilsem b4e8664115
Server CI and Windows package / verify (push) Successful in 1m44s
Server CI and Windows package / windows-package (push) Successful in 9m44s
Normalize native launcher source and test file endings
2026-10-12 19:28:37 +03:00

116 lines
5.8 KiB
Python

"""Windows owned-process lifetime tests; no installed process is terminated."""
import ctypes
from ctypes import wintypes as w
import hashlib
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import pytest
from prepare_native_watch import prepare
pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason='Windows executable lifecycle')
def descendants(parent):
class Entry(ctypes.Structure):
_fields_=[('size',w.DWORD),('usage',w.DWORD),('pid',w.DWORD),('heap',ctypes.c_size_t),('module',w.DWORD),('threads',w.DWORD),('parent',w.DWORD),('priority',w.LONG),('flags',w.DWORD),('exe',w.WCHAR*260)]
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Process32FirstW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.Process32NextW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.CloseHandle.argtypes=[w.HANDLE]
snap=k.CreateToolhelp32Snapshot(2,0);entry=Entry();entry.size=ctypes.sizeof(entry);rows=[]
try:
more=k.Process32FirstW(snap,ctypes.byref(entry))
while more:
rows.append((entry.pid,entry.parent));more=k.Process32NextW(snap,ctypes.byref(entry))
finally:k.CloseHandle(snap)
owned={parent}
while True:
children={pid for pid,ppid in rows if ppid in owned}
if children <= owned: return owned-{parent}
owned |= children
def assert_abrupt_cleanup(p, expected_min):
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.WaitForSingleObject.argtypes=[w.HANDLE,w.DWORD]
k.CloseHandle.argtypes=[w.HANDLE]
ids=descendants(p.pid)
assert len(ids)>=expected_min, ids
handles=[k.OpenProcess(0x100000,False,pid) for pid in ids]
assert all(handles)
try:
p.kill();p.wait(timeout=5)
assert all(k.WaitForSingleObject(h,5000)==0 for h in handles), ids
finally:
for h in handles:k.CloseHandle(h)
return len(ids)
def line(p):
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
return q.get(timeout=20)
def candidate(tmp_path, original, digest):
return prepare(Path(__file__).parent,tmp_path/'candidate',original,digest,Path(sys.executable),tmp_path/'bridge',tmp_path/'agent')
def test_abrupt_bootstrap_exit_kills_owned_python_and_grandchild(tmp_path):
exe=Path(sys.executable)
launcher=candidate(tmp_path,exe,hashlib.sha256(exe.read_bytes()).hexdigest())
script='import subprocess,sys,time; p=subprocess.Popen([sys.executable,"-c","import time; time.sleep(120)"]); print(p.pid,flush=True); time.sleep(120)'
p=subprocess.Popen([str(launcher),'-u','-c',script],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
assert int(line(p))>0
assert_abrupt_cleanup(p,3)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_pinned_original_initialize_and_abrupt_cleanup(tmp_path):
# Explicit provisioned evidence, never discovers or kills an existing app.
manifest_path=os.environ.get('NATIVE_WATCH_TEST_MANIFEST')
if not manifest_path: pytest.skip('requires explicit pinned original candidate')
manifest=json.loads(Path(manifest_path).read_text(encoding='utf-8-sig'))
launcher=candidate(tmp_path,Path(manifest['original']),manifest['original_sha256'])
p=subprocess.Popen([str(launcher),'-c','model_provider="openai"','app-server','--analytics-default-enabled'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
p.stdin.write(json.dumps({'id':1,'method':'initialize','params':{'clientInfo':{'name':'owned-cleanup-proof','version':'1.0'},'capabilities':{'experimentalApi':True}}}).encode()+b'\n');p.stdin.flush()
response=json.loads(line(p));assert response.get('id')==1 and 'result' in response,response
assert_abrupt_cleanup(p,2)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_compiled_launcher_fallback(tmp_path):
source=Path(__file__).parent
import hashlib
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(source,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
args=['-c','import json,sys,os; print(json.dumps([sys.argv[1:],os.getcwd()])); sys.stderr.write("stderr\\n"); sys.exit(23)','space arg','quote"arg','trailing\\']
direct=subprocess.run([str(exe),*args],cwd=tmp_path,capture_output=True)
wrapped=subprocess.run([str(launcher),*args],cwd=tmp_path,capture_output=True)
assert (wrapped.returncode,wrapped.stdout,wrapped.stderr)==(direct.returncode,direct.stdout,direct.stderr)
manifest=launcher.with_suffix('.json');data=json.loads(manifest.read_text(encoding='utf-8'));data['original_sha256']='0'*64;manifest.write_text(json.dumps(data),encoding='utf-8')
assert subprocess.run([str(launcher),'--version'],capture_output=True).returncode==78
def test_executable_flushes_interactive_stdin_before_eof(tmp_path):
import hashlib,queue
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(Path(__file__).parent,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
p=subprocess.Popen([str(launcher),'-u','-c','import sys; print(sys.stdin.readline().strip(),flush=True); sys.stdin.read()'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
try:
p.stdin.write(b'live-before-eof\n');p.stdin.flush()
assert q.get(timeout=5).strip()==b'live-before-eof'
finally:
p.stdin.close();p.wait(timeout=5)