Something went wrong. Try again.
Native control, inspection, UI automation and evidence capture for a running Witcher 3 process.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135"""Explicit closed-game migration; never copy ownership into a second runtime."""import fcntlimport hashlibimport jsonimport osfrom pathlib import Path
from . import game_pathsfrom .debug_journal import durable_json
RECEIPTS = ( 'deployment.json', 'native/deployment.json', 'native-session.json', 'session.json', 'debug-owner.json', 'debug-starting.json', 'background-mode.json', 'workspace-rule.json', 'performance-active.json', 'performance-launch.json', 'probe-install.json', 'probe.csv',)
def verify_installation(legacy, state): def receipt(name): src=legacy/name return json.loads((src if src.exists() else state/name).read_text()) from .lifecycle import owned_scripts bridge=receipt('deployment.json') native=receipt('native/deployment.json') files=owned_scripts(bridge)+[(Path(f['path']),f['sha256']) for f in native['files']] if 'executable' in native: files.append((Path(native['executable']['path']),native['executable']['sha256'])) for path,expected in files: if hashlib.sha256(path.read_bytes()).hexdigest()!=expected: raise RuntimeError('Installed file changed; migration refused: '+str(path)) return len(files)
def migrate(legacy, apply=False): """Moves retain bytes/inodes; historical artifact paths remain aliases.
Config is published last. A retained intent identifies partial migration. Retry completes the same moves; conflicting destinations are never overwritten. """ legacy=Path(legacy).expanduser().resolve() config=legacy/'local-config.json' game,docs=game_paths.load_paths(config,{}) if not config.is_file() or not game.is_dir() or not docs.is_dir(): raise ValueError('Migration requires an existing configured installation') key=hashlib.sha256(str(game.resolve()).encode()).hexdigest()[:24] state=Path(os.environ.get('XDG_STATE_HOME',Path.home()/'.local/state'))/'witcher3-devkit'/key global_config=game_paths.GLOBAL_CONFIG selected=dict(schema_version=1,game_directory=str(game),documents_directory=str(docs)) if global_config.exists() and game_paths.load_paths(global_config,{})!=(game,docs): raise ValueError('Global configuration selects another installation; refusing replacement') result=dict(ok=True,applied=False,legacy=str(legacy),state=str(state),configuration=str(global_config), receipts=list(RECEIPTS),historical_artifacts=str(legacy/'artifacts')) game_paths.ensure_game_closed() # A live broker can exist even with a closed game; never relocate its state. from .debugger import identity for name in ('debug-owner.json','debug-starting.json'): p=legacy/name if p.exists(): broker=json.loads(p.read_text()).get('broker') if broker and identity(broker['pid'])==broker: raise RuntimeError('Migration requires the recorded broker to exit first') for name in RECEIPTS: src,dst=legacy/name,state/name if src.exists() and dst.exists(): raise RuntimeError('Conflicting migration destination: '+str(dst)) result['verified_files']=verify_installation(legacy,state) if not apply: return result # Moving the existing lock inode is essential. Do not copy it across devices. state.mkdir(parents=True,exist_ok=True,mode=0o700) if legacy.stat().st_dev!=state.stat().st_dev: raise RuntimeError('Migration requires same-filesystem rename; no locks or receipts copied') with (state/'.migration.lock').open('a') as migration, (legacy/'.lock').open('a') as lock, (legacy/'.performance.lock').open('a') as perf: fcntl.flock(migration,fcntl.LOCK_EX|fcntl.LOCK_NB) fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB) fcntl.flock(perf,fcntl.LOCK_EX|fcntl.LOCK_NB) game_paths.ensure_game_closed() verify_installation(legacy,state) intent=state/'migration.json' if intent.exists(): previous=json.loads(intent.read_text()) if previous['legacy']!=str(legacy): raise RuntimeError('State belongs to another migration') hashes={name:hashlib.sha256((legacy/name).read_bytes()).hexdigest() for name in RECEIPTS if (legacy/name).is_file()} if intent.exists(): hashes=dict(previous.get('sha256',{}),**hashes) durable_json(intent,dict(result,status='moving',sha256=hashes)) for name in ('.lock','.performance.lock')+RECEIPTS+('artifacts',): src,dst=legacy/name,state/name if src.is_symlink(): if src.resolve()!=dst.resolve(): raise RuntimeError('Unexpected migration symlink: '+str(src)) continue if name in ('.lock','.performance.lock'): # Publish a second name for the SAME inode before atomically # replacing the old pathname. There must never be an interval # in which an old client can create a different lock inode. if dst.exists(): if not src.samefile(dst): raise RuntimeError('Conflicting lock inode: '+str(dst)) else: os.link(src,dst) alias=src.with_name(src.name+'.migration-next') if alias.is_symlink(): if alias.readlink()!=dst: raise RuntimeError('Unexpected lock alias') else: alias.symlink_to(dst) alias.replace(src) continue if src.exists(): if dst.exists(): raise RuntimeError('Migration destination exists: '+str(dst)) dst.parent.mkdir(parents=True,exist_ok=True,mode=0o700) src.rename(dst) # Preserve historical artifact paths and old lock exclusion, not old # mutable receipt paths (legacy atomic writes would replace aliases). if name=='artifacts': src.symlink_to(dst,target_is_directory=name=='artifacts') (state/'artifacts').mkdir(exist_ok=True,mode=0o700) durable_json(state/'installation.json',selected) global_config.parent.mkdir(parents=True,exist_ok=True,mode=0o700) durable_json(global_config,selected) cache=game_paths.CACHE_ROOT cache.mkdir(parents=True,exist_ok=True,mode=0o700) for name in ('toolchains','repos','downloads','python-env'): src=legacy/'research'/name dst=cache/name if src.exists() and not dst.exists() and not dst.is_symlink(): dst.symlink_to(src,target_is_directory=True) result['applied']=True durable_json(intent,dict(result,status='complete',sha256=hashes)) return result