State is cached via a repo

This commit is contained in:
SimplifiedPrivacy 2026-10-16 17:59:14 -04:00
parent 469268651b
commit e8373cf0ab
6 changed files with 216 additions and 25 deletions

View file

@ -1,19 +1,26 @@
from core.models.session.SessionState import SessionState from core.models.session.SessionState import SessionState
from core.controllers.ProfileController import ProfileController
from core.infrastructure.global_state import ProfileStateRepository
class SessionStateController: class SessionStateController:
@staticmethod @staticmethod
def get(id: int): def get(id: int):
return SessionState.find_by_id(id) return SessionState.find_by_id(id)
@staticmethod @staticmethod
def get_or_new(id: int): def get_or_new(id: int):
session_state = SessionStateController.get(id) session_state = SessionStateController.get(id)
if session_state is None: if session_state is None:
return SessionState(id) # Update the Singleton Repo
repo = ProfileStateRepository()
repo.sync_session_added(id)
# Create and save the session
session_state = SessionState(id)
session_state.save()
return session_state
return session_state return session_state
@ -28,3 +35,46 @@ class SessionStateController:
@staticmethod @staticmethod
def update_or_create(session_state): def update_or_create(session_state):
session_state.save() session_state.save()
# Ensure repo knows this session exists
ProfileStateRepository().sync_session_added(session_state.id)
@staticmethod
def remove(profile_id: int):
SessionState.dissolve(profile_id)
ProfileStateRepository().sync_session_removed(profile_id)
# class SessionStateController:
# @staticmethod
# def get(id: int):
# return SessionState.find_by_id(id)
# @staticmethod
# def get_or_new(id: int):
# session_state = SessionStateController.get(id)
# if session_state is None:
# # update global state:
# global_state = GlobalState()
# if id not in global_state.session_profile_ids: # sanity check.
# global_state.session_profile_ids.append(id)
# # create new:
# return SessionState(id)
# return session_state
# @staticmethod
# def exists(id: int):
# return SessionState.exists(id)
# @staticmethod
# def all():
# return SessionState.all()
# @staticmethod
# def update_or_create(session_state):
# session_state.save()

View file

@ -1,8 +1,10 @@
from core.models.system.SystemState import SystemState from core.models.system.SystemState import SystemState
from core.models.enum_models import ProtocolChoice from core.models.enum_models import ProtocolChoice
# from core.controllers.ProfileController import ProfileController
from core.infrastructure.global_state import ProfileStateRepository
class SystemStateController: class SystemStateController:
@staticmethod @staticmethod
def get(): def get():
return SystemState.get() return SystemState.get()
@ -13,17 +15,49 @@ class SystemStateController:
@staticmethod @staticmethod
def create(profile_id: int, firewalled: bool, dns_set: bool, protocol: ProtocolChoice, process_id: int = None) -> SystemState: def create(profile_id: int, firewalled: bool, dns_set: bool, protocol: ProtocolChoice, process_id: int = None) -> SystemState:
if process_id: current_state = SystemState(profile_id, firewalled, dns_set, protocol, process_id)
current_state = SystemState(profile_id, firewalled, dns_set, protocol, process_id)
else:
current_state = SystemState(profile_id, firewalled, dns_set, protocol)
current_state.save() current_state.save()
# Sync to Repo
ProfileStateRepository().sync_system_state(current_state)
return current_state return current_state
@staticmethod @staticmethod
def update_or_create(system_state): def update_or_create(system_state):
system_state.save() system_state.save()
# Sync to Repo
ProfileStateRepository().sync_system_state(system_state)
@staticmethod @staticmethod
def dissolve(): def dissolve():
return SystemState.dissolve() res = SystemState.dissolve()
# Sync to Repo: clear system ID
ProfileStateRepository().global_state.systemwide_profile_id = None
return res
# class SystemStateController:
# @staticmethod
# def get():
# return SystemState.get()
# @staticmethod
# def exists():
# return SystemState.exists()
# @staticmethod
# def create(profile_id: int, firewalled: bool, dns_set: bool, protocol: ProtocolChoice, process_id: int = None) -> SystemState:
# if process_id:
# current_state = SystemState(profile_id, firewalled, dns_set, protocol, process_id)
# else:
# current_state = SystemState(profile_id, firewalled, dns_set, protocol)
# current_state.save()
# return current_state
# @staticmethod
# def update_or_create(system_state):
# system_state.save()
# @staticmethod
# def dissolve():
# return SystemState.dissolve()

View file

