diff --git a/kernel_ai/services/network.py b/kernel_ai/services/network.py index 7c61c18..9d33c00 100644 --- a/kernel_ai/services/network.py +++ b/kernel_ai/services/network.py @@ -12,10 +12,22 @@ import psutil from kernel_ai.services.infra_utils import resolve_binary -from kernel_ai.state import NETWORK_STACK_PREV, TRACEROUTE_CACHE, TRACEROUTE_CACHE_TTL_SECONDS logger = logging.getLogger(__name__) +_NETWORK_STACK_PREV_DEFAULT = { + "timestamp": None, + "tcpext_retrans": None, + "ip_in": None, + "ip_out": None, + "ip_discards": None, + "iface_rx": None, + "iface_tx": None, + "iface_drops": None, +} +_TRACEROUTE_CACHE_DEFAULT = {} +_TRACEROUTE_CACHE_TTL_SECONDS_DEFAULT = 60 + def get_active_connections(): """Get active network connections.""" @@ -200,7 +212,7 @@ def _get_ss_tcp_metrics(): def get_network_stack_realtime(network_stack_prev=None): - network_stack_prev = NETWORK_STACK_PREV if network_stack_prev is None else network_stack_prev + network_stack_prev = _NETWORK_STACK_PREV_DEFAULT if network_stack_prev is None else network_stack_prev now = time.time() iface = _get_default_iface() pernic = psutil.net_io_counters(pernic=True) @@ -377,8 +389,8 @@ def get_route_hint(remote_ip): def get_traceroute_info(remote_ip, max_hops=8, traceroute_cache=None, cache_ttl_seconds=None): - traceroute_cache = TRACEROUTE_CACHE if traceroute_cache is None else traceroute_cache - cache_ttl_seconds = TRACEROUTE_CACHE_TTL_SECONDS if cache_ttl_seconds is None else cache_ttl_seconds + traceroute_cache = _TRACEROUTE_CACHE_DEFAULT if traceroute_cache is None else traceroute_cache + cache_ttl_seconds = _TRACEROUTE_CACHE_TTL_SECONDS_DEFAULT if cache_ttl_seconds is None else cache_ttl_seconds try: target_ip = ipaddress.ip_address(remote_ip) if target_ip.is_loopback or target_ip.is_unspecified: diff --git a/kernel_ai/services/system_view.py b/kernel_ai/services/system_view.py index 0dc0d4d..154fc65 100644 --- a/kernel_ai/services/system_view.py +++ b/kernel_ai/services/system_view.py @@ -11,13 +11,17 @@ import psutil from kernel_ai.collectors import proc_fs as _proc_fs -from kernel_ai.state import FILESYSTEM_PREV logger = logging.getLogger(__name__) +_FILESYSTEM_PREV_DEFAULT = { + "timestamp": None, + "write_bytes": None, +} + def get_filesystem_blocks(filesystem_prev=None): - filesystem_prev = FILESYSTEM_PREV if filesystem_prev is None else filesystem_prev + filesystem_prev = _FILESYSTEM_PREV_DEFAULT if filesystem_prev is None else filesystem_prev now = time.time() try: usage = psutil.disk_usage("/") diff --git a/kernel_ai/state.py b/kernel_ai/state.py index e8e4d94..edf56e4 100644 --- a/kernel_ai/state.py +++ b/kernel_ai/state.py @@ -1,6 +1,12 @@ -"""Process-wide mutable state (caches, deltas for realtime metrics).""" +"""Process-wide mutable state and per-app state container.""" + +from __future__ import annotations + import os +from copy import deepcopy +from dataclasses import dataclass from threading import Lock +from typing import Any TRACEROUTE_CACHE = {} TRACEROUTE_CACHE_TTL_SECONDS = 60 @@ -48,3 +54,66 @@ } FRONTEND_LOG_WRITE_LOCK = Lock() FRONTEND_LOG_FILE = os.getenv("FRONTEND_LOG_FILE", "/opt/ring0/kernel-ai/logs/frontend-events.jsonl") + + +@dataclass +class RuntimeState: + traceroute_cache: dict + traceroute_cache_ttl_seconds: int + network_stack_prev: dict + devices_prev: dict + filesystem_prev: dict + crypto_prev: dict + entropy_prev: dict + exec_context_prev: dict + security_prev: dict + frontend_log_write_lock: Lock + frontend_log_file: str + + +def create_state_container(frontend_log_file: str | None = None) -> RuntimeState: + """Create isolated mutable runtime state for one Flask app instance.""" + return RuntimeState( + traceroute_cache={}, + traceroute_cache_ttl_seconds=TRACEROUTE_CACHE_TTL_SECONDS, + network_stack_prev=deepcopy(NETWORK_STACK_PREV), + devices_prev=deepcopy(DEVICES_PREV), + filesystem_prev=deepcopy(FILESYSTEM_PREV), + crypto_prev=deepcopy(CRYPTO_PREV), + entropy_prev=deepcopy(ENTROPY_PREV), + exec_context_prev=deepcopy(EXEC_CONTEXT_PREV), + security_prev=deepcopy(SECURITY_PREV), + frontend_log_write_lock=Lock(), + frontend_log_file=frontend_log_file or FRONTEND_LOG_FILE, + ) + + +_LEGACY_STATE = RuntimeState( + traceroute_cache=TRACEROUTE_CACHE, + traceroute_cache_ttl_seconds=TRACEROUTE_CACHE_TTL_SECONDS, + network_stack_prev=NETWORK_STACK_PREV, + devices_prev=DEVICES_PREV, + filesystem_prev=FILESYSTEM_PREV, + crypto_prev=CRYPTO_PREV, + entropy_prev=ENTROPY_PREV, + exec_context_prev=EXEC_CONTEXT_PREV, + security_prev=SECURITY_PREV, + frontend_log_write_lock=FRONTEND_LOG_WRITE_LOCK, + frontend_log_file=FRONTEND_LOG_FILE, +) + + +def attach_state_container(app: Any, state: RuntimeState | None = None) -> RuntimeState: + """Attach runtime state container to Flask app extensions.""" + runtime_state = state or create_state_container() + app.extensions["kernel_ai_state"] = runtime_state + return runtime_state + + +def get_state_container(app: Any | None) -> RuntimeState: + """Get app state when available, fallback to process-global legacy state.""" + if app is not None: + runtime_state = getattr(app, "extensions", {}).get("kernel_ai_state") + if runtime_state is not None: + return runtime_state + return _LEGACY_STATE diff --git a/kernel_ai/webapp.py b/kernel_ai/webapp.py index 8b01775..70d84b6 100644 --- a/kernel_ai/webapp.py +++ b/kernel_ai/webapp.py @@ -11,6 +11,7 @@ from kernel_ai.hooks import register_hooks from kernel_ai.http.register import register_http_routes from kernel_ai.prometheus_setup import init_prometheus +from kernel_ai.state import attach_state_container def _ensure_prometheus_mpdir(): # Gunicorn -w N: set PROMETHEUS_MULTIPROC_DIR before workers import this module. @@ -39,6 +40,7 @@ def create_app(): template_folder=os.path.join(root, "templates"), ) app.config.from_object(Config) + attach_state_container(app) init_prometheus(app) register_hooks(app) register_http_routes(app)