"""Two production RNS directory processes, interrupted file, offline source, browser.""" import hashlib import base64 from contextlib import closing import json import os from pathlib import Path import socket import sqlite3 import subprocess import sys import sysconfig import tempfile import time import unittest import RNS from tools.tracks.blob_store import BlobStore from tools.tracks.directory_index import DirectoryIndex from tools.tracks.directory_service import load_identity from tools.tracks.protocol import packed,unpacked,verify_manifest from tools.tracks.attachment_index import encode_index ROOT=Path(__file__).resolve().parents[2] def free_port(): with socket.socket() as listener:listener.bind(('127.0.0.1',0));return listener.getsockname()[1] class ReplicationNetworkTests(unittest.TestCase): def test_native_restart_copy_and_fresh_browser_after_source_offline(self): retained=ROOT/'.codex-build/hostlink-codec/track-replication-network' retained.mkdir(parents=True,exist_ok=True) (retained/'NOTES.txt').write_text('Two local native track services, interrupted copy restart, source stopped and new browser reading archived replica. Current production sources. Isolated synthetic author only; no physical device, RF or public publication. Retain compact result/logs until acceptance.') children=[];flags=getattr(subprocess,'CREATE_NO_WINDOW',0) # Windows venv python.exe is a redirector: terminating that PID can # leave the real interpreter serving sockets. Own the actual process. service_python=sys._base_executable if os.name=='nt' else sys.executable service_environment={**os.environ,'TRAIL_MATE_REPLICATION_DEBUG':'1'} if os.name=='nt':service_environment['PYTHONPATH']=sysconfig.get_path('purelib') with tempfile.TemporaryDirectory() as work: root=Path(work);source_state=root/'source';source_state.mkdir() source_identity=load_identity(source_state) data=b'0,0'+b'\n'*16384 digest=hashlib.sha256(data).digest();author=RNS.Identity();blobs=BlobStore(source_state/'track-data');index=DirectoryIndex(source_state/'track-data',blobs) upload=blobs.begin(author.hash,digest,len(data)) for offset in range(0,len(data),1024):blobs.append(author.hash,upload['token'],offset,data[offset:offset+1024]) blobs.finish(author.hash,upload['token']);versions=[] record=[1,author.get_public_key(),os.urandom(16),0,1,None,0,100,100,'Native replica track','',[0,0,0,0],[0,None,None,1],[digest,len(data),'application/vnd.google-earth.kml+xml',1]] for revision in range(1,4): record[4]=revision;record[5]=verify_manifest(versions[-1])['revision_hash'] if versions else None;record[8]=99+revision;record[6]=int(revision==3) raw=packed(record);signed=[raw,author.sign(b'TrailMate.Track.Manifest.v1\0'+raw)];index.publish(author.hash,signed,record[5]);versions.append(signed) # A separate v2 object exercises the signed index and real PNG in # addition to the v1 history above; it shares the immutable KML. png=base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a7GoAAAAASUVORK5CYII=') def put_extra(content): file_hash=hashlib.sha256(content).digest();upload=blobs.begin(author.hash,file_hash,len(content)) if upload['token']: for offset in range(0,len(content),1024):blobs.append(author.hash,upload['token'],offset,content[offset:offset+1024]) blobs.finish(author.hash,upload['token']) return file_hash png_hash=put_extra(png) raw_index=encode_index([1,digest,len(data),[['icon',png_hash,len(png),'image/png','icon.png',0,1]]]) index_hash=put_extra(raw_index) record=[2,author.get_public_key(),os.urandom(16),0,1,None,0,100,100,'Native replica package','',[0,0,0,0],[0,None,None,1],[digest,len(data),'application/vnd.google-earth.kml+xml',1],[0,[],0,0,0,0,0,None],[index_hash,len(raw_index),1,1]] package_versions=[] for revision in range(1,4): record[4]=revision;record[5]=verify_manifest(package_versions[-1])['revision_hash'] if package_versions else None;record[8]=99+revision;record[6]=int(revision==3) raw=packed(record);signed=[raw,author.sign(b'TrailMate.Track.Manifest.v2\0'+raw)];index.publish(author.hash,signed,record[5]);package_versions.append(signed) fork_record=[2,author.get_public_key(),os.urandom(16),0,1,None,0,100,100,'Fork root','',[0,0,0,0],[0,None,None,1],[digest,len(data),'application/vnd.google-earth.kml+xml',1],[0,[],0,0,0,0,0,None],[index_hash,len(raw_index),1,1]] raw=packed(fork_record);fork_root=[raw,author.sign(b'TrailMate.Track.Manifest.v2\0'+raw)];index.import_replica(author.hash,fork_root) fork_id=verify_manifest(fork_root)['object_id'] for title in ('Fork left','Fork right'): fork_record[4]=2;fork_record[5]=verify_manifest(fork_root)['revision_hash'];fork_record[8]=101;fork_record[9]=title raw=packed(fork_record);index.import_replica(author.hash,[raw,author.sign(b'TrailMate.Track.Manifest.v2\0'+raw)]) index.close();blobs.close();object_id=verify_manifest(package_versions[0])['object_id'];latest=verify_manifest(package_versions[-1])['revision_hash'] source_port,replica_port=free_port(),free_port();source_config=root/'source-rns';source_config.mkdir();replica_config=root/'replica-rns';replica_config.mkdir() base='[reticulum]\nshare_instance = No\nenable_transport = Yes\n[interfaces]\n' (source_config/'config').write_text(base+f'[[Source]]\nenabled = Yes\ntype = TCPServerInterface\nlisten_ip = 127.0.0.1\nlisten_port = {source_port}\n') (replica_config/'config').write_text(base+f'[[Source uplink]]\nenabled = Yes\ntype = TCPClientInterface\ntarget_host = 127.0.0.1\ntarget_port = {source_port}\n[[Readers]]\nenabled = Yes\ntype = TCPServerInterface\nlisten_ip = 127.0.0.1\nlisten_port = {replica_port}\n') def start(label,args): log=retained/(label+'.log');handle=log.open('w',encoding='utf-8');child=subprocess.Popen([service_python,*args],cwd=ROOT,stdout=handle,stderr=subprocess.STDOUT,creationflags=flags,env=service_environment);children.append((child,handle)) until=time.monotonic()+25 while child.poll()is None and time.monotonic()