# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio from collections.abc import Callable from typing import Any try: from admin_console.core.config import PAUSE_FILE except ImportError: from apps.admin_console.core.config import PAUSE_FILE class ServerState: """Encapsulates all runtime states of the debug server.""" def __init__(self): self.ipc_subscribers: list[Callable[[str, Any], None]] = [] self.ipc_server: asyncio.Server | None = None self.ipc_serve_task: asyncio.Task | None = None self.ipc_port: int | None = None self.is_shutting_down: bool = False self.current_process: asyncio.subprocess.Process | None = None self.current_goal: str | None = None self.current_profile: str | None = None self.active_connections: dict[str, dict[str, Any]] = {} self.active_session_id: str | None = None self.was_stopped_manually: bool = False self.cancelled_session_ids: set[str] = set() # Unified single source of truth for task queue self.queue_items: list[dict[str, Any]] = [] self._wake_event: asyncio.Event | None = None self._shutdown_event: asyncio.Event | None = None self.worker_task: asyncio.Task | None = None @property def wake_event(self) -> asyncio.Event: try: loop = asyncio.get_running_loop() except RuntimeError: loop = None if self._wake_event is None or ( loop and getattr(self._wake_event, "_loop", None) is not None and self._wake_event._loop != loop ): self._wake_event = asyncio.Event() return self._wake_event @property def shutdown_event(self) -> asyncio.Event: """Event set as soon as the HTTP server receives a shutdown signal.""" try: loop = asyncio.get_running_loop() except RuntimeError: loop = None if self._shutdown_event is None or ( loop and getattr(self._shutdown_event, "_loop", None) is not None and self._shutdown_event._loop != loop ): self._shutdown_event = asyncio.Event() return self._shutdown_event @property def task_queue(self) -> list[dict[str, Any]]: """Backward compatibility alias for queue_items.""" return self.queue_items @property def queue_tasks(self) -> list[dict[str, Any]]: """Returns all currently pending tasks in the queue.""" return [t for t in self.queue_items if isinstance(t, dict) and t.get("status") == "pending"] @queue_tasks.setter def queue_tasks(self, val: list[dict[str, Any]]): # Compatibility setter self.queue_items = list(val) @property def queue_goals(self) -> list[str]: """Backward compatibility helper for queue goals.""" return [t.get("goal", "") for t in self.queue_tasks] @queue_goals.setter def queue_goals(self, val: list[Any]): pass @property def is_running(self) -> bool: has_proc = False if self.current_process is not None: if self.current_process.returncode is not None: has_proc = False self.current_process = None else: pid = getattr(self.current_process, "pid", None) if pid: try: import os os.kill(pid, 0) has_proc = True except (ProcessLookupError, PermissionError, OSError): has_proc = False self.current_process = None else: has_proc = False self.current_process = None has_running_item = any( isinstance(t, dict) and t.get("status") == "running" for t in self.queue_items ) has_live_connection = False for sid, conn in list(self.active_connections.items()): c_pid = conn.get("pid") if c_pid: try: import os os.kill(c_pid, 0) has_live_connection = True except (ProcessLookupError, PermissionError, OSError): self.active_connections.pop(sid, None) if not has_proc and not has_running_item and not has_live_connection: self.active_session_id = None self.current_process = None return False return True @property def is_paused(self) -> bool: return PAUSE_FILE.exists() @property def paused_error(self) -> str | None: """Return the persisted pause reason for clients that missed the SSE event.""" if not PAUSE_FILE.exists(): return None try: message = PAUSE_FILE.read_text(encoding="utf-8", errors="replace").strip() except OSError: return "AI model request failed. The task is paused." if message.startswith("LLM Error: "): message = message[len("LLM Error: ") :] return message or "AI model request failed. The task is paused." def add_subscriber(self, callback: Callable[[str, Any], None]): if callback not in self.ipc_subscribers: self.ipc_subscribers.append(callback) def remove_subscriber(self, callback: Callable[[str, Any], None]): if callback in self.ipc_subscribers: self.ipc_subscribers.remove(callback) def clear_queue(self): """Clears all pending items from the task queue.""" self.queue_items = [t for t in self.queue_items if t.get("status") == "running"] if self._wake_event: self._wake_event.set() # Global shared instance state = ServerState()