okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
60047c7
main
1"""The kernel's half of the durable session layer. 2 3A detached kernel listens on a unix socket in its run directory. Whoever 4attaches (a bridge the daemon runs, locally today and over ssh later) presents 5the kernel's secret token; the newest attach wins and cuts the previous 6connection off. Every frame after the handshake travels as 7``{"seq": n, "ack": m, "frame": {...}}``: the kernel keeps what it sent until 8the daemon acknowledges it and resends the rest on the next attach, and it 9drops any daemon frame it has already seen, so a reconnect neither loses nor 10repeats a message. A kernel run over plain stdio (the remote plugin's targets) 11keeps the raw frames and ends with its pipe. 12""" 13 14from __future__import annotations 15 16import collections 17import hmac 18import json 19import os 20import socket 21import struct 22import threading 23import time 24from collections .abc import Callable 25from typing import Any ,BinaryIO 26 27PROTOCOL = 1 # framing version; hello fields describe the current bundle 28MAX_FRAME = 8 * 1024 * 1024 29SOCKET = "kernel.sock" 30ATTACH_TIMEOUT = 10.0 # seconds a fresh connection has to present its token 31OUTBOX_FRAMES = 4096 32OUTBOX_BYTES = 64 * 1024 * 1024 33# Frames whose newest copy supersedes every older unacknowledged one, by the 34# field naming what they describe. Everything else must be delivered. 35COALESCE = {"mirror" :"handle" ,"trace" :"id" ,"jobs" :None ,"cells" :None } 36 37 38def encode (value :object )-> bytes : 39return json .dumps (value ,ensure_ascii = True ,separators = ("," ,":" )).encode () 40 41 42def envelope (seq :int ,ack :int ,frame :bytes )-> bytes : 43return b'{"seq":%d,"ack":%d,"frame":%s}' % (seq ,ack ,frame ) 44 45 46def count_of (value :object )-> int | None : 47"""A frame's count: a non-negative integer, else None.""" 48if isinstance (value ,int )and not isinstance (value ,bool )and value >= 0 : 49return value 50return None 51 52 53def write_all (write :Callable [[Any ],int | None ],data :bytes )-> None : 54remaining = memoryview (data ) 55while remaining : 56written = write (remaining ) 57remaining = remaining [len (remaining )if written is None else written :] 58 59 60def write_frame (write :Callable [[Any ],int | None ],data :bytes )-> None : 61write_all (write ,struct .pack (">I" ,len (data ))+ data ) 62 63 64def read_exact (read :Callable [[int ],bytes ],size :int )-> bytes : 65data = bytearray () 66while len (data )< size : 67chunk = read (size - len (data )) 68if not chunk : 69raise EOFError () 70data .extend (chunk ) 71return bytes (data ) 72 73 74def read_frame (read :Callable [[int ],bytes ])-> object : 75size = struct .unpack (">I" ,read_exact (read ,4 ))[0 ] 76if size > MAX_FRAME : 77raise ValueError ("control frame too large" ) 78return json .loads (read_exact (read ,size )) 79 80 81class Outbox : 82"""Frames sent and not yet acknowledged, oldest first. 83 84Coalescing frames replace their older unacknowledged copy, so a flood of 85output mirrors costs one entry. Past the bound, the oldest coalescing 86frames go first; a must-deliver frame is dropped only when nothing else is 87left, and `dropped` counts every such loss for the next hello. 88""" 89 90def __init__ (self ,frames :int = OUTBOX_FRAMES ,size :int = OUTBOX_BYTES ): 91self .limit_frames = frames 92self .limit_bytes = size 93self .seq = 0 94self .size = 0 95self .dropped = 0 96self .entries :collections .OrderedDict [int ,tuple [bytes ,object ]]= ( 97collections .OrderedDict () 98 ) 99self .keys :dict [object ,int ]= {} 100 101def add (self ,frame :dict [str ,object ])-> tuple [int ,bytes ]: 102data = encode (frame ) 103self .seq += 1 104kind = frame .get ("type" ) 105key :object = None 106if isinstance (kind ,str )and kind in COALESCE : 107field = COALESCE [kind ] 108key = (kind ,frame .get (field )if field else None ) 109older = self .keys .pop (key ,None ) 110if older is not None : 111self ._remove (older ) 112self .entries [self .seq ]= (data ,key ) 113self .size += len (data ) 114if key is not None : 115self .keys [key ]= self .seq 116self ._bound () 117return self .seq ,data 118 119def ack (self ,upto :int )-> None : 120while self .entries : 121seq = next (iter (self .entries )) 122if seq > upto : 123return 124self ._remove (seq ) 125 126def pending (self )-> list [tuple [int ,bytes ]]: 127return [(seq ,data )for seq , (data ,_ )in self .entries .items ()] 128 129def _remove (self ,seq :int )-> None : 130entry = self .entries .pop (seq ,None ) 131if entry is None : 132return 133data ,key = entry 134self .size -= len (data ) 135if key is not None and self .keys .get (key )== seq : 136del self .keys [key ] 137 138def _bound (self )-> None : 139def over ()-> bool : 140return len (self .entries )> self .limit_frames or self .size > self .limit_bytes 141 142if not over (): 143return 144# `keys` indexes every coalescing entry (one per live handle or cell), 145# so shedding them oldest first never walks the whole outbox. 146while over ()and self .keys : 147self ._remove (min (self .keys .values ())) 148while over ()and len (self .entries )> 1 : 149self ._remove (next (iter (self .entries ))) 150self .dropped += 1 151 152 153class Inbound : 154"""The newest daemon frame applied; anything at or below it is a replay.""" 155 156def __init__ (self )-> None : 157self .last = 0 158 159def accept (self ,seq :int )-> bool : 160if seq <= self .last : 161return False 162self .last = seq 163return True 164 165 166class JobBook : 167"""Job groups this kernel still owns, read from the frames it sends. 168 169The same bookkeeping the daemon keeps: a started job's group is owned 170until a job frame proves it gone. A reattaching daemon learns the groups 171from the hello. The kernel stays alive past its grace while a job is live. 172""" 173 174def __init__ (self )-> None : 175self .started :dict [str ,dict [str ,object ]]= {} 176self .external = 0 177self .awaited_external = 0 178self .external_jobs :list [object ]= [] 179 180def observe (self ,frame :dict [str ,object ])-> None : 181kind ,id = frame .get ("type" ),frame .get ("id" ) 182if kind == "job_start" and isinstance (id ,str ): 183pgid = frame .get ("pgid" ) 184if isinstance (pgid ,int )and not isinstance (pgid ,bool )and pgid > 1 : 185self .started [id ]= frame 186elif kind == "job" and isinstance (id ,str ): 187cleanup = frame .get ("cleanup" ) 188if isinstance (cleanup ,dict )and cleanup .get ("gone" )is True : 189self .started .pop (id ,None ) 190elif kind == "jobs" : 191live = count_of (frame .get ("live" )) 192awaited = count_of (frame .get ("awaited" ,0 )) 193items = frame .get ("items" ) 194if live is not None and awaited is not None : 195self .external ,self .awaited_external = live ,awaited 196self .external_jobs = items if isinstance (items ,list )else [] 197 198def live (self )-> int : 199return len (self .started )+ self .external 200 201 202class StdioLink : 203"""Raw frames over the inherited stdin/stdout; the pipe closing ends us.""" 204 205def __init__ (self )-> None : 206self .control_in :BinaryIO = os .fdopen (os .dup (0 ),"rb" ,buffering = 0 ) 207self .control_out :BinaryIO = os .fdopen (os .dup (1 ),"wb" ,buffering = 0 ) 208self .lock = threading .Lock () 209self .jobs = JobBook () 210 211def send (self ,frame :dict [str ,object ])-> None : 212data = encode (frame ) 213with self .lock : 214self .jobs .observe (frame ) 215write_frame (self .control_out .write ,data ) 216 217def serve (self ,dispatch :Callable [[object ],None ])-> None : 218"""Deliver frames until the pipe closes; raises when it does.""" 219while True : 220dispatch (read_frame (self .control_in .read )) 221 222def attached (self )-> bool : 223return True 224 225 226class SocketLink : 227"""The session layer over a unix socket in the kernel's run directory.""" 228 229def __init__ ( 230self , 231run_dir :str , 232token :str , 233hello :Callable [[],dict [str ,object ]], 234* , 235bundle :str , 236grace :float , 237 )-> None : 238self .run_dir = run_dir 239self .token = token 240self .hello = hello 241self .bundle = bundle 242self .grace = grace 243self .lock = threading .RLock () 244self .outbox = Outbox () 245self .inbound = Inbound () 246self .jobs = JobBook () 247self .epoch = 0 248self .connection :socket .socket | None = None 249self .detached_at = time .monotonic () 250self .listener = socket .socket (socket .AF_UNIX ,socket .SOCK_STREAM ) 251# Bound relative to the run directory: sun_path holds ~104 bytes and a 252# temporary ALBEDO_HOME alone can use most of that. 253previous = os .getcwd () 254os .chdir (run_dir ) 255try : 256if os .path .exists (SOCKET ): 257os .unlink (SOCKET ) 258self .listener .bind (SOCKET ) 259os .chmod (SOCKET ,0o600 ) 260finally : 261os .chdir (previous ) 262self .listener .listen (4 ) 263 264def send (self ,frame :dict [str ,object ])-> None : 265with self .lock : 266self .jobs .observe (frame ) 267seq ,data = self .outbox .add (frame ) 268self ._write (envelope (seq ,self .inbound .last ,data )) 269 270def attached (self )-> bool : 271return self .connection is not None 272 273def idle_for (self )-> float : 274"""Seconds with nothing attached; zero while attached.""" 275if self .connection is not None : 276return 0.0 277return time .monotonic ()- self .detached_at 278 279def serve (self ,dispatch :Callable [[object ],None ])-> None : 280"""Accept attaches forever; each connection reads on its own thread.""" 281while True : 282connection ,_ = self .listener .accept () 283threading .Thread ( 284target = self ._session ,args = (connection ,dispatch ),daemon = True 285 ).start () 286 287def _write (self ,data :bytes )-> None : 288connection = self .connection 289if connection is None : 290return 291try : 292write_frame (connection .send ,data ) 293except OSError : 294self ._detach (connection ) 295 296def _detach (self ,connection :socket .socket )-> None : 297with self .lock : 298if self .connection is connection : 299self .connection = None 300self .detached_at = time .monotonic () 301try : 302connection .close () 303except OSError : 304pass 305 306def _attach (self ,connection :socket .socket )-> bool : 307connection .settimeout (ATTACH_TIMEOUT ) 308message = read_frame (connection .recv ) 309attach = message .get ("attach" )if isinstance (message ,dict )else None 310token = attach .get ("token" )if isinstance (attach ,dict )else None 311if not isinstance (attach ,dict )or not isinstance (token ,str ): 312write_frame (connection .send ,encode ({"refused" :"expected attach" })) 313return False 314if not hmac .compare_digest (token .encode (),self .token .encode ()): 315write_frame (connection .send ,encode ({"refused" :"wrong token" })) 316return False 317connection .settimeout (None ) 318grace = attach .get ("grace" ) 319ack = attach .get ("ack" ) 320with self .lock : 321if isinstance (grace ,int | float )and not isinstance (grace ,bool ): 322self .grace = max (float (grace ),0.0 ) 323previous ,self .connection = self .connection ,connection 324self .epoch += 1 325if previous is not None : 326try : 327previous .shutdown (socket .SHUT_RDWR ) 328except OSError : 329pass 330if isinstance (ack ,int )and not isinstance (ack ,bool ): 331self .outbox .ack (ack ) 332hello = { 333** self .hello (), 334"protocol" :PROTOCOL , 335"bundle" :self .bundle , 336"epoch" :self .epoch , 337"ack" :self .inbound .last , 338"jobs" :list (self .jobs .started .values ()), 339"external" :self .jobs .external , 340"awaited_external" :self .jobs .awaited_external , 341"external_jobs" :self .jobs .external_jobs , 342"dropped" :self .outbox .dropped , 343 } 344write_frame (connection .send ,encode ({"hello" :hello })) 345for seq ,data in self .outbox .pending (): 346write_frame (connection .send ,envelope (seq ,self .inbound .last ,data )) 347return True 348 349def _session ( 350self ,connection :socket .socket ,dispatch :Callable [[object ],None ] 351 )-> None : 352try : 353if not self ._attach (connection ): 354return 355while True : 356message = read_frame (connection .recv ) 357if not isinstance (message ,dict ): 358raise ValueError ("expected an envelope" ) 359ack = message .get ("ack" ) 360seq = message .get ("seq" ) 361with self .lock : 362if isinstance (ack ,int )and not isinstance (ack ,bool ): 363self .outbox .ack (ack ) 364fresh = isinstance (seq ,int )and self .inbound .accept (seq ) 365if fresh : 366dispatch (message .get ("frame" )) 367if isinstance (seq ,int ): 368with self .lock : 369if self .connection is connection : 370self ._write (encode ({"ack" :self .inbound .last })) 371except (OSError ,EOFError ,ValueError ): 372pass 373finally : 374self ._detach (connection )