Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ zmq>=0.0.0
pywin32>=311
loguru>=0.7.3
debugpy>=1.8.20
PyQt5-stubs>=5.15.6.0
43 changes: 12 additions & 31 deletions src/lib_zmq_plugins/client/zmq_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,31 +104,6 @@ def connect(self) -> None:
# 启动后立即发一次心跳作为探测
self._probe_connection()

@property
def is_connected(self) -> bool:
"""当前连接状态"""
return self._is_connected

@property
def reconnect_count(self) -> int:
"""重连次数"""
return self._reconnect_count

@property
def endpoint(self) -> str:
"""当前端点地址"""
return self._endpoint

@property
def pub_endpoint(self) -> str:
"""PUB 端点地址"""
return self._pub_endpoint

@property
def ctrl_endpoint(self) -> str:
"""CTRL 端点地址"""
return self._ctrl_endpoint

def disconnect(self) -> None:
self._stopped.set()
if self._thread and self._thread.is_alive():
Expand All @@ -145,6 +120,8 @@ def _create_sockets(self) -> None:
return
self._sub_socket = self._ctx.socket(zmq.SUB)
self._dealer_socket = self._ctx.socket(zmq.DEALER)
if self._dealer_socket is None:
return
self._dealer_socket.setsockopt_string(zmq.IDENTITY, uuid.uuid4().hex)

# 启用 ZMQ 原生心跳,自动检测服务端断开
Expand All @@ -154,7 +131,8 @@ def _create_sockets(self) -> None:
self._dealer_socket.setsockopt(zmq.HEARTBEAT_IVL, 5000)
self._dealer_socket.setsockopt(zmq.HEARTBEAT_TIMEOUT, 5000)
self._dealer_socket.setsockopt(zmq.HEARTBEAT_TTL, 10000)

if self._sub_socket is None:
return
self._sub_socket.connect(self._pub_endpoint)
self._dealer_socket.connect(self._ctrl_endpoint)

Expand Down Expand Up @@ -202,7 +180,8 @@ def _request_snapshot(self, topic: str) -> None:
try:
self._dealer_socket.send(payload)
except zmq.ZMQError:
self._log.warning("Failed to send sync request for topic: %s", topic)
self._log.warning(
"Failed to send sync request for topic: %s", topic)
self._sync_topics.pop(rid, None)