@ -0,0 +1,59 @@
from core.models.system.SystemState import SystemState
from core.models.session.SessionState import SessionState
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class GlobalState:
systemwide_active: bool = False
firewalled: bool = False
dns_set: bool = False
systemwide_profile_id: Optional[int] = None
session_profile_ids: list[int] = field(default_factory=list)
verified: bool = False
class ProfileStateRepository:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(ProfileStateRepository, cls).__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
self.global_state = GlobalState()
# Initial load: Sync memory with whatever is currently on disk
sys = SystemState.get()
if sys:
self.global_state.systemwide_profile_id = sys.profile_id
self.global_state.firewalled = sys.firewalled
self.global_state.dns_set = sys.dns_set
# Load existing session IDs from disk into the list
self.global_state.session_profile_ids = [s.id for s in SessionState.all()]
def sync_system_state(self, state: SystemState):
"""Called by SystemStateController to update memory cache."""
self.global_state.systemwide_profile_id = state.profile_id
self.global_state.firewalled = state.firewalled
self.global_state.dns_set = state.dns_set
def sync_session_added(self, profile_id: int):
"""Called by SessionStateController to update memory cache."""
if profile_id not in self.global_state.session_profile_ids:
self.global_state.session_profile_ids.append(profile_id)
def sync_session_removed(self, profile_id: int):
"""Called by SessionStateController to update memory cache."""
if profile_id in self.global_state.session_profile_ids:
self.global_state.session_profile_ids.remove(profile_id)
def is_profile_active(self, profile_id: int) -> bool:
# Check system profile
if self.global_state.systemwide_active and self.global_state.systemwide_profile_id == profile_id:
return True
# Check session profiles
return profile_id in self.global_state.session_profile_ids

View file

@ -0,0 +1,27 @@
from core.controllers.SystemStateController import SystemStateController
from core.controllers.SessionStateController import SessionStateController
from core.models.Result import Result, ResultError
from core.errors.logger import logger
def evaluate_state_on_startup() -> Result:
"""When the app first starts, evaluate pre-existing state"""
#-----------------------
# SYSTEM-WIDE
#-----------------------
systemwide_active = SystemStateController.get()
if systemwide_active:
error_msg = "A pre-existing systemwide state is active."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.STATE_CONFLICT, message=error_msg)
#-----------------------
# SESSION
#-----------------------
active_session_profiles = SessionStateController.all()
if active_session_profiles:
for each_profile in active_session_profiles:
logger.info(f"Removing this profile {active_session_profiles} from session since it's a fresh restart.")
SessionStateController.remove(each_profile.id)
return Result(valid=True)

View file

@ -1,4 +1,3 @@
from core.models.enum_models import ProtocolChoice, valid_enum
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection from core.services.networking.systemwide.systemwide_wireguard import terminate_system_connection
@ -206,7 +205,8 @@ def disable_profile(
for port_number in session_state.network_port_numbers.tor: for port_number in session_state.network_port_numbers.tor:
ConnectionController.terminate_tor_session_connection(port_number) ConnectionController.terminate_tor_session_connection(port_number)
session_state.dissolve(session_state.id) # session_state.dissolve(session_state.id)
SessionStateController.remove(profile.id)
if profile_observer is not None: if profile_observer is not None:
@ -250,11 +250,20 @@ def disable_profile(
else: else:
raise ProfileDeactivationError('Unsupported protocol.') raise ProfileDeactivationError('Unsupported protocol.')
# ================= UPDATE UI ================ # ================= UPDATE UI ================
# if it made it this far, it worked in theory. # if it made it this far, it worked (in theory).
# wipe state:
SystemStateController.remove(profile.id)
# update ui:
if profile_observer is not None: if profile_observer is not None:
profile_observer.notify('disabled', profile, dict( profile_observer.notify('disabled', profile, dict(
explicitly=explicitly, explicitly=explicitly,
)) ))
# return profile type for UI:
return Result(valid=True, data=profile.type)
except ConnectionTerminationError: except ConnectionTerminationError:
raise ProfileDeactivationError('The profile could not be disabled.') raise ProfileDeactivationError('The profile could not be disabled.')
except ValueError: except ValueError:
@ -313,16 +322,3 @@ def destroy_profile(
profile_observer.notify('destroyed', profile) profile_observer.notify('destroyed', profile)
return Result(valid=True, message="Deleted Profile") return Result(valid=True, message="Deleted Profile")
def get_protocol_enum(profile_id: int) -> ProtocolChoice|bool:
"""Return the Enum for a given Profile's Protocol Choice"""
profile = ProfileController.get(profile_id)
protocol_string = profile.connection.code
if not valid_enum(protocol_string, ProtocolChoice):
return False
else:
return ProtocolChoice(protocol_string)

25
core/utils/singleton.py Normal file
View file

@ -0,0 +1,25 @@
from functools import wraps
def singleton(cls):
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
instance_id = kwargs.get('instance_id')
# If instance_id provided, use localized mode (one instance per ID)
# Otherwise, use global mode (single instance)
key = (cls, instance_id) if instance_id else cls
if key not in instances:
print(f"Creating new instance with key: {key}")
instances[key] = cls(*args, **kwargs)
else:
print(f"Reusing existing instance with key: {key}")
# either way:
return instances[key]
return get_instance