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