diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f7c026b..616d9252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - feat(keypad): ability to use key-combinations, set key combinations for screenshot, snapshot and quit - feat(web-ui): add `fields` in `InputDescription` with `InputFieldDescription` data structures to describe the fields of an input demand in detail - fix(users): avoid setting user as sudoer when it performs a password reset +- feat(ip): use pythonping to perform a real ping test instead to determine the internet connection status instead of opening a socket ## Version 1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 8913f00b..1d695f86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "betterproto [compiler] >=2.0.0b7", "gpiozero >=2.0.1 ; platform_machine != 'aarch64'", "quart >=0.19.6", + "pythonping>=1.1.4", ] [build-system] @@ -187,7 +188,13 @@ quote-style = "single" profile = "black" [tool.pyright] -exclude = ["typings", "ubo_app/rpc/generated", ".venv", "setup_scm_schemes.py"] +exclude = [ + "typings", + "ubo_app/rpc/generated", + ".venv", + "setup_scm_schemes.py", + "dist/", +] disableTaggedHints = true [[tool.pyright.executionEnvironments]] diff --git a/tests/fixtures/mock_environment.py b/tests/fixtures/mock_environment.py index 0b4610f9..1942a43d 100644 --- a/tests/fixtures/mock_environment.py +++ b/tests/fixtures/mock_environment.py @@ -9,6 +9,8 @@ import pytest +from ubo_app.store.services.ethernet import NetState + originals = {} @@ -259,14 +261,6 @@ def _monkeypatch_asyncio_subprocess(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(asyncio, 'create_subprocess_exec', _fake_create_subprocess_exec) -def _monkeypatch_asyncio_socket(monkeypatch: pytest.MonkeyPatch) -> None: - import asyncio - - from fake import Fake - - monkeypatch.setattr(asyncio, 'open_connection', Fake()) - - @pytest.fixture def mock_environment(monkeypatch: pytest.MonkeyPatch) -> None: """Mock external resources.""" @@ -280,6 +274,7 @@ def mock_environment(monkeypatch: pytest.MonkeyPatch) -> None: import ubo_app.constants import ubo_app.utils.serializer + import ubo_app.utils.server tracemalloc.start() @@ -289,6 +284,11 @@ def mock_environment(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ubo_app.constants, 'STORE_GRACE_PERIOD', 0.1) monkeypatch.setattr(ubo_app.constants, 'NOTIFICATIONS_FLASH_TIME', 1000) monkeypatch.setattr(ubo_app.utils.serializer, 'add_type_field', lambda _, obj: obj) + monkeypatch.setattr( + ubo_app.utils.server, + 'send_command', + Fake(_Fake__return_value=Fake(_Fake__await_value=NetState.CONNECTED)), + ) sys.modules['ubo_app.utils.secrets'] = Fake( _Fake__attrs={'read_secret': lambda _: None}, @@ -302,4 +302,3 @@ def mock_environment(monkeypatch: pytest.MonkeyPatch) -> None: _monkeypatch_rpi_modules() _monkeypatch_subprocess(monkeypatch) _monkeypatch_asyncio_subprocess(monkeypatch) - _monkeypatch_asyncio_socket(monkeypatch) diff --git a/ubo_app/services/030-ethernet/ethernet_manager.py b/ubo_app/services/030-ethernet/ethernet_manager.py index c32ad3fe..4bf93fa1 100644 --- a/ubo_app/services/030-ethernet/ethernet_manager.py +++ b/ubo_app/services/030-ethernet/ethernet_manager.py @@ -5,7 +5,7 @@ import asyncio from typing import TYPE_CHECKING, Any, TypeVar -from ubo_app.store.services.ethernet import GlobalEthernetState +from ubo_app.store.services.ethernet import NetState from ubo_app.utils.bus_provider import get_system_bus if TYPE_CHECKING: @@ -40,23 +40,23 @@ async def get_ethernet_device() -> NetworkDeviceGeneric | None: return None -async def get_ethernet_device_state() -> GlobalEthernetState: +async def get_ethernet_device_state() -> NetState: ethernet_device = await get_ethernet_device() if ethernet_device is None: - return GlobalEthernetState.UNKNOWN + return NetState.UNKNOWN state = await ethernet_device.state if state is DeviceState.UNKNOWN: - return GlobalEthernetState.UNKNOWN + return NetState.UNKNOWN if state in ( DeviceState.DISCONNECTED, DeviceState.UNMANAGED, DeviceState.UNAVAILABLE, DeviceState.FAILED, ): - return GlobalEthernetState.DISCONNECTED + return NetState.DISCONNECTED if state in (DeviceState.NEED_AUTH,): - return GlobalEthernetState.NEEDS_ATTENTION + return NetState.NEEDS_ATTENTION if state in ( DeviceState.DEACTIVATING, DeviceState.PREPARE, @@ -65,8 +65,8 @@ async def get_ethernet_device_state() -> GlobalEthernetState: DeviceState.IP_CHECK, DeviceState.SECONDARIES, ): - return GlobalEthernetState.PENDING + return NetState.PENDING if state == DeviceState.ACTIVATED: - return GlobalEthernetState.CONNECTED + return NetState.CONNECTED - return GlobalEthernetState.UNKNOWN + return NetState.UNKNOWN diff --git a/ubo_app/services/030-ethernet/setup.py b/ubo_app/services/030-ethernet/setup.py index 5dd2fe0e..08b2c105 100644 --- a/ubo_app/services/030-ethernet/setup.py +++ b/ubo_app/services/030-ethernet/setup.py @@ -6,7 +6,7 @@ from ethernet_manager import get_ethernet_device, get_ethernet_device_state from ubo_app.store.main import store -from ubo_app.store.services.ethernet import GlobalEthernetState +from ubo_app.store.services.ethernet import NetState from ubo_app.store.status_icons import StatusIconsRegisterAction from ubo_app.utils.async_ import create_task @@ -20,11 +20,11 @@ async def update_ethernet_icon() -> None: store.dispatch( StatusIconsRegisterAction( icon={ - GlobalEthernetState.CONNECTED: '󱊪', - GlobalEthernetState.DISCONNECTED: '󰌙', - GlobalEthernetState.PENDING: '󰌘', - GlobalEthernetState.NEEDS_ATTENTION: '󰌚', - GlobalEthernetState.UNKNOWN: '󰈅', + NetState.CONNECTED: '󱊪', + NetState.DISCONNECTED: '󰌙', + NetState.PENDING: '󰌘', + NetState.NEEDS_ATTENTION: '󰌚', + NetState.UNKNOWN: '󰈅', }[state], priority=ETHERNET_STATE_ICON_PRIORITY, id=ETHERNET_STATE_ICON_ID, diff --git a/ubo_app/services/030-ip/setup.py b/ubo_app/services/030-ip/setup.py index 6303509d..d80d132b 100644 --- a/ubo_app/services/030-ip/setup.py +++ b/ubo_app/services/030-ip/setup.py @@ -13,12 +13,14 @@ from ubo_app.store.core import RegisterSettingAppAction, SettingsCategory from ubo_app.store.main import store +from ubo_app.store.services.ethernet import NetState from ubo_app.store.services.ip import ( IpNetworkInterface, IpSetIsConnectedAction, IpUpdateInterfacesAction, ) from ubo_app.store.status_icons import StatusIconsRegisterAction +from ubo_app.utils.server import send_command if TYPE_CHECKING: from collections.abc import Sequence @@ -71,31 +73,10 @@ def load_ip_addresses() -> None: ) -async def is_connected() -> bool: - results = await asyncio.gather( - *( - asyncio.wait_for(asyncio.open_connection(ip, 53), timeout=1) - for ip in ('1.1.1.1', '8.8.8.8') - ), - return_exceptions=True, - ) - is_connected = any(not isinstance(result, Exception) for result in results) - - close_tasks = [] - for result in results: - if isinstance(result, tuple): - _, writer = result - writer.close() - close_tasks.append(writer.wait_closed()) - await asyncio.gather(*close_tasks) - - return is_connected - - async def check_connection() -> bool: while True: load_ip_addresses() - if await is_connected(): + if await send_command('connection', has_output=True) == NetState.CONNECTED: store.dispatch( StatusIconsRegisterAction( icon='󰖟', diff --git a/ubo_app/services/030-wifi/reducer.py b/ubo_app/services/030-wifi/reducer.py index c10535db..6765802c 100644 --- a/ubo_app/services/030-wifi/reducer.py +++ b/ubo_app/services/030-wifi/reducer.py @@ -12,8 +12,8 @@ ReducerResult, ) +from ubo_app.store.services.ethernet import NetState from ubo_app.store.services.wifi import ( - GlobalWiFiState, WiFiAction, WiFiEvent, WiFiSetHasVisitedOnboardingAction, @@ -34,7 +34,7 @@ def reducer( return CompleteReducerResult( state=WiFiState( connections=[], - state=GlobalWiFiState.UNKNOWN, + state=NetState.UNKNOWN, current_connection=None, ), actions=[WiFiUpdateRequestAction()], @@ -64,15 +64,15 @@ def reducer( actions=[ StatusIconsRegisterAction( icon={ - GlobalWiFiState.CONNECTED: get_signal_icon( + NetState.CONNECTED: get_signal_icon( action.current_connection.signal_strength if action.current_connection else 0, ), - GlobalWiFiState.DISCONNECTED: '󰖪', - GlobalWiFiState.PENDING: '󱛇', - GlobalWiFiState.NEEDS_ATTENTION: '󱚵', - GlobalWiFiState.UNKNOWN: '󰈅', + NetState.DISCONNECTED: '󰖪', + NetState.PENDING: '󱛇', + NetState.NEEDS_ATTENTION: '󱚵', + NetState.UNKNOWN: '󰈅', }[action.state], priority=WIFI_STATE_ICON_PRIORITY, id=WIFI_STATE_ICON_ID, diff --git a/ubo_app/services/030-wifi/wifi_manager.py b/ubo_app/services/030-wifi/wifi_manager.py index 4f332391..8903279b 100644 --- a/ubo_app/services/030-wifi/wifi_manager.py +++ b/ubo_app/services/030-wifi/wifi_manager.py @@ -11,18 +11,14 @@ from ubo_gui.constants import DANGER_COLOR from ubo_app.store.main import store +from ubo_app.store.services.ethernet import NetState from ubo_app.store.services.notifications import ( Chime, Notification, NotificationDisplayType, NotificationsAddAction, ) -from ubo_app.store.services.wifi import ( - ConnectionState, - GlobalWiFiState, - WiFiConnection, - WiFiType, -) +from ubo_app.store.services.wifi import ConnectionState, WiFiConnection, WiFiType from ubo_app.utils.bus_provider import get_system_bus if TYPE_CHECKING: @@ -73,23 +69,23 @@ async def get_wifi_device() -> NetworkDeviceWireless | None: return None -async def get_wifi_device_state() -> GlobalWiFiState: +async def get_wifi_device_state() -> NetState: wifi_device = await get_wifi_device() if wifi_device is None: - return GlobalWiFiState.UNKNOWN + return NetState.UNKNOWN state = await wifi_device.state if state is DeviceState.UNKNOWN: - return GlobalWiFiState.UNKNOWN + return NetState.UNKNOWN if state in ( DeviceState.DISCONNECTED, DeviceState.UNMANAGED, DeviceState.UNAVAILABLE, DeviceState.FAILED, ): - return GlobalWiFiState.DISCONNECTED + return NetState.DISCONNECTED if state in (DeviceState.NEED_AUTH,): - return GlobalWiFiState.NEEDS_ATTENTION + return NetState.NEEDS_ATTENTION if state in ( DeviceState.DEACTIVATING, DeviceState.PREPARE, @@ -98,11 +94,11 @@ async def get_wifi_device_state() -> GlobalWiFiState: DeviceState.IP_CHECK, DeviceState.SECONDARIES, ): - return GlobalWiFiState.PENDING + return NetState.PENDING if state == DeviceState.ACTIVATED: - return GlobalWiFiState.CONNECTED + return NetState.CONNECTED - return GlobalWiFiState.UNKNOWN + return NetState.UNKNOWN @debounce(wait=0.5, options=DebounceOptions(trailing=True, time_window=2)) diff --git a/ubo_app/store/services/ethernet.py b/ubo_app/store/services/ethernet.py index d340b445..5eac7f31 100644 --- a/ubo_app/store/services/ethernet.py +++ b/ubo_app/store/services/ethernet.py @@ -4,7 +4,7 @@ from enum import StrEnum -class GlobalEthernetState(StrEnum): +class NetState(StrEnum): CONNECTED = 'Connected' DISCONNECTED = 'Disconnected' PENDING = 'Pending' diff --git a/ubo_app/store/services/wifi.py b/ubo_app/store/services/wifi.py index 6e906a13..75e8e134 100644 --- a/ubo_app/store/services/wifi.py +++ b/ubo_app/store/services/wifi.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from ubo_app.store.services.ethernet import NetState + class WiFiType(StrEnum): WEP = 'WEP' @@ -25,14 +27,6 @@ class ConnectionState(StrEnum): UNKNOWN = 'Unknown' -class GlobalWiFiState(StrEnum): - CONNECTED = 'Connected' - DISCONNECTED = 'Disconnected' - PENDING = 'Pending' - NEEDS_ATTENTION = 'Needs Attention' - UNKNOWN = 'Unknown' - - class WiFiConnection(Immutable): ssid: str state: ConnectionState = ConnectionState.UNKNOWN @@ -51,7 +45,7 @@ class WiFiSetHasVisitedOnboardingAction(WiFiAction): class WiFiUpdateAction(WiFiAction): connections: Sequence[WiFiConnection] - state: GlobalWiFiState + state: NetState current_connection: WiFiConnection | None @@ -67,6 +61,6 @@ class WiFiUpdateRequestEvent(WiFiEvent): ... class WiFiState(Immutable): connections: Sequence[WiFiConnection] | None - state: GlobalWiFiState + state: NetState current_connection: WiFiConnection | None has_visited_onboarding: bool | None = None diff --git a/ubo_app/system/system_manager/main.py b/ubo_app/system/system_manager/main.py index b9fa72e2..211580be 100644 --- a/ubo_app/system/system_manager/main.py +++ b/ubo_app/system/system_manager/main.py @@ -11,12 +11,16 @@ import string import subprocess import sys +from dataclasses import dataclass from pathlib import Path from threading import Thread +from pythonping import ping + from ubo_app.constants import USERNAME from ubo_app.error_handlers import setup_error_handling from ubo_app.logging import add_file_handler, add_stdout_handler, get_logger +from ubo_app.store.services.ethernet import NetState from ubo_app.system.system_manager.audio import audio_handler from ubo_app.system.system_manager.docker import docker_handler from ubo_app.system.system_manager.led import LEDManager @@ -38,6 +42,25 @@ logger.setLevel(logging.DEBUG) +@dataclass(kw_only=True) +class ConnectionState: + state: NetState = NetState.UNKNOWN + + +connection_state = ConnectionState() + + +def check_connection() -> None: + while True: + try: + response = ping('1.1.1.1', count=1, timeout=1) + connection_state.state = ( + NetState.CONNECTED if response.success() else NetState.DISCONNECTED + ) + except OSError: + connection_state.state = NetState.DISCONNECTED + + def handle_command(command: str) -> str | None: header, *arguments = command.split() if header == 'led': @@ -52,6 +75,8 @@ def handle_command(command: str) -> str | None: } if header in handlers: return handlers[header](*arguments) + if header == 'connection': + return connection_state.state return None @@ -71,6 +96,9 @@ def setup_hostname() -> None: logger.info('Setting hostname...') from ubo_app.constants import INSTALLATION_PATH + thread = Thread(target=check_connection) + thread.start() + available_letters = list( set(string.ascii_lowercase + string.digits + '-') - set('I1lO'), ) diff --git a/uv.lock b/uv.lock index 93d2e266..fc3ff593 100644 --- a/uv.lock +++ b/uv.lock @@ -1566,6 +1566,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/51/f2d9ea45c25440fad0937001395132a3303cef973c2c85143114910d4377/python_strtobool-1.0.2-py3-none-any.whl", hash = "sha256:107d93760078a7b7eee91b455077fd6123fd94e6474815dfe306854cdfa47d70", size = 6010 }, ] +[[package]] +name = "pythonping" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/e5/6c12081f6be60648e6432dca495c3c048a096c9e719283270158256af69c/pythonping-1.1.4.tar.gz", hash = "sha256:acef84640fee6f20b725f2a1d2392771f2845554cfabcef30b1fdea5030161af", size = 21482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/d7/ac061f5010ae44604a091820c75e78edb8153214caf4519007cff812cd72/pythonping-1.1.4-py3-none-any.whl", hash = "sha256:dedd690e36cb8c69703175610cf6ce01ff0c2070b447cc64c31a82951c9a53f6", size = 16328 }, +] + [[package]] name = "pyusb" version = "1.2.1" @@ -1875,7 +1884,7 @@ wheels = [ [[package]] name = "ubo-app" -version = "1.0.1.dev6+unknown" +version = "1.0.1.dev9+unknown" source = { editable = "." } dependencies = [ { name = "adafruit-circuitpython-aw9523" }, @@ -1900,6 +1909,7 @@ dependencies = [ { name = "python-fake" }, { name = "python-redux" }, { name = "python-strtobool" }, + { name = "pythonping" }, { name = "pyzbar" }, { name = "quart" }, { name = "rpi-lgpio", marker = "platform_machine == 'aarch64'" }, @@ -1954,6 +1964,7 @@ requires-dist = [ { name = "python-fake", specifier = ">=0.1.3" }, { name = "python-redux", specifier = ">=0.17.2" }, { name = "python-strtobool", specifier = ">=1.0.0" }, + { name = "pythonping", specifier = ">=1.1.4" }, { name = "pyzbar", specifier = ">=0.1.9" }, { name = "quart", specifier = ">=0.19.6" }, { name = "rpi-lgpio", marker = "platform_machine == 'aarch64'", specifier = ">=0.6" },