"""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)