# ── 指令发送(可在任意线程调用) ──
Expand Down Expand Up @@ -231,7 +210,7 @@ def request(
def _poll_loop(self) -> None:
while not self._stopped.is_set():
try:
events = self._poller.poll(timeout=200)
events = self._poller.poll(timeout=200) # type: ignore
except zmq.ZMQError:
self._handle_reconnect(0.1)
continue
Expand Down Expand Up @@ -275,7 +254,7 @@ def _probe_connection(self) -> bool:

def _handle_sub_message(self) -> None:
try:
msg = self._sub_socket.recv_multipart(zmq.NOBLOCK)
msg = self._sub_socket.recv_multipart(zmq.NOBLOCK) # type: ignore
except zmq.Again:
return
if len(msg) < 2:
Expand All @@ -293,7 +272,8 @@ def _handle_sub_message(self) -> None:

def _handle_dealer_message(self) -> None:
try:
msg = self._dealer_socket.recv_multipart(zmq.NOBLOCK)
msg = self._dealer_socket.recv_multipart( # type: ignore
zmq.NOBLOCK)
except zmq.Again:
return
if len(msg) < 2:
Expand All @@ -312,7 +292,8 @@ def _handle_dealer_message(self) -> None:
snapshot = self._serializer.decode_event(resp.data)
self._notify_subscribers(topic, snapshot)
except Exception:
self._log.warning("Failed to decode snapshot", exc_info=True)
self._log.warning(
"Failed to decode snapshot", exc_info=True)
else:
self._sync_topics.pop(resp.request_id, None)

Expand Down
2 changes: 1 addition & 1 deletion src/lib_zmq_plugins/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def _make_union(types: list[type]) -> type:
"""将类型列表转为 Union 类型,供 msgspec 多态反序列化使用"""
if len(types) == 1:
return types[0]
return Union[tuple(types)]
return Union[tuple(types)] # type: ignore


class Serializer:
Expand Down
33 changes: 21 additions & 12 deletions src/lib_zmq_plugins/server/zmq_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def __init__(self, endpoint: str, log_handler: LogHandler | None = None) -> None
self._pub_endpoint, self._ctrl_endpoint = _derive_endpoints(endpoint)
self._serializer = Serializer()
self._serializer.register_command_types(SyncCommand)
self._handlers: dict[str, Callable[[BaseCommand], CommandResponse | None]] = {}
self._handlers: dict[str, Callable[[
BaseCommand], CommandResponse | None]] = {}
self._snapshot_providers: dict[str, Callable[[], BaseEvent]] = {}
self._log: LogHandler = log_handler or NullHandler()

Expand Down Expand Up @@ -89,7 +90,8 @@ def start(self) -> None:
self._ctx = zmq.Context()
self._pub_socket = self._ctx.socket(zmq.PUB)
self._router_socket = self._ctx.socket(zmq.ROUTER)

if self._pub_socket is None or self._router_socket is None:
raise RuntimeError("Failed to create socket")
# 启用 ZMQ 原生心跳,与客户端匹配
# HEARTBEAT_IVL: 每 5 秒发送心跳
# HEARTBEAT_TIMEOUT: 5 秒内没收到回复视为断连
Expand All @@ -108,7 +110,8 @@ def start(self) -> None:
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()

self._log.info("Server started: pub=%s, ctrl=%s", self._pub_endpoint, self._ctrl_endpoint)
self._log.info("Server started: pub=%s, ctrl=%s",
self._pub_endpoint, self._ctrl_endpoint)

def stop(self) -> None:
self._stopped.set()
Expand Down Expand Up @@ -141,14 +144,15 @@ def publish(self, topic: type[BaseEvent], event: BaseEvent) -> None:
def _poll_loop(self) -> None:
while not self._stopped.is_set():
try:
events = self._poller.poll(timeout=100)
events = self._poller.poll(timeout=100) # type: ignore
except zmq.ZMQError:
break

for socket, _ in events:
if socket is self._router_socket:
try:
msg = self._router_socket.recv_multipart(zmq.NOBLOCK)
msg = self._router_socket.recv_multipart( # type: ignore
zmq.NOBLOCK)
except zmq.Again:
continue
if len(msg) < 2:
Expand All @@ -158,7 +162,8 @@ def _poll_loop(self) -> None:
try:
cmd = self._serializer.decode_command(payload)
except Exception:
self._log.warning("Failed to decode command", exc_info=True)
self._log.warning(
"Failed to decode command", exc_info=True)
continue
self._dispatch(client_id, cmd)

Expand All @@ -167,11 +172,12 @@ def _dispatch(self, client_id: bytes, cmd: BaseCommand) -> None:
if isinstance(tag, type):
tag = tag.__name__
tag = str(tag)

self._log.info("[Server] 收到命令: tag=%s, request_id=%s", tag, cmd.request_id)

self._log.info("[Server] 收到命令: tag=%s, request_id=%s",
tag, cmd.request_id)

if tag == "__sync__":
self._handle_sync(client_id, cmd)
self._handle_sync(client_id, cmd) # type: ignore
return

handler = self._handlers.get(tag)
Expand All @@ -181,7 +187,8 @@ def _dispatch(self, client_id: bytes, cmd: BaseCommand) -> None:

try:
result = handler(cmd)
self._log.info("[Server] handler 执行完成: tag=%s, result=%s", tag, result)
self._log.info(
"[Server] handler 执行完成: tag=%s, result=%s", tag, result)
except Exception as e:
self._log.error("Handler error for %s: %s", tag, e, exc_info=True)
if cmd.request_id:
Expand Down Expand Up @@ -211,7 +218,8 @@ def _handle_sync(self, client_id: bytes, cmd: SyncCommand) -> None:
request_id=cmd.request_id, success=True, data=payload
)
except Exception as e:
self._log.error("Snapshot provider error for %s: %s", topic, e, exc_info=True)
self._log.error(
"Snapshot provider error for %s: %s", topic, e, exc_info=True)
resp = CommandResponse(
request_id=cmd.request_id, success=False, error=str(e)
)
Expand All @@ -225,4 +233,5 @@ def _send_to_client(self, client_id: bytes, resp: CommandResponse) -> None:
[client_id, b"", self._serializer.encode_response(resp)]
)
except zmq.ZMQError:
self._log.warning("Failed to send response to client", exc_info=True)
self._log.warning(
"Failed to send response to client", exc_info=True)
2 changes: 1 addition & 1 deletion src/mineSweeperGUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def game_state(self, game_state: str):
current_status=state_map.get(game_state, 0),
)
GameServerBridge.instance().send_event(event)
self._send_board_update_event()
self._send_board_update_event()

@property
def row(self):
Expand Down
4 changes: 4 additions & 0 deletions src/plugin_manager/config_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def _setup_ui(self) -> None:
return

for name, config_field in fields.items():
# 跳过不可见的配置项(用于插件存储私有数据)
if not config_field.visible:
continue

# 使用 config_field 自己的 create_widget 方法
widget = config_field.create_widget()

Expand Down
2 changes: 1 addition & 1 deletion src/plugin_manager/plugin_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PluginLoader:
- 实例化插件类
"""

def __init__(self, plugin_dirs: list[str | Path] | None = None):
def __init__(self, plugin_dirs: list[Path] | None = None):
self._plugin_dirs: list[Path] = []
self._added_paths: set[Path] = set() # 已添加到 sys.path 的目录
if plugin_dirs:
Expand Down
Loading
Loading