ApplicationController transitioned to the HTTPx Client system, instead of using requests library proxy for Tor. Also the GUI's observers now do strings to be more neutral to what object type is being downloaded (application_version objects vs dependency).

This commit is contained in:
SimplifiedPrivacy 2026-08-12 14:19:55 -04:00
parent b84bb2eefa
commit 01bd5f8d3f
4 changed files with 78 additions and 32 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ prototype_client.py
.venv
__pycache__
dist
.mypy_cache

View file

@ -3,6 +3,9 @@
# Singbox Setup
### Aug 12, 2026
Prepared Singbox setup modules, which includes installation, download, move to sudo folder, and sudo setup scripts. Added a `Dependency` model, endpoint, and the ability to sync that model. (Related note: Server-side prepared the endpoint, and stocked with real data.) And also the sync service modules were adjusted to handle new data types more smoothly, before they had errors. As part of that sync flow change, the CachedSync metadata model was transitioned to ints instead of strings, with default 0 values. This should in theory migrate all clients without further changes needed.
### Additional Changes:
ApplicationController transitioned to the HTTPx Client system, instead of using requests library proxy for Tor. Also the GUI's observers now do strings to be more neutral to what object type is being downloaded (application_version objects vs dependency).
<br/>
# Singbox Orchestration

View file

@ -1,3 +1,9 @@
from core.services.networking.httpx import httpx_client
from core.services.networking.httpx import connect
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import Configuration, ConnectionChoice
from core.Errors import FileIntegrityError, UnsupportedApplicationVersionError, ApplicationAlreadyInstalledError
from core.controllers.ApplicationController import ApplicationController
from core.models.session.Application import Application
@ -12,6 +18,7 @@ from core.observers.ConnectionObserver import ConnectionObserver
from core.services.WebServiceApiService import WebServiceApiService
from core.errors.logger import logger
import httpx
from io import BytesIO
from typing import Optional
import hashlib
@ -19,6 +26,7 @@ import shutil
import tarfile
from sqlalchemy.orm import Session
from sqlalchemy import select
import os
class ApplicationVersionController:
@ -48,8 +56,12 @@ class ApplicationVersionController:
if application_version.is_installed():
raise ApplicationAlreadyInstalledError('The application in question is already installed.')
from core.controllers.ConnectionController import ConnectionController
ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController.__install, application_version_observer=application_version_observer, connection_observer=connection_observer)
# this used to go through "with_preferred_connection", but now re-uses the same HTTPx client as sync,
ApplicationVersionController.__install(application_version, application_version_observer, connection_observer)
# legacy:
# from core.controllers.ConnectionController import ConnectionController
# ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController.__install, application_version_observer=application_version_observer, connection_observer=connection_observer)
@staticmethod
def uninstall(application_version: ApplicationVersion):
@ -72,41 +84,53 @@ class ApplicationVersionController:
ApplicationVersion.save_many(application_versions)
@staticmethod
def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, proxies: Optional[dict] = None):
import requests
def __install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None):
target_app_name = application_version.application_code.capitalize()
target_app_version = application_version.version_number
if application_version_observer is not None:
application_version_observer.notify('downloading', application_version)
application_version_observer.notify('downloading', f"Downloading {target_app_name} {target_app_version}. Connecting..")
# legacy:
# application_version_observer.notify('downloading', application_version)
if proxies is not None:
response = requests.get(application_version.download_path, stream=True, proxies=proxies)
else:
response = requests.get(application_version.download_path, stream=True)
################################################
# SETUP HTTP CLIENT
################################################
client = httpx_client.get_http_session()
logger.info(f"client type is {type(client)}")
if client is None:
client = _get_httpx_client(target_app_name=target_app_name, connection_observer=connection_observer)
################################################
# GET THE DATA
################################################
download_path = application_version.download_path
logger.info(f"download_path is {download_path}")
with client.stream('GET', download_path) as response:
logger.info("doing the stream...")
if response.status_code == 200:
response_size = int(response.headers.get('Content-Length', 0))
response_buffer = BytesIO()
block_size = 1024
bytes_written = 0
for data in response.iter_content(block_size):
for data in response.iter_bytes(block_size):
bytes_written += len(data)
response_buffer.write(data)
progress = (bytes_written / response_size) * 100 if response_size > 0 else 0
if application_version_observer is not None:
application_version_observer.notify('download_progressing', f"Downloading {target_app_name} {progress:.2f}% v: {target_app_version}")
else:
raise ConnectionError('The application version could not be downloaded.')
application_version_observer.notify('download_progressing', application_version, dict(
progress=progress
))
application_version_observer.notify('downloaded', application_version)
application_version_observer.notify('downloaded', f"Downloaded {target_app_name} {target_app_version}")
response_buffer.seek(0)
################################################
# VERIFY THE HASH
################################################
file_hash = ApplicationVersionController.__calculate_file_hash(response_buffer)
if file_hash != application_version.file_hash:
@ -118,9 +142,6 @@ class ApplicationVersionController:
with open(f'{application_version.get_installation_path()}/.sha3-512', 'w') as hash_file:
hash_file.write(f'{file_hash}\n')
else:
raise ConnectionError('The application version could not be downloaded.')
@staticmethod
def __calculate_file_hash(file):
@ -136,6 +157,26 @@ class ApplicationVersionController:
return hasher.hexdigest()
# This function is a temporary transition for the connect module to get better public APIs
def _get_httpx_client(target_app_name: str, connection_observer: Optional[ConnectionObserver]) -> httpx.Client:
connection_type = ConfigurationController.get_connection_enum()
client = connect.make_client(connection_type, connection_observer)
logger.info(f"client type is {type(client)}")
if isinstance(client, ApiResponse):
if not client.valid:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
if isinstance(client, bool):
if not client:
raise ConnectionError(f'Could not connect, to download {target_app_name}.')
else:
logger.info("It's a boolean, getting the client now.")
client = httpx_client.get_http_session()
logger.info(f"client type is {type(client)}")
return client
# legacy:
# @staticmethod

View file

@ -22,6 +22,7 @@ from essentials.observers.ConnectionObserver import ConnectionObserver
from core.errors.logger import logger
import httpx
from typing import Optional
_port_used = None
@ -43,7 +44,7 @@ def single_endpoint(method: str, url: str, observer: ConnectionObserver, payload
########################################################
if client is None:
observer.notify('message', "Testing Connection..")
made_client = _make_client(connection_type, observer)
made_client = make_client(connection_type, observer)
if not made_client and connection_type == ConnectionChoice.TOR:
observer.notify('message', "Tor Bootstrap..")
return bootstrap_and_try_again(
@ -113,7 +114,7 @@ def single_endpoint(method: str, url: str, observer: ConnectionObserver, payload
return initial_result
def _make_client(connection_type: str, observer) -> httpx.Client | ApiResponse:
def make_client(connection_type: str, observer: Optional[ConnectionObserver] = None) -> httpx.Client | ApiResponse:
"""
Rank:
Coordinator