Something went wrong. Try again.
Native control, inspection, UI automation and evidence capture for a running Witcher 3 process.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522"""Persistent local debugger broker and small reconnectable Python client.
The broker owns .lock for its lifetime. Socket threads never contact the game;exactly one worker does. Cancellation is an Event consumed by that worker."""import fcntlimport hashlibimport jsonimport mathimport osfrom pathlib import Pathimport reimport secretsimport signalimport socketimport socketserverimport subprocessimport sysimport tempfileimport threadingimport time
from . import nativefrom .debug_journal import Journal, durable_json, encode, recover_torn_tailfrom .debug_runtime import Runtime, validate
MAX_MESSAGE=4*1024*1024
def descriptor_path(): return native.STATE_ROOT/'debug-owner.json'
def source_identity(root=None): root=Path(root or native.ROOT).resolve() launchers=[root/name for name in ('w3dev','cli.py','native.py') if (root/name).is_file()] files=launchers+sorted((root/'witcher_devkit').glob('*.py'))+sorted((root/'scripts').glob('*.ws.in')) return dict(root=str(root),sha256={p.relative_to(root).as_posix():hashlib.sha256(p.read_bytes()).hexdigest() for p in files})
def recovery_source(old): if old and old.get('source'): root=Path(old['source']['root']) if source_identity(root)==old['source']: return root/'witcher_devkit/debugger.py' # Receipts from the original flat source layout can still be served by # their exact, unchanged checkout, without dispatching through new code. if not (root/'witcher_devkit').exists(): files=sorted(root.glob('*.py'))+sorted(root.glob('*.ws.in')) legacy=dict(root=str(root),sha256={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in files}) if legacy==old['source']: return root/'debugger.py' # After external closure, newer source may audit/restore, never dispatch # gameplay through an implementation different from the original owner. try: native.legacy.manage.ensure_game_closed() except RuntimeError as error: raise RuntimeError('owner_source_changed: original source is changed/unavailable; live recovery refused. ' 'Restore its source, or close the exact lab game before audited post-exit recovery.') from error return Path(__file__).resolve()
def identity(pid=None): pid=pid or os.getpid() try: data=Path(f'/proc/{pid}/stat').read_text().rsplit(')',1)[1].split() if data[0]=='Z': return None return dict(pid=pid,start=data[19]) except OSError: return None
def descriptor(): if not descriptor_path().exists(): return None return json.loads(descriptor_path().read_text())
def active_owner(): d=descriptor() return d if d and identity(d['broker']['pid'])==d['broker'] else None
def reject_legacy(): d=descriptor() if d and d['broker']['pid']!=os.getpid() and (identity(d['broker']['pid'])==d['broker'] or (d.get('process') and native.legacy.process_identity(d['process']['pid'])==d['process'])): raise RuntimeError(f"session_owned: debugger {d['session_id']} owns the game. Use debug commands; do not bypass its operation journal.") pending=starting_status() if (pending and pending.get('startup',{}).get('failure') and pending['startup'].get('cleanup_required')): raise RuntimeError('startup_recovery_required: inspect debug status, confirm exact lab process exit, then debug recover')
def receive(stream): raw=stream.readline(MAX_MESSAGE+1) if len(raw)>MAX_MESSAGE or not raw.endswith(b'\n'): raise ValueError('message_size_or_framing') return json.loads(raw)
class Client: def __init__(self, receipt=None): self.receipt=receipt or descriptor()
def request(self, method, **args): d=self.receipt if not d: raise RuntimeError('no_debug_session: use debug open or debug attach --adopt') cleanup=method in ('cancel','pause','recover-closed') or (method=='submit' and args.get('kind')=='close') if not cleanup and method not in ('status','operation') and d.get('source') and source_identity()['sha256']!=d['source']['sha256']: raise RuntimeError('owner_source_mismatch: use the owning source checkout or close it before testing this worktree') if identity(d['broker']['pid'])!=d['broker'] and method in ('status','operation'): closed_recovery=Path(d['folder'])/'closed-recovery.json' if method=='status' and closed_recovery.exists(): return dict(ok=True,state='closed',historical=True,session_id=d['session_id'], recovery=json.loads(closed_recovery.read_text()),next=None) journal=Journal(d['folder']) if method=='operation': op=journal.operations.get(args['operation_id']) if op and op['status'] in ('accepted','running'): op=dict(op,status='interrupted',outcome='unknown') return dict(ok=op is not None,operation=op,historical=True) closed=any(o.get('kind')=='close' and o['status']=='completed' for o in journal.operations.values()) or (Path(d['folder'])/'closed-recovery.json').exists() return dict(ok=True,state='closed' if closed else 'disconnected',historical=True, session_id=d['session_id'],journal=str(journal.path),next=None if closed else 'debug recover') with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as s: s.settimeout(10); s.connect(d['socket']) s.sendall(encode(dict(protocol_version=1,method=method,token=d['token'],**args))+b'\n') with s.makefile('rb') as f: return receive(f)
def submit(self, kind, args=None, operation_id=None, wall_seconds=30, expected_generation=None): return self.request('submit',kind=kind,args=args or {},operation_id=operation_id or secrets.token_hex(12), wall_seconds=wall_seconds,expected_generation=expected_generation)
def wait(self, operation_id, timeout=30): end=time.monotonic()+timeout while True: r=self.request('operation',operation_id=operation_id) if not r.get('ok') or r['operation']['status'] not in ('accepted','running'): return r if time.monotonic()>=end: return dict(ok=True,waiting=True,operation_id=operation_id,next='debug operation '+operation_id) time.sleep(.1)
class Broker: def __init__(self, receipt, journal): self.receipt,self.journal=receipt,journal self.mutex=threading.Lock(); self.cancelled=threading.Event() self.runtime=Runtime(journal,self.cancelled) self.operations=dict(journal.operations) self.active=None; self.closed=False; self.storage_failed=False self.uncertain=False self.owner_started_ns=time.monotonic_ns() self.accepting=True self.shutdown_operations=[] for op in self.operations.values(): if op['status']=='uncertain': self.uncertain=True if op['status'] in ('accepted','running'): op['status']='interrupted'; op['outcome']='unknown' self.uncertain=True self.journal.append('result',op,cleanup=True)
def state(self): if self.closed: return 'closed' try: s=native.state() if s.get('uncertain') or self.uncertain: return 'uncertain' except (OSError,ValueError,RuntimeError): return 'disconnected' if self.closed: return 'closed' if self.active: return 'running' c=self.runtime.cached if not c or not c['data'].get('ready'): return 'attached_no_world' return 'paused' if c['data']['world']['devkit_paused'] else 'running'
def request(self, msg, internal=False): if msg.pop('protocol_version',1)!=1: raise ValueError('unsupported_protocol_version: expected 1') if not secrets.compare_digest(str(msg.pop('token','')),self.receipt['token']): raise ValueError('owner_token_mismatch: reread the local owner descriptor') method=msg.pop('method') if method=='submit' and msg.get('kind')!='close' and self.receipt.get('source') and source_identity()!=self.receipt['source']: raise ValueError('owner_source_changed: close the session before editing its source') with self.mutex: if method=='recover-closed': if self.active: raise ValueError('busy: wait for the current operation') from .debug_recovery import closed_cleanup result=closed_cleanup(self.receipt) self.journal.append('closed_recovery',result,cleanup=True); self.journal.manifest() self.closed=True return result if method=='status': return dict(ok=True,session_id=self.receipt['session_id'],state=self.state(),active_operation=self.active, cached=True,snapshot=self.runtime.cached,controls=self.runtime.cached_controls,journal=str(self.journal.path),storage_failed=self.storage_failed) if method=='operation': op=self.operations.get(msg['operation_id']) return dict(ok=op is not None,operation=op,error=None if op else 'unknown_operation') if method in ('cancel','pause'): requested=msg.get('operation_id',self.active) if self.active and requested==self.active: self.journal.append('cancel_requested',dict(operation_id=self.active),cleanup=True) self.cancelled.set(); return dict(ok=True,cancel_requested=True,operation_id=self.active, completion='pending: query operation; cancellation acknowledgement is not pause acknowledgement') if method=='cancel': return dict(ok=False,error='operation_not_running') msg=dict(kind='pause',args={},operation_id=secrets.token_hex(12),wall_seconds=15,expected_generation=None) method='submit' if method!='submit': raise ValueError('unknown_method') if not self.accepting and not internal: raise ValueError('busy: owner is shutting down; results remain available') if set(msg)-{'kind','args','operation_id','wall_seconds','expected_generation'}: raise ValueError('unknown operation request fields') expected=msg.get('expected_generation') if expected is not None and (type(expected) is not int or not 0<=expected<=2147483646): raise ValueError('expected_generation must be a nonnegative native integer') kind,args=msg['kind'],msg['args']; validate(kind,args) opid=msg['operation_id'] if not re.fullmatch('[A-Za-z0-9_-]{1,80}',opid): raise ValueError('invalid_operation_id') wall=msg['wall_seconds'] if type(wall) not in (int,float) or not math.isfinite(wall) or not 0<wall<=300: raise ValueError('wall_seconds must be finite >0 and <=300') fingerprint=hashlib.sha256(encode(msg)).hexdigest() if opid in self.operations: old=self.operations[opid] if old['fingerprint']!=fingerprint: raise ValueError('operation_id_conflict: same ID has different arguments; no dispatch') return dict(ok=True,replayed=False,operation=old) if self.closed: raise ValueError('session_closed') if self.active: raise ValueError('busy: query or cancel active operation '+self.active) if self.storage_failed and kind not in ('pause','close'): raise ValueError('storage_failed: restore journal storage before more native work') if (self.uncertain or self.state()=='uncertain') and kind not in ('inspect','events') and not ( (kind=='control' and args['mode']=='status') or (kind=='overlay' and args['action']=='status')): if kind!='close': raise ValueError('uncertain: read-only inspection only; no replay or uncertainty flag clearing') # Historical/compound uncertainty is not unresolved native # delivery. Explicit close may exit without replaying that work, # but only while the actual native receipt is certain. try: native_receipt = native.state() if not isinstance(native_receipt,dict) or 'uncertain' not in native_receipt: raise RuntimeError('native uncertainty field unavailable') native_uncertain = native_receipt['uncertain'] except (OSError, ValueError, RuntimeError, KeyError, TypeError) as error: raise ValueError('uncertain: native receipt unavailable for close; follow external-exit/recovery instructions') from error if native_uncertain is not None: raise ValueError('uncertain: unresolved native delivery blocks close; externally close the identified lab process, then debug recover') op=dict(schema_version=1,operation_id=opid,fingerprint=fingerprint,kind=kind,args=args,status='accepted', expected_generation=msg.get('expected_generation'),wall_seconds=wall,session_id=self.receipt['session_id']) self.journal.append('intent',op,cleanup=kind in ('pause','close')) self.operations[opid]=op; self.active=opid; self.cancelled.clear() threading.Thread(target=self.execute,args=(opid,),daemon=True).start() return dict(ok=True,operation=op)
def orderly_shutdown(self): """Poll from the server loop; never introduce a second native writer.""" self.accepting=False; self.cancelled.set() if self.active: return False if self.state() in ('uncertain','disconnected'): self.journal.append('owner_shutdown',dict(state=self.state(),pause_verified=False,operations=self.shutdown_operations),cleanup=True) self.journal.manifest(); return True if not self.shutdown_operations: kind='inspect'; args={'view':'world'} else: previous=self.operations[self.shutdown_operations[-1]] if previous['status']!='completed': self.journal.append('owner_shutdown',dict(state=self.state(),pause_verified=False,operations=self.shutdown_operations),cleanup=True) self.journal.manifest(); return True snapshot=previous['result'] data=snapshot.get('data',{}) if previous['kind']=='pause' or data.get('control')=='human' or not data.get('ready') or (data.get('world',{}).get('devkit_paused') and not data.get('remaining_ticks') and not data.get('input_ticks')): self.journal.append('owner_shutdown',dict(state=self.state(),pause_verified=bool(data.get('ready') and data.get('control')!='human'),operations=self.shutdown_operations),cleanup=True) self.journal.manifest(); return True kind='pause'; args={} opid=secrets.token_hex(12) self.shutdown_operations.append(opid) self.request(dict(method='submit',token=self.receipt['token'],kind=kind,args=args, operation_id=opid,wall_seconds=15,expected_generation=None),internal=True) return False
def execute(self, opid): op=dict(self.operations[opid]); op['status']='running' with self.mutex: self.operations[opid]=op started=time.monotonic() try: expected=op['expected_generation'] if expected is not None: self.runtime.deadline=time.monotonic()+op['wall_seconds'] if self.runtime.call('snapshot')['generation']!=expected: raise ValueError('stale_generation: operation not dispatched') remaining=op['wall_seconds']-(time.monotonic()-started) if remaining<=0: raise TimeoutError('operation deadline expired before dispatch') value=self.runtime.execute(op['kind'],op['args'],remaining) if op['kind']=='close': baseline=self.journal.folder/'mods-before.json' value['mods_unchanged']=json.loads(baseline.read_text())==native.legacy.manage.files(native.legacy.manage.GAME/'mods') if baseline.exists() else None if value['mods_unchanged'] is False: value['ok']=False if op['kind']=='handoff': self.receipt=dict(self.receipt,token=secrets.token_hex(24)) durable_json(descriptor_path(),self.receipt) value['token_rotated']=True value['next']='Reconnect by reading the local owner descriptor; old tokens are rejected.' op.update(status='completed',result=value) if op['kind']=='close': self.closed=True except Exception as error: try: uncertain=bool(native.state().get('uncertain')) except Exception: uncertain=True # Completion timeout after an acknowledgement still leaves compound # operation outcome unknown. Never allow a second action to erase it. uncertain=uncertain or bool(getattr(error,'outcome_uncertain',False)) or (self.runtime.mutation_dispatched and (isinstance(error,(TimeoutError,ConnectionError)) or any(t in str(error) for t in ('acknowledgement','Response identity','response part')))) self.uncertain=self.uncertain or uncertain if isinstance(error,OSError) and not isinstance(error,(TimeoutError,ConnectionError)): self.storage_failed=True op.update(status='uncertain' if uncertain else 'rejected',error=str(error),error_type=type(error).__name__) if hasattr(error,'evidence'): op['error_evidence']=error.evidence op['timing']=dict(host_wall_seconds=time.monotonic()-started,wall_budget_seconds=op['wall_seconds'], deadline_overrun_seconds=max(0,time.monotonic()-started-op['wall_seconds'])) op['observation_cost']=dict(native_calls=self.runtime.rpc_count,response_parts=self.runtime.rpc_parts, response_json_bytes=self.runtime.response_bytes,scope='Nested RPC counts; not additive exclusive phase timing') if op['kind']=='close': op['observation_cost']=dict(native_calls=None,response_parts=None,response_json_bytes=None, availability='unavailable',reason='native.stop uses its existing lifecycle transport; consult native actions.jsonl for its exit request', scope='Operation wall time includes cleanup; no zero-cost claim for uninstrumented helper calls') op['experimental_validity']='not_evaluated' with self.mutex: try: self.journal.append('result',op,cleanup=True) self.journal.manifest() except Exception as error: self.storage_failed=True op.update(status='uncertain',persistence_error=str(error)) self.operations[opid]=op; self.active=None
class Server(socketserver.ThreadingUnixStreamServer): daemon_threads=True
class Handler(socketserver.StreamRequestHandler): def handle(self): self.connection.settimeout(10) try: result=self.server.broker.request(receive(self.rfile)) except Exception as e: result=dict(ok=False,error=str(e),error_type=type(e).__name__) result['protocol_version']=1 data=encode(result)+b'\n' if len(data)>MAX_MESSAGE: data=encode(dict(ok=False,error='response_too_large: read journal artifact for the durable result'))+b'\n' self.wfile.write(data)
def recover_closed(receipt): """Restoration must not depend on a valid journal after confirmed exit.""" from .debug_recovery import closed_cleanup result=closed_cleanup(receipt) try: journal=recover_torn_tail(receipt['folder']) Broker(receipt,journal) # Preserve interrupted operations when readable. journal.append('closed_recovery',result,cleanup=True) journal.manifest() except (ValueError,OSError) as error: # Never renumber/rewrite a corrupt journal or call it verified evidence. result.update(cleanup_ok=result['ok'],ok=False,journal_integrity=False,journal_error=str(error)) durable_json(Path(receipt['folder'])/'closed-recovery.json',result) durable_json(descriptor_path(),dict(receipt,recovery=result)) return result
def serve(mode, startup_seconds, startup_folder=None): # A detached broker must not inherit a terminal-scoped (or missing) TMPDIR. # Keep compiler scratch alive for its entire lifetime, including cleanup. parent = native.STATE_ROOT/'tmp' parent.mkdir(parents=True, exist_ok=True, mode=0o700) with tempfile.TemporaryDirectory(prefix='broker-', dir=parent) as temporary: previous = {key: os.environ.get(key) for key in ('TMPDIR', 'TMP', 'TEMP')} try: os.environ.update({key: temporary for key in previous}) try: return (serve_owned(mode, startup_seconds, startup_folder) if startup_folder else serve_owned(mode, startup_seconds)) except Exception as error: if startup_folder: from . import debug_startup from .diagnostics import classify debug_startup.fail(startup_folder, classify(error), 'startup_exception', str(error)) raise finally: for key, value in previous.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value
def serve_owned(mode, startup_seconds, startup_folder=None): os.umask(0o077) native.legacy.manage.state_directory() with (native.STATE_ROOT/'.lock').open('a') as lock: fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB) old=descriptor() if mode=='recover': if not old: raise ValueError('no_previous_debug_owner') if old.get('source') and source_identity()!=old['source']: native.legacy.manage.ensure_game_closed() receipt=dict(old,broker=identity(),token=secrets.token_hex(24),created_ns=time.time_ns()) # Native session is reused, never attach/hello over unresolved work. if native.STATE.exists(): s=json.loads(native.STATE.read_text()) if s['session']!=receipt['native_session']: raise ValueError('native_session_changed: recovery refused') if native.legacy.process_identity(s['process']['pid'])!=s['process']: recover_closed(receipt) return journal=recover_torn_tail(receipt['folder']) else: if old and old.get('native_session') and native.STATE.exists(): s=json.loads(native.STATE.read_text()) if s['session']==old['native_session'] and native.legacy.process_identity(s['process']['pid'])==s['process']: raise ValueError('previous_owner_recovery_required: use debug recover') if mode=='open': from . import game_settings from . import debug_startup if not startup_folder: raise ValueError('startup evidence folder required for open') debug_startup.wait_for_window(startup_folder, startup_seconds, game_settings.launch) reuse=False if mode=='attach' and native.STATE.exists(): candidate=json.loads(native.STATE.read_text()) reuse=native.legacy.process_identity(candidate['process']['pid'])==candidate['process'] if reuse and candidate.get('uncertain'): raise ValueError('uncertain legacy session cannot be adopted') if mode=='attach': from . import lifecycle if not lifecycle.verify().get('background_enabled'): raise ValueError('adoption_requires_instrumented_lab_mode') native.legacy.window(native.legacy.process()) if not reuse: native.attach() s=native.state(); sid=secrets.token_hex(12) folder=native.legacy.ARTIFACTS/('debug-'+sid) journal=Journal(folder) durable_json(folder/'mods-before.json',native.legacy.manage.files(native.legacy.manage.GAME/'mods')) receipt=dict(schema_version=1,session_id=sid,native_session=s['session'],process=s['process'],build=s['build'], broker=identity(),token=secrets.token_hex(24),folder=str(folder), created_ns=time.time_ns(),host_clock_id=Path('/proc/sys/kernel/random/boot_id').read_text().strip(), source=source_identity(), bridge_sha256=json.loads((native.STATE_ROOT/'deployment.json').read_text())['mod_sha256']) runtime=native.legacy.manage.RUNTIME_ROOT runtime.mkdir(parents=True,exist_ok=True,mode=0o700) receipt['socket']=str(runtime/'owner.sock') sock=Path(receipt['socket']) if sock.exists(): sock.unlink() broker=Broker(receipt,journal) with Server(str(sock),Handler) as server: os.chmod(sock,0o600); server.broker=broker journal.append('owner',dict(receipt,token='redacted')) durable_json(descriptor_path(),receipt) if startup_folder: from . import debug_startup debug_startup.update(startup_folder, phase='owned', cleanup_required=False, owner_session=receipt['session_id'], process=receipt['process']) shutdown=threading.Event() def shutdown_signal(signum,frame): broker.accepting=False; broker.cancelled.set(); shutdown.set() signal.signal(signal.SIGTERM,shutdown_signal); signal.signal(signal.SIGINT,shutdown_signal) try: while not broker.closed: if shutdown.is_set() and broker.orderly_shutdown(): break server.timeout=.2; server.handle_request() # Let the close worker persist its final audit before exit. if broker.closed: while broker.active: time.sleep(.02) finally: if sock.exists(): sock.unlink()
def starting_status(): path=native.STATE_ROOT/'debug-starting.json' if not path.exists() or active_owner(): return None request=json.loads(path.read_text()); owner=descriptor() if owner and owner.get('created_ns',0)>request['created_ns']: return None alive=identity(request['broker']['pid'])==request['broker'] if request.get('folder'): from . import debug_startup startup=debug_startup.read(request['folder']) failed=bool(startup.get('failure')) # Full exact manifests/source hashes remain in the durable artifact, not # every status socket response (which has a fixed byte budget). summary={key:value for key,value in startup.items() if key not in ('before','after','source')} if 'compiler' in summary: compiler=summary['compiler'] summary['compiler']={key:value for key,value in compiler.items() if key not in ('packets','log')} summary['compiler']['packet_count']=len(compiler.get('packets',[])) summary['compiler']['raw_artifact']=str(Path(request['folder'])/'startup.json') summary['compiler']['log']={key:value for key,value in compiler.get('log',{}).items() if key!='fresh_text'} return dict(ok=alive and not failed,state='startup_failed' if failed or not alive else 'starting', log=request['log'],broker=request['broker'],startup=summary, startup_folder=request['folder'], next=startup.get('recovery') if failed else 'debug status' if alive else 'Inspect retained startup evidence; do not replay game operations.') return dict(ok=alive,state='starting' if alive else 'startup_failed',log=request['log'],broker=request['broker'], next='debug status' if alive else 'Inspect startup log and process state; do not replay game operations.')
def start(mode='open', startup_seconds=120): if not math.isfinite(startup_seconds) or not 0 < startup_seconds <= 180: raise ValueError('startup_seconds must be finite >0 and <=180') if active_owner(): if mode=='recover': return Client().request('recover-closed') raise ValueError('session_owned: use debug status to reconnect') pending=starting_status() if pending and pending['state']=='starting': raise ValueError('session_owned: debugger startup already in progress') if pending and pending.get('startup_folder') and pending.get('startup',{}).get('failure'): from . import debug_startup if mode=='recover': with (native.STATE_ROOT/'.lock').open('a') as lock: fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB) return debug_startup.recover_closed(pending['startup_folder']) if pending['startup'].get('cleanup_required'): raise ValueError('startup_recovery_required: inspect debug status, confirm exact lab process exit, then debug recover') requested_ns=time.time_ns() native.legacy.manage.state_directory() script=recovery_source(descriptor()) if mode=='recover' else Path(__file__).resolve() logfile=native.STATE_ROOT/'artifacts'/'debug-broker-startup.log'; logfile.parent.mkdir(exist_ok=True) folder=logfile.parent/('debug-startup-'+str(requested_ns));folder.mkdir(mode=0o700) durable_json(folder/'startup.json',dict(phase='prepared',mode=mode,created_ns=requested_ns, source=source_identity(),startup_seconds=startup_seconds)) with logfile.open('ab') as out: command=([sys.executable,'-m','witcher_devkit.debugger'] if script.parent.name=='witcher_devkit' else [sys.executable,str(script)]) child=subprocess.Popen([*command,'serve',mode,str(startup_seconds),str(folder)], cwd=script.parent.parent if script.parent.name=='witcher_devkit' else script.parent, stdin=subprocess.DEVNULL,stdout=out,stderr=out,start_new_session=True) durable_json(native.STATE_ROOT/'debug-starting.json',dict(broker=identity(child.pid) or {'pid':child.pid,'start':'exited'},created_ns=requested_ns,log=str(logfile),folder=str(folder))) return dict(ok=True,broker_pid=child.pid,state='starting',log=str(logfile), startup_folder=str(folder), next='debug status; startup is asynchronous and does not load a save')
if __name__=='__main__': if sys.argv[1:2]==['serve']: serve(sys.argv[2],float(sys.argv[3]),sys.argv[4] if len(sys.argv)>4 else None)