Merge branch fix/provider-channel-stability
This commit is contained in:
@@ -11,6 +11,8 @@ import yaml
|
||||
import unideskcprovider as provider
|
||||
|
||||
from unideskcprovider import (
|
||||
DataChannel,
|
||||
ProviderManager,
|
||||
ProviderConfig,
|
||||
UiBus,
|
||||
UpdateManager,
|
||||
@@ -135,6 +137,42 @@ class ProtocolHelpersTest(unittest.TestCase):
|
||||
self.assertIn(b'"length":3', header)
|
||||
self.assertEqual(frame[-3:], b"abc")
|
||||
|
||||
def test_recycles_only_the_oldest_expired_idle_channel(self):
|
||||
manager = ProviderManager.__new__(ProviderManager)
|
||||
manager.state_lock = provider.threading.RLock()
|
||||
manager.logger = MagicMock()
|
||||
expired = DataChannel(manager, "expired")
|
||||
expired.status = "ready"
|
||||
expired.idle_since = 100.0
|
||||
newer = DataChannel(manager, "newer")
|
||||
newer.status = "ready"
|
||||
newer.idle_since = 200.0
|
||||
claimed = DataChannel(manager, "claimed")
|
||||
claimed.status = "claimed"
|
||||
claimed.session_id = "session"
|
||||
claimed.idle_since = 0.0
|
||||
manager.channels = {value.id: value for value in (newer, claimed, expired)}
|
||||
for channel in manager.channels.values():
|
||||
channel.close = MagicMock()
|
||||
|
||||
self.assertTrue(manager.recycle_idle_channel(now=1000.0))
|
||||
expired.close.assert_called_once_with("空闲通道主动轮换")
|
||||
newer.close.assert_not_called()
|
||||
claimed.close.assert_not_called()
|
||||
|
||||
def test_keeps_recent_idle_channels(self):
|
||||
manager = ProviderManager.__new__(ProviderManager)
|
||||
manager.state_lock = provider.threading.RLock()
|
||||
manager.logger = MagicMock()
|
||||
channel = DataChannel(manager, "recent")
|
||||
channel.status = "ready"
|
||||
channel.idle_since = 500.0
|
||||
channel.close = MagicMock()
|
||||
manager.channels = {channel.id: channel}
|
||||
|
||||
self.assertFalse(manager.recycle_idle_channel(now=1000.0))
|
||||
channel.close.assert_not_called()
|
||||
|
||||
|
||||
class UpdateManagerTest(unittest.TestCase):
|
||||
@staticmethod
|
||||
|
||||
@@ -48,7 +48,7 @@ else:
|
||||
|
||||
|
||||
APP_NAME = "UniDesk Windows Provider"
|
||||
APP_VERSION = "0.3.0"
|
||||
APP_VERSION = "0.3.1"
|
||||
CONFIG_DIR = Path.home() / ".unidesk"
|
||||
CONFIG_PATH = CONFIG_DIR / "provider.yaml"
|
||||
LOG_DIR = CONFIG_DIR / "logs"
|
||||
@@ -61,6 +61,7 @@ POOL_SIZE = 32
|
||||
MAX_HEADER_BYTES = 64 * 1024
|
||||
MAX_PAYLOAD_BYTES = 16 * 1024 * 1024
|
||||
HEARTBEAT_SECONDS = 15
|
||||
DATA_CHANNEL_MAX_IDLE_SECONDS = 10 * 60
|
||||
DEFAULT_UPDATE_METADATA_URL = (
|
||||
"https://apps.hwpod.com/v1/unideskcprovider/update.json"
|
||||
)
|
||||
@@ -669,6 +670,7 @@ class DataChannel:
|
||||
self.buffer = bytearray()
|
||||
self.pending: list[tuple[dict, bytes]] = []
|
||||
self.pending_bytes = 0
|
||||
self.idle_since = time.monotonic()
|
||||
self.write_lock = threading.Lock()
|
||||
self.closed = threading.Event()
|
||||
|
||||
@@ -690,6 +692,7 @@ class DataChannel:
|
||||
}
|
||||
)
|
||||
self.status = "ready"
|
||||
self.idle_since = time.monotonic()
|
||||
self.manager.logger.info("TCP 通道已就绪: %s", self.id)
|
||||
self.manager.pool_changed()
|
||||
while not self.closed.is_set():
|
||||
@@ -879,6 +882,7 @@ class ProviderManager:
|
||||
while not self.stop_event.is_set():
|
||||
if time.time() - last_heartbeat >= HEARTBEAT_SECONDS:
|
||||
self._send_heartbeat()
|
||||
self.recycle_idle_channel()
|
||||
last_heartbeat = time.time()
|
||||
try:
|
||||
opcode, payload = control.receive()
|
||||
@@ -1055,8 +1059,26 @@ class ProviderManager:
|
||||
channel.pending_bytes = 0
|
||||
if not channel.closed.is_set():
|
||||
channel.status = "ready"
|
||||
channel.idle_since = time.monotonic()
|
||||
self.pool_changed()
|
||||
|
||||
def recycle_idle_channel(self, now: float | None = None) -> bool:
|
||||
current = time.monotonic() if now is None else now
|
||||
with self.state_lock:
|
||||
candidates = [
|
||||
channel
|
||||
for channel in self.channels.values()
|
||||
if channel.status == "ready"
|
||||
and channel.session_id is None
|
||||
and current - channel.idle_since >= DATA_CHANNEL_MAX_IDLE_SECONDS
|
||||
]
|
||||
channel = min(candidates, key=lambda value: value.idle_since) if candidates else None
|
||||
if channel is None:
|
||||
return False
|
||||
self.logger.info("主动轮换空闲 TCP 通道: %s", channel.id)
|
||||
channel.close("空闲通道主动轮换")
|
||||
return True
|
||||
|
||||
def handle_data_frame(self, channel: DataChannel, header: dict, payload: bytes) -> None:
|
||||
session_id = str(header.get("sessionId") or "")
|
||||
with self.state_lock:
|
||||
@@ -1603,16 +1625,22 @@ class ProviderApp:
|
||||
elif kind == "restart":
|
||||
self.restart_requested = True
|
||||
self.quit()
|
||||
return
|
||||
self.root.after(120, self._poll_events)
|
||||
|
||||
def _append_log(self, line: str) -> None:
|
||||
self.log_text.configure(state="normal")
|
||||
self.log_text.insert("end", line + "\n")
|
||||
lines = int(self.log_text.index("end-1c").split(".")[0])
|
||||
if lines > 1000:
|
||||
self.log_text.delete("1.0", f"{lines - 1000}.0")
|
||||
self.log_text.see("end")
|
||||
self.log_text.configure(state="disabled")
|
||||
try:
|
||||
if not self.log_text.winfo_exists():
|
||||
return
|
||||
self.log_text.configure(state="normal")
|
||||
self.log_text.insert("end", line + "\n")
|
||||
lines = int(self.log_text.index("end-1c").split(".")[0])
|
||||
if lines > 1000:
|
||||
self.log_text.delete("1.0", f"{lines - 1000}.0")
|
||||
self.log_text.see("end")
|
||||
self.log_text.configure(state="disabled")
|
||||
except tk.TclError:
|
||||
return
|
||||
|
||||
def _set_connection(self, state: str) -> None:
|
||||
self.connection_state = state
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"channel": "stable",
|
||||
"latestVersion": "0.3.0",
|
||||
"latestVersion": "0.3.1",
|
||||
"downloadUrl": "unideskcprovider.py",
|
||||
"bytes": 71119,
|
||||
"sha256": "98dbdd9e28e2dabe0b89da22924543a36502f5e5bebda9d1e27a4d677ed7f746",
|
||||
"releaseNotes": "将 Windows Provider TCP warm pool 提升到 32 条,并增加面向人类和 Agent 的 L1 快速部署入口。"
|
||||
"bytes": 72278,
|
||||
"sha256": "075a41d81d551b5ef2b75aa7958a7cf38da3c2865bc999e79cc440c1b3b4195d",
|
||||
"releaseNotes": "主动轮换长期空闲 TCP 通道,修复托盘退出期间的日志控件异常,并保持 32 条 warm pool。"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user