okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f479db5
main
1"""Bounded, atomic snapshots of the kernel-owned namespace and definitions.""" 2 3from __future__import annotations 4 5import os 6import pickle 7import tempfile 8import types 9from typing import Any ,cast 10 11# Saved state is the session's own namespace, written by this kernel into the 12# daemon's home. Loading pickle and replaying definitions share the transcript's trust domain. 13STATE_MAX = 64 * 1024 * 1024 14STATE_MAX_VALUE = 8 * 1024 * 1024 15DEFINITION_MAX = 256 * 1024 16DEFINITION_MAX_VALUE = 32 * 1024 17 18 19def engine ()-> tuple [object ,str ]: 20"""dill when it is installed; pickle keeps plain data working without it.""" 21try : 22import dill 23 24dill .settings ["recurse" ]= True 25return dill ,"dill" 26except ImportError : 27return pickle ,"pickle" 28 29 30class TooLarge (Exception ): 31"""The value's serialisation passed the per-variable cap.""" 32 33 34class _Bounded : 35"""A write sink that stops a serialiser once it has produced more than 36`limit` bytes, so an oversized value costs the cap, not its full size.""" 37 38def __init__ (self ,limit :int )-> None : 39self .limit = limit 40self .chunks :list [bytes ]= [] 41self .size = 0 42 43def write (self ,data :bytes )-> int : 44self .size += len (data ) 45if self .size > self .limit : 46raise TooLarge 47self .chunks .append (bytes (data )) 48return len (data ) 49 50def getvalue (self )-> bytes : 51return b"" .join (self .chunks ) 52 53 54def dumps_within (serialiser :Any ,value :object ,limit :int )-> bytes : 55"""`serialiser.dumps(value)`, or TooLarge once it passes `limit` bytes.""" 56sink = _Bounded (limit ) 57serialiser .dump (value ,sink ) 58return sink .getvalue () 59 60 61def save_state ( 62path :str , 63namespace :dict [str ,object ], 64definitions :dict [str ,str ], 65injected :set [str ], 66)-> dict [str ,object ]: 67"""Serialise values and readable top-level definitions within fixed caps.""" 68serialiser ,kind = cast ("Any" ,engine ()) 69payload :dict [str ,bytes ]= {} 70skipped :list [dict [str ,str ]]= [] 71total = 0 72largest :list [tuple [str ,int ]]= [] 73for name in list (namespace .keys ()): 74if name .startswith ("_" )or name in injected : 75continue 76value = namespace .get (name ,injected ) 77if value is injected : 78continue 79replayed = name in definitions and isinstance ( 80value , (types .FunctionType ,type ,types .ModuleType ) 81 ) 82try : 83blob = dumps_within (serialiser ,value ,STATE_MAX_VALUE ) 84except BaseException as error : 85reason = ( 86f"over the { STATE_MAX_VALUE } -byte per-variable cap" 87if isinstance (error ,TooLarge ) 88else f" { type ( error ). __name__ } : { error } " [:200 ] 89 ) 90if not replayed : 91skipped .append ({"name" :name ,"reason" :reason }) 92continue 93if total + len (blob )> STATE_MAX : 94skipped .append ({"name" :name ,"reason" :"saved state is full" }) 95else : 96payload [name ]= blob 97total += len (blob ) 98if len (blob )>= 64 * 1024 : 99largest .append ((name ,len (blob ))) 100saved_definitions :list [dict [str ,str ]]= [] 101definition_bytes = 0 102for name ,source in definitions .items (): 103size = len (source .encode ("utf-8" )) 104if size > DEFINITION_MAX_VALUE : 105skipped .append ( 106 { 107"name" :name , 108"reason" :f"definition is { size } bytes; per-definition cap is { DEFINITION_MAX_VALUE } " , 109 } 110 ) 111elif definition_bytes + size > DEFINITION_MAX : 112skipped .append ( 113 {"name" :name ,"reason" :"definitions are over the total source cap" } 114 ) 115else : 116saved_definitions .append ({"name" :name ,"source" :source }) 117definition_bytes += size 118try : 119os .makedirs (os .path .dirname (path )or "." ,exist_ok = True ) 120handle ,temporary = tempfile .mkstemp ( 121dir = os .path .dirname (path )or "." ,prefix = os .path .basename (path )+ "." 122 ) 123try : 124with os .fdopen (handle ,"wb" )as file : 125pickle .dump ( 126 { 127"cwd" :os .getcwd (), 128"names" :payload , 129"definitions" :saved_definitions , 130 }, 131file , 132 ) 133os .replace (temporary ,path ) 134except BaseException : 135os .unlink (temporary ) 136raise 137except OSError as error : 138return {"error" :f"could not write saved state: { error } " } 139largest .sort (key = lambda item :item [1 ],reverse = True ) 140return { 141"saved" :sorted (set (payload )| {item ["name" ]for item in saved_definitions }), 142"defs" : [item ["name" ]for item in saved_definitions ], 143"skipped" :skipped , 144"largest" : [{"name" :name ,"bytes" :size }for name ,size in largest [:5 ]], 145"bytes" :total , 146"engine" :kind , 147 } 148 149 150def load_state ( 151path :str ,namespace :dict [str ,object ],definitions :dict [str ,str ] 152)-> dict [str ,object ]: 153"""Restore pickled values, then replay definitions in their saved order.""" 154try : 155with open (path ,"rb" )as file : 156saved = cast (dict [str ,object ],pickle .load (file )) 157except FileNotFoundError : 158return {"restored" : [],"defs" : [],"failed" : [],"error" :"no saved state" } 159except BaseException as error : 160return { 161"restored" : [], 162"defs" : [], 163"failed" : [], 164"error" :f"unreadable saved state: { type ( error ). __name__ } " , 165 } 166serialiser ,kind = cast ("Any" ,engine ()) 167restored :list [str ]= [] 168failed :list [dict [str ,str ]]= [] 169for name ,blob in cast (dict [str ,bytes ],saved .get ("names" , {})).items (): 170try : 171namespace [name ]= cast (object ,serialiser .loads (blob )) 172except BaseException as error : 173failed .append ( 174 {"name" :name ,"reason" :f" { type ( error ). __name__ } : { error } " [:200 ]} 175 ) 176else : 177restored .append (name ) 178defs :list [str ]= [] 179for item in cast (list [object ],saved .get ("definitions" , [])): 180if ( 181not isinstance (item ,dict ) 182or not isinstance (item .get ("name" ),str ) 183or not isinstance (item .get ("source" ),str ) 184 ): 185continue 186name ,source = item ["name" ],item ["source" ] 187try : 188exec (compile (source ,"<albedo:state>" ,"exec" ),namespace ) 189except BaseException as error : 190failed .append ( 191 {"name" :name ,"reason" :f" { type ( error ). __name__ } : { error } " [:200 ]} 192 ) 193else : 194if source not in definitions .values (): 195key = name 196while key in definitions : 197key += "#2" 198definitions [key ]= source 199if name not in restored : 200restored .append (name ) 201defs .append (name ) 202directory = saved .get ("cwd" ) 203if isinstance (directory ,str )and os .path .isdir (directory ): 204os .chdir (directory ) 205return { 206"restored" :sorted (set (restored )), 207"defs" :defs , 208"failed" :failed , 209"engine" :kind , 210 }