mqttxx 2.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mqttxx/rpc.py ADDED
@@ -0,0 +1,417 @@
1
+ # MQTT RPC 模块 - 基于 aiomqtt 的双向对等 RPC 调用
2
+
3
+ import asyncio
4
+ import json
5
+ import uuid
6
+ from typing import Any, Callable, Optional
7
+ from loguru import logger
8
+
9
+ from .client import MQTTClient
10
+ from .config import RPCConfig
11
+ from .exceptions import (
12
+ RPCTimeoutError,
13
+ RPCRemoteError,
14
+ TooManyConcurrentCallsError,
15
+ MQTTXError,
16
+ ErrorCode,
17
+ )
18
+ from .protocol import RPCRequest, RPCResponse
19
+
20
+
21
+ # 权限检查回调类型
22
+ AuthCallback = Callable[[str, str, RPCRequest], bool]
23
+
24
+
25
+ class RPCManager:
26
+ """RPC 调用管理器(基于 aiomqtt 客户端)
27
+
28
+ 核心改进(相对 gmqtt 版本):
29
+ 1. 适配纯 async/await 模式(不依赖 EventEmitter)
30
+ 2. 权限控制(auth_callback 参数,修复 P0-4)
31
+ 3. 并发限制(max_concurrent_calls)
32
+ 4. 注销机制(unregister 方法,修复 P0-3)
33
+ 5. 方法拆分(降低复杂度)
34
+
35
+ 职责:
36
+ - 发起远程 RPC 调用(通过 call 方法)
37
+ - 注册本地方法供远程调用(通过 register 装饰器)
38
+ - 处理 RPC 请求和响应消息
39
+ - 管理并发调用的 Future 对象
40
+
41
+ 设计决策:
42
+ - 不再依赖 EventEmitter,直接注册消息处理器到 MQTTClient
43
+ - 使用 handle_rpc_message 作为消息处理入口
44
+ - 权限检查在请求处理前进行(可拒绝执行)
45
+
46
+ 示例:
47
+ # 创建 RPC 管理器(带权限控制)
48
+ async def auth_check(caller_id, method, request):
49
+ if method in ["delete_user"]:
50
+ return caller_id in ADMIN_LIST
51
+ return True
52
+
53
+ rpc = RPCManager(client, auth_callback=auth_check)
54
+
55
+ # 注册本地方法
56
+ @rpc.register("get_status")
57
+ async def get_status(params):
58
+ return {"status": "online", "temperature": 25.5}
59
+
60
+ # 订阅 RPC 主题并绑定处理器
61
+ client.subscribe(
62
+ "my/rpc/topic",
63
+ lambda t, m: rpc.handle_rpc_message(t, m)
64
+ )
65
+
66
+ # 远程调用
67
+ result = await rpc.call(
68
+ topic="remote/rpc/topic",
69
+ method="get_status",
70
+ reply_to="my/rpc/topic"
71
+ )
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ client: MQTTClient,
77
+ config: Optional[RPCConfig] = None,
78
+ auth_callback: Optional[AuthCallback] = None
79
+ ):
80
+ """初始化 RPC 管理器
81
+
82
+ Args:
83
+ client: MQTTClient 实例(用于底层消息收发)
84
+ config: RPC 配置(可选,默认使用标准配置)
85
+ auth_callback: 权限检查回调函数(可选)
86
+ 签名:async def auth_callback(caller_id: str, method: str, request: RPCRequest) -> bool
87
+ 返回:True = 允许,False = 拒绝
88
+
89
+ 使用示例:
90
+ client = MQTTClient(...)
91
+ await client.connect()
92
+
93
+ # 基础用法(无权限控制)
94
+ rpc = RPCManager(client)
95
+
96
+ # 带权限控制
97
+ async def auth_check(caller_id, method, request):
98
+ return caller_id in ALLOWED_CLIENTS
99
+
100
+ rpc = RPCManager(client, auth_callback=auth_check)
101
+ """
102
+ self._client = client
103
+ self.config = config or RPCConfig()
104
+ self._auth_callback = auth_callback
105
+
106
+ # RPC 状态
107
+ self._pending_calls: dict[str, asyncio.Future] = {} # request_id → Future
108
+ self._handlers: dict[str, Callable] = {} # method_name → handler
109
+ self._pending_calls_lock = asyncio.Lock() # 修复 P0-1:保护 _pending_calls 并发访问
110
+
111
+ logger.info("RPCManager 已初始化")
112
+
113
+ def register(self, method_name: str):
114
+ """装饰器:注册本地 RPC 方法供远程调用
115
+
116
+ Args:
117
+ method_name: 方法名称(远程节点通过此名称调用)
118
+
119
+ Returns:
120
+ 装饰器函数
121
+
122
+ 使用示例:
123
+ @rpc.register("get_status")
124
+ async def get_status(params):
125
+ return {"status": "online", "temperature": 25.5}
126
+
127
+ @rpc.register("process_command")
128
+ def process_command(params):
129
+ # 同步方法也支持
130
+ return {"result": "ok"}
131
+ """
132
+ def decorator(func: Callable):
133
+ self._handlers[method_name] = func
134
+ logger.success(f"RPC 方法已注册: {method_name}")
135
+ return func
136
+
137
+ return decorator
138
+
139
+ def unregister(self, method_name: str):
140
+ """注销 RPC 方法
141
+
142
+ 修复点:
143
+ - ✅ P0-3: 提供注销机制,防止内存泄漏
144
+
145
+ Args:
146
+ method_name: 方法名称
147
+
148
+ 使用示例:
149
+ rpc.unregister("get_status")
150
+ """
151
+ if method_name in self._handlers:
152
+ del self._handlers[method_name]
153
+ logger.info(f"RPC 方法已注销: {method_name}")
154
+
155
+ def handle_rpc_message(self, topic: str, message: RPCRequest | RPCResponse):
156
+ """处理 RPC 消息(由 MQTTClient 调用)
157
+
158
+ 这是 RPC 消息的处理入口,替代 gmqtt 版本的 EventEmitter.emit
159
+
160
+ Args:
161
+ topic: MQTT 主题
162
+ message: RPC 请求或响应消息
163
+
164
+ 使用示例:
165
+ # 订阅主题并绑定 RPC 处理器
166
+ client.subscribe(
167
+ "my/rpc/topic",
168
+ lambda t, m: rpc.handle_rpc_message(t, m)
169
+ )
170
+ """
171
+ if isinstance(message, RPCRequest):
172
+ asyncio.create_task(
173
+ self._handle_request(topic, message),
174
+ name=f"rpc_req_{message.request_id[:8]}"
175
+ )
176
+ elif isinstance(message, RPCResponse):
177
+ asyncio.create_task(
178
+ self._handle_response(topic, message),
179
+ name=f"rpc_resp_{message.request_id[:8]}"
180
+ )
181
+
182
+ async def call(
183
+ self,
184
+ topic: str,
185
+ method: str,
186
+ params: Any = None,
187
+ timeout: Optional[float] = None,
188
+ reply_to: str = None,
189
+ ) -> Any:
190
+ """远程调用 RPC 方法
191
+
192
+ 修复点:
193
+ - ✅ 新增并发限制检查
194
+
195
+ Args:
196
+ topic: 目标 MQTT 主题(例如:bots/456)
197
+ method: 远程方法名
198
+ params: 方法参数(可选)
199
+ timeout: 超时时间(秒,None 则使用配置的默认值)
200
+ reply_to: 响应主题(必需,例如:server/device_123)
201
+
202
+ Returns:
203
+ 远程方法的返回值
204
+
205
+ Raises:
206
+ MQTTXError: 客户端未连接
207
+ ValueError: reply_to 参数缺失
208
+ TooManyConcurrentCallsError: 并发调用超限
209
+ RPCTimeoutError: 调用超时
210
+ RPCRemoteError: 远程执行失败
211
+
212
+ 使用示例:
213
+ # 基础调用
214
+ result = await rpc.call(
215
+ topic="bots/456",
216
+ method="get_status",
217
+ reply_to="server/device_123"
218
+ )
219
+
220
+ # 带参数调用
221
+ result = await rpc.call(
222
+ topic="bots/456",
223
+ method="process_command",
224
+ params={"command": "restart"},
225
+ reply_to="server/device_123",
226
+ timeout=60.0
227
+ )
228
+ """
229
+ if not self._client.is_connected:
230
+ raise MQTTXError("MQTT 客户端未连接", ErrorCode.NOT_CONNECTED)
231
+
232
+ if reply_to is None:
233
+ raise ValueError("reply_to 参数是必需的,不能为 None")
234
+
235
+ # 生成请求
236
+ request_id = str(uuid.uuid4())
237
+ timeout = timeout or self.config.default_timeout
238
+
239
+ request = RPCRequest(
240
+ request_id=request_id,
241
+ method=method,
242
+ params=params,
243
+ reply_to=reply_to,
244
+ caller_id=self._client.config.client_id,
245
+ )
246
+
247
+ # 修复 P0-1:原子性检查并发限制 + 注册 Future
248
+ async with self._pending_calls_lock:
249
+ if len(self._pending_calls) >= self.config.max_concurrent_calls:
250
+ raise TooManyConcurrentCallsError(
251
+ f"并发 RPC 调用超限: {len(self._pending_calls)}/{self.config.max_concurrent_calls}"
252
+ )
253
+ # 创建 Future
254
+ future = asyncio.get_event_loop().create_future()
255
+ self._pending_calls[request_id] = future
256
+
257
+ # 发送请求
258
+ await self._client.publish(topic, json.dumps(request.to_dict()), qos=1)
259
+ logger.debug(f"RPC 请求已发送 - method: {method}, request_id: {request_id[:8]}")
260
+
261
+ try:
262
+ # 等待响应
263
+ result = await asyncio.wait_for(future, timeout=timeout)
264
+ logger.debug(f"RPC 调用成功 - method: {method}")
265
+ return result
266
+ except asyncio.TimeoutError:
267
+ logger.error(f"RPC 超时 - method: {method}, timeout: {timeout}s")
268
+ raise RPCTimeoutError(f"RPC 调用超时: {method}")
269
+ finally:
270
+ # 清理 Future(修复 P0-1:使用锁保护)
271
+ async with self._pending_calls_lock:
272
+ self._pending_calls.pop(request_id, None)
273
+
274
+ async def _handle_response(self, topic: str, message: RPCResponse):
275
+ """处理 RPC 响应
276
+
277
+ Args:
278
+ topic: MQTT 主题
279
+ message: RPC 响应消息
280
+ """
281
+ # 修复 P0-1:使用锁保护读取
282
+ async with self._pending_calls_lock:
283
+ future = self._pending_calls.get(message.request_id)
284
+
285
+ if not future or future.done():
286
+ logger.warning(f"收到未知/过期响应 - request_id: {message.request_id[:8]}")
287
+ return
288
+
289
+ if message.error:
290
+ future.set_exception(RPCRemoteError(message.error))
291
+ else:
292
+ future.set_result(message.result)
293
+
294
+ async def _handle_request(self, topic: str, message: RPCRequest):
295
+ """处理 RPC 请求
296
+
297
+ 修复点:
298
+ - ✅ P0-4: 添加权限检查
299
+ - ✅ 重构为子方法(降低复杂度)
300
+
301
+ Args:
302
+ topic: MQTT 主题
303
+ message: RPC 请求消息
304
+
305
+ 执行流程:
306
+ 1. 权限检查(如果配置了 auth_callback)
307
+ 2. 查找处理器
308
+ 3. 执行方法
309
+ 4. 发送响应
310
+ """
311
+ # 权限检查
312
+ if self._auth_callback:
313
+ allowed = await self._check_permission(message)
314
+ if not allowed:
315
+ logger.warning(
316
+ f"RPC 权限拒绝 - caller: {message.caller_id}, method: {message.method}"
317
+ )
318
+ response = RPCResponse(
319
+ request_id=message.request_id,
320
+ error="Permission denied"
321
+ )
322
+ await self._send_response(message.reply_to, response)
323
+ return
324
+
325
+ # 查找处理器
326
+ handler = self._handlers.get(message.method)
327
+
328
+ if not handler:
329
+ logger.warning(f"方法未找到 - method: {message.method}")
330
+ response = RPCResponse(
331
+ request_id=message.request_id,
332
+ error=f"方法未找到: {message.method}"
333
+ )
334
+ else:
335
+ # 执行方法
336
+ response = await self._execute_handler(handler, message)
337
+
338
+ # 发送响应
339
+ await self._send_response(message.reply_to, response)
340
+
341
+ async def _check_permission(self, message: RPCRequest) -> bool:
342
+ """检查 RPC 调用权限
343
+
344
+ Args:
345
+ message: RPC 请求消息
346
+
347
+ Returns:
348
+ True = 允许,False = 拒绝
349
+
350
+ 异常处理:
351
+ - 如果 auth_callback 抛出异常,默认拒绝(fail-safe)
352
+ """
353
+ try:
354
+ if asyncio.iscoroutinefunction(self._auth_callback):
355
+ allowed = await self._auth_callback(
356
+ message.caller_id,
357
+ message.method,
358
+ message
359
+ )
360
+ else:
361
+ allowed = self._auth_callback(
362
+ message.caller_id,
363
+ message.method,
364
+ message
365
+ )
366
+ return bool(allowed)
367
+ except Exception as e:
368
+ logger.exception(f"权限检查失败: {e}")
369
+ return False # 默认拒绝
370
+
371
+ async def _execute_handler(self, handler: Callable, message: RPCRequest) -> RPCResponse:
372
+ """执行 RPC 方法处理器
373
+
374
+ Args:
375
+ handler: 注册的方法处理器
376
+ message: RPC 请求消息
377
+
378
+ Returns:
379
+ RPC 响应消息(包含结果或错误)
380
+
381
+ 异常处理:
382
+ - asyncio.CancelledError: 向上传播(任务被取消)
383
+ - 其他异常: 捕获并封装到 RPCResponse.error
384
+ """
385
+ try:
386
+ # 自动处理同步/异步函数
387
+ if asyncio.iscoroutinefunction(handler):
388
+ result = await handler(message.params)
389
+ else:
390
+ result = handler(message.params)
391
+
392
+ logger.debug(f"RPC 方法执行成功 - method: {message.method}")
393
+ return RPCResponse(
394
+ request_id=message.request_id,
395
+ result=result
396
+ )
397
+
398
+ except asyncio.CancelledError:
399
+ # 任务被取消,向上传播
400
+ raise
401
+
402
+ except Exception as e:
403
+ logger.exception(f"RPC 方法执行失败 - method: {message.method}")
404
+ return RPCResponse(
405
+ request_id=message.request_id,
406
+ error=str(e)
407
+ )
408
+
409
+ async def _send_response(self, topic: str, response: RPCResponse):
410
+ """发送 RPC 响应
411
+
412
+ Args:
413
+ topic: 响应主题
414
+ response: RPC 响应消息
415
+ """
416
+ await self._client.publish(topic, json.dumps(response.to_dict()), qos=1)
417
+ logger.debug(f"RPC 响应已发送 - request_id: {response.request_id[:8]}")
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MQTTX Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.