From e8373cf0ab0ede4c2c70499c345715d79db164f6 Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Fri, 16 Oct 2026 17:59:14 -0400 Subject: [PATCH] State is cached via a repo --- core/controllers/SessionStateController.py | 56 +++++++++++++++++- core/controllers/SystemStateController.py | 46 +++++++++++++-- core/infrastructure/global_state.py | 59 +++++++++++++++++++ .../global_state_coordinator.py | 27 +++++++++ core/services/profiles/profile_connections.py | 28 ++++----- core/utils/singleton.py | 25 ++++++++ 6 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 core/infrastructure/global_state.py create mode 100644 core/infrastructure/global_state_coordinator.py create mode 100644 core/utils/singleton.py diff --git a/core/controllers/SessionStateController.py b/core/controllers/SessionStateController.py index 72284f1..f1ee8e5 100644 --- a/core/controllers/SessionStateController.py +++ b/core/controllers/SessionStateController.py @@ -1,19 +1,26 @@ from core.models.session.SessionState import SessionState +from core.controllers.ProfileController import ProfileController +from core.infrastructure.global_state import ProfileStateRepository 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: - 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 @@ -28,3 +35,46 @@ class SessionStateController: @staticmethod def update_or_create(session_state): 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() diff --git a/core/controllers/SystemStateController.py b/core/controllers/SystemStateController.py index d459570..9e057e0 100644 --- a/core/controllers/SystemStateController.py +++ b/core/controllers/SystemStateController.py @@ -1,8 +1,10 @@ from core.models.system.SystemState import SystemState from core.models.enum_models import ProtocolChoice +# from core.controllers.ProfileController import ProfileController +from core.infrastructure.global_state import ProfileStateRepository + class SystemStateController: - @staticmethod def get(): return SystemState.get() @@ -13,17 +15,49 @@ class SystemStateController: @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 = SystemState(profile_id, firewalled, dns_set, protocol, process_id) current_state.save() + + # Sync to Repo + ProfileStateRepository().sync_system_state(current_state) return current_state @staticmethod def update_or_create(system_state): system_state.save() + # Sync to Repo + ProfileStateRepository().sync_system_state(system_state) @staticmethod 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() diff --git a/core/infrastructure/global_state.py b/core/infrastructure/global_state.py new file mode 100644 index 0000000..783ebb5 --- /dev/null +++ b/core/infrastructure/global_state.py @@ -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 + diff --git a/core/infrastructure/global_state_coordinator.py b/core/infrastructure/global_state_coordinator.py new file mode 100644 index 0000000..129a646 --- /dev/null +++ b/core/infrastructure/global_state_coordinator.py @@ -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) \ No newline at end of file diff --git a/core/services/profiles/profile_connections.py b/core/services/profiles/profile_connections.py index 9c4a4b9..069cb9d 100644 --- a/core/services/profiles/profile_connections.py +++ b/core/services/profiles/profile_connections.py @@ -1,4 +1,3 @@ -from core.models.enum_models import ProtocolChoice, valid_enum from core.controllers.ConnectionController import ConnectionController from core.controllers.ProfileController import ProfileController 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: 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: @@ -250,11 +250,20 @@ def disable_profile( else: raise ProfileDeactivationError('Unsupported protocol.') # ================= 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: profile_observer.notify('disabled', profile, dict( explicitly=explicitly, )) + + # return profile type for UI: + return Result(valid=True, data=profile.type) + except ConnectionTerminationError: raise ProfileDeactivationError('The profile could not be disabled.') except ValueError: @@ -313,16 +322,3 @@ def destroy_profile( profile_observer.notify('destroyed', 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) \ No newline at end of file diff --git a/core/utils/singleton.py b/core/utils/singleton.py new file mode 100644 index 0000000..ca6f038 --- /dev/null +++ b/core/utils/singleton.py @@ -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 +