diff --git a/core/controllers/ProfileController.py b/core/controllers/ProfileController.py index 1a22185..4024347 100644 --- a/core/controllers/ProfileController.py +++ b/core/controllers/ProfileController.py @@ -5,6 +5,7 @@ from core.errors.logger import logger from core.Errors import InvalidSubscriptionError, MissingSubscriptionError from core.models.BaseProfile import BaseProfile as Profile +from core.models.BaseProfile import ProfileType from core.models.Subscription import Subscription from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile @@ -69,6 +70,17 @@ class ProfileController: ProfileController._repo.invalidate(profile_id=profile_id) + @staticmethod + def what_type_is_profile(profile_id: int) -> ProfileType: + all_data = ProfileController._repo.get_all() + given_profile = all_data[profile_id] + return given_profile.type + + # print(f"all_data[{profile_id}] is {all_data[profile_id]}") + + + + # @staticmethod # def attach_subscription(profile: Union[SessionProfile, SystemProfile], subscription: Subscription): diff --git a/core/controllers/SessionStateController.py b/core/controllers/SessionStateController.py index 6f49a3f..79a409c 100644 --- a/core/controllers/SessionStateController.py +++ b/core/controllers/SessionStateController.py @@ -1,6 +1,7 @@ from core.models.session.SessionState import SessionState from core.controllers.ProfileController import ProfileController from core.infrastructure.global_state import ProfileStateRepository +from core.observers.Notifications import Notifications, EventType class SessionStateController: @@ -34,11 +35,25 @@ class SessionStateController: @staticmethod def update_or_create(session_state): + # save to model: session_state.save() + + # get profile id: + profile_id = session_state.id + # Ensure repo knows this session exists - ProfileStateRepository().sync_session_added(session_state.id) + ProfileStateRepository().sync_session_added(profile_id) + + # notify UI: + Notifications.emit(EventType.SESSION_PROFILE_ENABLE, profile_id) @staticmethod def remove(profile_id: int): + # save to model: SessionState.dissolve(profile_id) + + # cache: ProfileStateRepository().sync_session_removed(profile_id) + + # notify UI: + Notifications.emit(EventType.SESSION_PROFILE_DISABLE, profile_id) diff --git a/core/controllers/SystemStateController.py b/core/controllers/SystemStateController.py index 808848b..4eb8247 100644 --- a/core/controllers/SystemStateController.py +++ b/core/controllers/SystemStateController.py @@ -2,7 +2,8 @@ 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 - +from core.observers.Notifications import Notifications, EventType +from core.errors.logger import logger class SystemStateController: @staticmethod @@ -15,23 +16,64 @@ class SystemStateController: @staticmethod def create(profile_id: int, firewalled: bool, dns_set: bool, protocol: ProtocolChoice, process_id: int = None) -> SystemState: + + # model on disk current_state = SystemState(profile_id, firewalled, dns_set, protocol, process_id) current_state.save() - # Sync to Repo + # cache (Sync to Repo) ProfileStateRepository().sync_system_state(current_state) + return current_state @staticmethod def update_or_create(system_state): + """Update the model & cache for a systemwide connection""" + + # model system_state.save() - # Sync to Repo + + # cache (Sync to Repo) ProfileStateRepository().sync_system_state(system_state) + # notify is done separately because this function can be used to modify state in the event of firewall failure. + @staticmethod def dissolve(): - res = SystemState.dissolve() - # Sync to Repo: clear system ID - ProfileStateRepository().global_state.systemwide_profile_id = None - return res + # get singleton: + state_repo = ProfileStateRepository() + # extract current active id: + profile_id = state_repo.global_state.systemwide_profile_id + + # model wipes data on disk: + SystemState.dissolve() + + # Wipe the Repo's Cache: + state_repo.turn_off_system_connection() + + # notify UI with that past id: + Notifications.emit(EventType.SYSTEM_DISABLE, profile_id) + + +# old version: +# ProfileStateRepository().global_state.systemwide_profile_id = None + + +# @staticmethod +# def notify_on(): +# """Notify the notifications that systemwide state shows on""" +# # get singleton: +# state_repo = ProfileStateRepository() + +# # extract current data: +# profile_id = state_repo.global_state.systemwide_profile_id +# systemwide_active = state_repo.global_state.systemwide_active + +# # double check, confirm: +# if systemwide_active and systemwide_active: +# # notify UI: +# Notifications.emit(EventType.SYSTEM_ENABLE, profile_id) +# else: +# logger.error("Critical Error, notify_on was called out of place.") +# Notifications.emit(EventType.SYSTEM_DISABLE, profile_id) diff --git a/core/errors/translate_error.py b/core/errors/translate_error.py index 6f0c85b..4e5cdc5 100644 --- a/core/errors/translate_error.py +++ b/core/errors/translate_error.py @@ -6,6 +6,7 @@ from core.Errors import ( InvalidSubscriptionError, MissingSubscriptionError, ProfileActivationError, ProfileModificationError, ProfileStateConflictError, UnsupportedApplicationVersionError, ) +from core.utils.type_checker import type_checker from typing import Type, Optional @@ -17,7 +18,7 @@ ERROR_TO_UI_MESSAGE = { ProfileModificationError: "WireGuard configuration could not be attached.", } - +@type_checker def translate_error(error: Type[Exception], profile_id: int) -> Result: """Translate legacy core errors into actions and/or messages via Result Objects.""" logger.error(str(error)) @@ -66,7 +67,7 @@ def translate_error(error: Type[Exception], profile_id: int) -> Result: return Result(valid=False, error_type=ResultError.UNKNOWN, message="An unknown error occured.") - +@type_checker def get_ignored_exceptions(ignore_state_conflict: Optional[bool] = False): ignored_as_list = [] endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled() diff --git a/core/infrastructure/global_state.py b/core/infrastructure/global_state.py index 783ebb5..a8e9fa1 100644 --- a/core/infrastructure/global_state.py +++ b/core/infrastructure/global_state.py @@ -34,11 +34,22 @@ class ProfileStateRepository: # Load existing session IDs from disk into the list self.global_state.session_profile_ids = [s.id for s in SessionState.all()] + def turn_off_system_connection(self): + """Clear the system-wide memory cache.""" + self.global_state.systemwide_profile_id = None + self.global_state.firewalled = False + self.global_state.dns_set = False + self.global_state.systemwide_active = False + + 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 + if state.profile_id: + self.global_state.systemwide_active = True + def sync_session_added(self, profile_id: int): """Called by SessionStateController to update memory cache.""" diff --git a/core/infrastructure/global_state_coordinator.py b/core/infrastructure/global_state_coordinator.py index 129a646..64d4a61 100644 --- a/core/infrastructure/global_state_coordinator.py +++ b/core/infrastructure/global_state_coordinator.py @@ -1,8 +1,41 @@ +from core.infrastructure.global_state import ProfileStateRepository 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 is_systemwide_on() -> bool: + """Returns a boolean if systemwide is connected. True is Yes, False is No""" + state_repo = ProfileStateRepository() + return state_repo.global_state.systemwide_active + +def connected_system_profile_id() -> int|None: + """Returns a single number of the connected system profile""" + state_repo = ProfileStateRepository() + print(f"state_repo is {state_repo.global_state}") + return state_repo.global_state.systemwide_profile_id + +def connected_session_profile_ids() -> list|None: + """Returns a list of the currently connected session profile IDs""" + state_repo = ProfileStateRepository() + return state_repo.global_state.session_profile_ids + + +def is_connected(profile_id: int) -> bool: + """Returns a boolean if a given profile is connected""" + + # system-wide: + systemwide_profile_id = connected_system_profile_id() + if profile_id == systemwide_profile_id: + return True + + # session: + state_repo = ProfileStateRepository() + return state_repo.is_profile_active(profile_id) + + def evaluate_state_on_startup() -> Result: """When the app first starts, evaluate pre-existing state""" @@ -24,4 +57,6 @@ def evaluate_state_on_startup() -> Result: 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 + return Result(valid=True) + + diff --git a/core/observers/ClientObserver.py b/core/observers/ClientObserver.py index bb28bc2..832b011 100644 --- a/core/observers/ClientObserver.py +++ b/core/observers/ClientObserver.py @@ -4,9 +4,9 @@ from essentials.observers.BaseObserver import BaseObserver class ClientObserver(BaseObserver): def __init__(self): - - self.on_synchronizing = [] - self.on_synchronized = [] - self.on_updating = [] - self.on_update_progressing = [] - self.on_updated = [] + pass + # self.on_synchronizing = [] + # self.on_synchronized = [] + # self.on_updating = [] + # self.on_update_progressing = [] + # self.on_updated = [] diff --git a/core/observers/EventBus.py b/core/observers/EventBus.py new file mode 100644 index 0000000..b9aa000 --- /dev/null +++ b/core/observers/EventBus.py @@ -0,0 +1,19 @@ + +class EventBus: + _subscribers = {} + + @classmethod + def subscribe(cls, event_type, callback): + if event_type not in cls._subscribers: + cls._subscribers[event_type] = [] + cls._subscribers[event_type].append(callback) + + @classmethod + def emit(cls, event_type, data=None): + for callback in cls._subscribers.get(event_type, []): + callback(data) + + @classmethod + def unsubscribe(cls, event_type, callback): + if event_type in cls._subscribers: + cls._subscribers[event_type].remove(callback) diff --git a/core/observers/Notifications.py b/core/observers/Notifications.py new file mode 100644 index 0000000..101daaf --- /dev/null +++ b/core/observers/Notifications.py @@ -0,0 +1,28 @@ + +from enum import Enum, auto + +class EventType(Enum): + SESSION_PROFILE_ENABLE = auto() + SESSION_PROFILE_DISABLE = auto() + SYSTEM_ENABLE = auto() + SYSTEM_DISABLE = auto() + PROFILE_CREATED = auto() + PROFILE_DELETED = auto() + +class Notifications: + _subscribers = {} + + @classmethod + def subscribe(cls, event_type: EventType, callback): + if event_type not in cls._subscribers: + cls._subscribers[event_type] = [] + cls._subscribers[event_type].append(callback) + + @classmethod + def emit(cls, event_type: EventType, data=None): + for callback in cls._subscribers.get(event_type, []): + callback(data) + + + + diff --git a/core/observers/ProfileObserver.py b/core/observers/ProfileObserver.py index e9c62f7..15ab4af 100644 --- a/core/observers/ProfileObserver.py +++ b/core/observers/ProfileObserver.py @@ -4,9 +4,9 @@ from essentials.observers.BaseObserver import BaseObserver class ProfileObserver(BaseObserver): def __init__(self): - - self.on_created = [] - self.on_updated = [] - self.on_destroyed = [] - self.on_disabled = [] - self.on_enabled = [] + pass + # self.on_created = [] + # self.on_updated = [] + # self.on_destroyed = [] + # self.on_disabled = [] + # self.on_enabled = [] diff --git a/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py b/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py index aac91f4..173847f 100644 --- a/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py +++ b/core/services/networking/systemwide/encrypted_proxy/singbox_runner.py @@ -11,6 +11,7 @@ from core.services.networking.general_connection_tools import ip from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file from core.utils.search_tools import search_for_phrase from core.utils.basic_operations.does_file_exist import does_file_exist +from core.observers.Notifications import Notifications, EventType from core.models.Result import Result, ResultError from core.errors.logger import logger @@ -348,6 +349,7 @@ def start_singbox( connection_observer.notify("connecting", "DNS Changed.") # raises error if not okay. + Notifications.emit(EventType.SYSTEM_ENABLE, profile_id) # ============= CONCLUSION ============= return Result(valid=True, data=process_id) diff --git a/core/services/networking/systemwide/systemwide_wireguard.py b/core/services/networking/systemwide/systemwide_wireguard.py index 91e5864..c604724 100644 --- a/core/services/networking/systemwide/systemwide_wireguard.py +++ b/core/services/networking/systemwide/systemwide_wireguard.py @@ -4,6 +4,7 @@ from core.services.networking.general_connection_tools.testing_evaluating import from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint from core.models.Result import Result, ResultError from core.services.networking.systemwide.systemwide_utils import extract_wg_interface_name, check_system_prereqs +from core.observers.Notifications import Notifications, EventType # from core.services.networking.general_connection_tools.config_tools import extract_endpoint_ip, extract_dns from core.services.networking.systemwide import killswitch @@ -211,6 +212,7 @@ def __establish_system_connection( current_state.dns_set = did_dns_work SystemStateController.update_or_create(current_state) + Notifications.emit(EventType.SYSTEM_ENABLE, profile_id) return Result(valid=True, data=current_state)