diff --git a/src/components/unideskcprovider/test_unideskcprovider.py b/src/components/unideskcprovider/test_unideskcprovider.py index 7d21f222..88d577e9 100644 --- a/src/components/unideskcprovider/test_unideskcprovider.py +++ b/src/components/unideskcprovider/test_unideskcprovider.py @@ -1,6 +1,7 @@ import hashlib import json import logging +import socket import tempfile import unittest from pathlib import Path @@ -16,6 +17,7 @@ from unideskcprovider import ( ProviderConfig, UiBus, UpdateManager, + configure_tcp_keepalive, default_data_endpoint, encode_data_frame, parse_bridge_powershell_command, @@ -137,6 +139,11 @@ class ProtocolHelpersTest(unittest.TestCase): self.assertIn(b'"length":3', header) self.assertEqual(frame[-3:], b"abc") + def test_data_channel_enables_tcp_keepalive(self): + value = MagicMock() + configure_tcp_keepalive(value) + value.setsockopt.assert_any_call(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + def test_recycles_only_the_oldest_expired_idle_channel(self): manager = ProviderManager.__new__(ProviderManager) manager.state_lock = provider.threading.RLock() diff --git a/src/components/unideskcprovider/unideskcprovider.py b/src/components/unideskcprovider/unideskcprovider.py index 5b36614d..ba57ecfe 100644 --- a/src/components/unideskcprovider/unideskcprovider.py +++ b/src/components/unideskcprovider/unideskcprovider.py @@ -48,7 +48,7 @@ else: APP_NAME = "UniDesk Windows Provider" -APP_VERSION = "0.3.1" +APP_VERSION = "0.3.2" CONFIG_DIR = Path.home() / ".unidesk" CONFIG_PATH = CONFIG_DIR / "provider.yaml" LOG_DIR = CONFIG_DIR / "logs" @@ -62,6 +62,9 @@ MAX_HEADER_BYTES = 64 * 1024 MAX_PAYLOAD_BYTES = 16 * 1024 * 1024 HEARTBEAT_SECONDS = 15 DATA_CHANNEL_MAX_IDLE_SECONDS = 10 * 60 +TCP_KEEPALIVE_IDLE_SECONDS = 30 +TCP_KEEPALIVE_INTERVAL_SECONDS = 10 +TCP_KEEPALIVE_PROBES = 3 DEFAULT_UPDATE_METADATA_URL = ( "https://apps.hwpod.com/v1/unideskcprovider/update.json" ) @@ -121,6 +124,24 @@ def default_data_endpoint(server: urllib.parse.ParseResult) -> tuple[str, int]: return server.hostname or "", port +def configure_tcp_keepalive(value: socket.socket) -> None: + value.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + if os.name == "nt" and hasattr(socket, "SIO_KEEPALIVE_VALS"): + value.ioctl( + socket.SIO_KEEPALIVE_VALS, + (1, TCP_KEEPALIVE_IDLE_SECONDS * 1000, TCP_KEEPALIVE_INTERVAL_SECONDS * 1000), + ) + return + for option_name, option_value in ( + ("TCP_KEEPIDLE", TCP_KEEPALIVE_IDLE_SECONDS), + ("TCP_KEEPINTVL", TCP_KEEPALIVE_INTERVAL_SECONDS), + ("TCP_KEEPCNT", TCP_KEEPALIVE_PROBES), + ): + option = getattr(socket, option_name, None) + if option is not None: + value.setsockopt(socket.IPPROTO_TCP, option, option_value) + + def version_key(value: str) -> tuple[int, ...]: if not isinstance(value, str) or not VERSION_PATTERN.fullmatch(value): raise ValueError(f"invalid update version: {value!r}") @@ -681,6 +702,7 @@ class DataChannel: reason = "data channel closed" try: self.socket = socket.create_connection((self.manager.config.data_host, self.manager.config.data_port), timeout=5) + configure_tcp_keepalive(self.socket) self.socket.settimeout(1) self.write( { diff --git a/src/components/unideskcprovider/update.json b/src/components/unideskcprovider/update.json index 5e237737..42448b27 100644 --- a/src/components/unideskcprovider/update.json +++ b/src/components/unideskcprovider/update.json @@ -1,9 +1,9 @@ { "schemaVersion": 1, "channel": "stable", - "latestVersion": "0.3.1", + "latestVersion": "0.3.2", "downloadUrl": "unideskcprovider.py", - "bytes": 72278, - "sha256": "075a41d81d551b5ef2b75aa7958a7cf38da3c2865bc999e79cc440c1b3b4195d", - "releaseNotes": "主动轮换长期空闲 TCP 通道,修复托盘退出期间的日志控件异常,并保持 32 条 warm pool。" + "bytes": 73147, + "sha256": "5b98d5f18bed0c1b3efd736bc3f9464681d8610cee3ebca64f844abc402900bf", + "releaseNotes": "为 32 条 warm pool 启用 Windows TCP keepalive,避免旧空闲通道在并发传输时被网络设备重置。" }