tauri-runtime 0.1.0__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.
- tauri_runtime/__init__.py +37 -0
- tauri_runtime/app.py +587 -0
- tauri_runtime/channel.py +79 -0
- tauri_runtime/cli/__init__.py +75 -0
- tauri_runtime/cli/bundle.py +127 -0
- tauri_runtime/cli/scaffold.py +310 -0
- tauri_runtime/cli/templates.py +92 -0
- tauri_runtime/command.py +206 -0
- tauri_runtime/event.py +35 -0
- tauri_runtime/protocol.py +483 -0
- tauri_runtime/py.typed +0 -0
- tauri_runtime/router.py +147 -0
- tauri_runtime-0.1.0.dist-info/METADATA +95 -0
- tauri_runtime-0.1.0.dist-info/RECORD +18 -0
- tauri_runtime-0.1.0.dist-info/WHEEL +4 -0
- tauri_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- tauri_runtime-0.1.0.dist-info/licenses/LICENSE-APACHE +202 -0
- tauri_runtime-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""线协议(v2):消息模型、帧编解码与 stdio 接管。
|
|
2
|
+
|
|
3
|
+
每一帧的布局为::
|
|
4
|
+
|
|
5
|
+
[u32 BE inner_len][u32 BE header_len][header JSON][body]
|
|
6
|
+
|
|
7
|
+
`inner_len` 计入其后的全部字节(不含自身),上限为 `MAX_FRAME_SIZE`。header 是一个以
|
|
8
|
+
`type` 字段区分的 JSON 对象,在 Python 侧由 pydantic 模型组成的可辨识联合 `Message` 表示,
|
|
9
|
+
与 Rust 侧的 `bridge::protocol::Message` 枚举一一对应。
|
|
10
|
+
|
|
11
|
+
载荷有两种形式:JSON 值内联在 header 的 `value` 字段中;二进制数据放在帧体 `body` 中,
|
|
12
|
+
并在 header 中设置 `raw: true`。未设置 `raw` 时帧体必须为空。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import struct
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Annotated, Any, BinaryIO, Literal, NoReturn, Union
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError
|
|
26
|
+
|
|
27
|
+
PROTOCOL_VERSION = 2
|
|
28
|
+
SDK_VERSION = "0.1.0"
|
|
29
|
+
MAX_FRAME_SIZE = 64 * 1024 * 1024
|
|
30
|
+
|
|
31
|
+
_U32 = struct.Struct(">I")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProtocolError(Exception):
|
|
35
|
+
"""协议层错误的基类。
|
|
36
|
+
|
|
37
|
+
直接抛出该类型(而非子类)表示字节流已损坏或意外结束(如帧内 EOF),无法继续读取。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MalformedFrame(ProtocolError):
|
|
42
|
+
"""已完整读取一帧但无法解码;字节流仍保持帧对齐,可以跳过该帧继续读取。"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FrameTooLarge(MalformedFrame):
|
|
46
|
+
"""帧大小超过上限。
|
|
47
|
+
|
|
48
|
+
读取时超限帧的内容会被完整丢弃以保持对齐;写入时则拒绝发送。
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
size: 超限帧的字节数(不含 4 字节外层长度前缀)。
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, size: int) -> None:
|
|
55
|
+
super().__init__(f"frame of {size} bytes exceeds limit")
|
|
56
|
+
self.size = size
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Raw:
|
|
60
|
+
"""二进制载荷。
|
|
61
|
+
|
|
62
|
+
作为命令参数注解时,接收前端传入的 `Uint8Array` / `ArrayBuffer`;作为返回值或经
|
|
63
|
+
`Channel` / `App.emit` 发送时,数据放入帧体并设置 `raw: true`,前端收到 `Uint8Array`。
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
data: 原始字节,会被复制为不可变的 `bytes`。
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
bytes: 载荷内容。
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
```python
|
|
73
|
+
@app.command
|
|
74
|
+
def reverse(data: Raw) -> Raw:
|
|
75
|
+
return Raw(data.bytes[::-1])
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
__slots__ = ("bytes",)
|
|
80
|
+
|
|
81
|
+
def __init__(self, data: bytes | bytearray | memoryview) -> None:
|
|
82
|
+
self.bytes = bytes(data)
|
|
83
|
+
|
|
84
|
+
def __eq__(self, other: object) -> bool:
|
|
85
|
+
return isinstance(other, Raw) and other.bytes == self.bytes
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
return f"Raw({len(self.bytes)} bytes)"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# 字段声明顺序即线上 JSON 的字段顺序,须与 Rust `Message` 枚举保持一致(golden frames 按字节比对)。
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _Message(BaseModel):
|
|
95
|
+
"""所有协议消息的基类:严格类型校验,允许按字段名或别名填充。"""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(strict=True, populate_by_name=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Hello(_Message):
|
|
101
|
+
"""握手消息(Python → Rust),启动后发送的第一帧。
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
protocol: Python 侧的协议版本,须与 Rust 侧一致,否则启动失败(`protocol_mismatch`)。
|
|
105
|
+
sdk: SDK 版本号。
|
|
106
|
+
commands: 已注册的命令名列表(已排序)。
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
type: Literal["hello"] = "hello"
|
|
110
|
+
protocol: int
|
|
111
|
+
sdk: str = SDK_VERSION
|
|
112
|
+
commands: list[str] = []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AppInfo(_Message):
|
|
116
|
+
"""宿主 Tauri 应用的基本信息,随 `Ready` 下发。
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
name: 应用名称。
|
|
120
|
+
version: 应用版本。
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
name: str
|
|
124
|
+
version: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Ready(_Message):
|
|
128
|
+
"""握手确认(Rust → Python);收到后运行 `ready` 钩子。
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
protocol: Rust 侧的协议版本。
|
|
132
|
+
app: 宿主应用信息。
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
type: Literal["ready"] = "ready"
|
|
136
|
+
protocol: int
|
|
137
|
+
app: AppInfo
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Shutdown(_Message):
|
|
141
|
+
"""关闭通知(Rust → Python)。
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
timeout_ms: 关闭预算(毫秒),线上字段名为 `timeoutMs`。类型刻意放宽为 `Any`,
|
|
145
|
+
以便非法值不会导致整帧被丢弃,而是由运行时回退到默认的 3 秒。
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
type: Literal["shutdown"] = "shutdown"
|
|
149
|
+
timeout_ms: Any = Field(None, alias="timeoutMs")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Request(_Message):
|
|
153
|
+
"""命令调用请求(Rust → Python)。
|
|
154
|
+
|
|
155
|
+
Attributes:
|
|
156
|
+
id: 调用 id,后续的 `Response` / `Error` / `Cancel` 均以此关联。
|
|
157
|
+
cmd: 命令名。
|
|
158
|
+
channels: 参数名(camelCase)到前端 `Channel` id 的映射。
|
|
159
|
+
window: 发起调用的 webview 标签。
|
|
160
|
+
value: 内联的 JSON 参数对象。
|
|
161
|
+
raw: 为 `True` 时参数为帧体中的二进制数据。
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
type: Literal["request"] = "request"
|
|
165
|
+
id: int
|
|
166
|
+
cmd: str
|
|
167
|
+
channels: dict[str, int] = {}
|
|
168
|
+
window: str | None = None
|
|
169
|
+
value: Any = None
|
|
170
|
+
raw: bool | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Response(_Message):
|
|
174
|
+
"""命令成功返回(Python → Rust)。
|
|
175
|
+
|
|
176
|
+
Attributes:
|
|
177
|
+
id: 对应请求的调用 id。
|
|
178
|
+
value: 内联的 JSON 返回值。
|
|
179
|
+
raw: 为 `True` 时返回值为帧体中的二进制数据。
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
type: Literal["response"] = "response"
|
|
183
|
+
id: int
|
|
184
|
+
value: Any = None
|
|
185
|
+
raw: bool | None = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Error(_Message):
|
|
189
|
+
"""命令失败返回(Python → Rust)。
|
|
190
|
+
|
|
191
|
+
Attributes:
|
|
192
|
+
id: 对应请求的调用 id。
|
|
193
|
+
code: 错误码,原样透传给前端(如 `invalid_args`、`internal` 或 `TauriError` 的自定义码)。
|
|
194
|
+
message: 可读的错误描述。
|
|
195
|
+
data: 附加的 JSON 数据,可选。
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
type: Literal["error"] = "error"
|
|
199
|
+
id: int
|
|
200
|
+
code: str | None = None
|
|
201
|
+
message: str | None = None
|
|
202
|
+
data: Any = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class Cancel(_Message):
|
|
206
|
+
"""取消调用(Rust → Python)。
|
|
207
|
+
|
|
208
|
+
Attributes:
|
|
209
|
+
id: 要取消的调用 id。
|
|
210
|
+
reason: 取消原因,`"client"`(前端主动中止)或 `"timeout"`(调用超时)。
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
type: Literal["cancel"] = "cancel"
|
|
214
|
+
id: int
|
|
215
|
+
reason: str | None = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Event(_Message):
|
|
219
|
+
"""事件消息(双向)。
|
|
220
|
+
|
|
221
|
+
Python → Rust 方向使用 `target`,Rust → Python 方向使用 `window`。
|
|
222
|
+
|
|
223
|
+
Attributes:
|
|
224
|
+
event: 事件名。
|
|
225
|
+
target: 目标 webview 标签;为空时广播。
|
|
226
|
+
window: 发出事件的 webview 标签。
|
|
227
|
+
value: 内联的 JSON 载荷。
|
|
228
|
+
raw: 为 `True` 时载荷为帧体中的二进制数据。
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
type: Literal["event"] = "event"
|
|
232
|
+
event: str
|
|
233
|
+
target: str | None = None
|
|
234
|
+
window: str | None = None
|
|
235
|
+
value: Any = None
|
|
236
|
+
raw: bool | None = None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ChannelData(_Message):
|
|
240
|
+
"""向前端 `Channel` 推送一条数据(Python → Rust)。
|
|
241
|
+
|
|
242
|
+
Attributes:
|
|
243
|
+
channel: 前端 `Channel` 的 id。
|
|
244
|
+
value: 内联的 JSON 载荷。
|
|
245
|
+
raw: 为 `True` 时载荷为帧体中的二进制数据。
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
type: Literal["channel_data"] = "channel_data"
|
|
249
|
+
channel: int
|
|
250
|
+
value: Any = None
|
|
251
|
+
raw: bool | None = None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ChannelClose(_Message):
|
|
255
|
+
"""关闭前端 `Channel`(Python → Rust)。
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
channel: 前端 `Channel` 的 id。
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
type: Literal["channel_close"] = "channel_close"
|
|
262
|
+
channel: int
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
Message = Annotated[
|
|
266
|
+
Union[Hello, Ready, Shutdown, Request, Response, Error, Cancel, Event, ChannelData, ChannelClose],
|
|
267
|
+
Field(discriminator="type"),
|
|
268
|
+
]
|
|
269
|
+
# NaN / ±Infinity 不是合法 JSON。让 pydantic 将其序列化为 JSON 常量(嵌套的用户模型同样适用),
|
|
270
|
+
# encode() 才能检测并拒绝;否则 pydantic 默认会静默写成 null。
|
|
271
|
+
MESSAGE: TypeAdapter[Message] = TypeAdapter(Message, config=ConfigDict(ser_json_inf_nan="constants"))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass
|
|
275
|
+
class Frame:
|
|
276
|
+
"""一个完整的协议帧。
|
|
277
|
+
|
|
278
|
+
Attributes:
|
|
279
|
+
message: 帧头中的协议消息。
|
|
280
|
+
body: 帧体;仅当消息设置了 `raw: true` 时非空。
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
message: Message
|
|
284
|
+
body: bytes = b""
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def split_payload(value: Any) -> tuple[dict[str, Any], bytes]:
|
|
288
|
+
"""将载荷拆分为消息字段与帧体。
|
|
289
|
+
|
|
290
|
+
`Raw` / `bytes` / `bytearray` / `memoryview` 走帧体并设置 `raw: True`,其余值内联为 `value`。
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
value: 待发送的载荷。
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
二元组 `(fields, body)`:`fields` 可直接展开为消息构造参数,`body` 为帧体字节。
|
|
297
|
+
"""
|
|
298
|
+
if isinstance(value, Raw):
|
|
299
|
+
return {"raw": True}, value.bytes
|
|
300
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
301
|
+
return {"raw": True}, bytes(value)
|
|
302
|
+
return {"value": value}, b""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def join_payload(message: Request | Response | Event | ChannelData, body: bytes) -> Any:
|
|
306
|
+
"""`split_payload` 的逆操作:由消息与帧体还原载荷。
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
message: 携带载荷的消息。
|
|
310
|
+
body: 帧体。
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
`raw` 为真时返回 `Raw`,否则返回内联的 `value`(可能为 `None`)。
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
MalformedFrame: `raw` 与内联 `value` 同时出现,或未设置 `raw` 却带有帧体。
|
|
317
|
+
"""
|
|
318
|
+
if message.raw:
|
|
319
|
+
if message.value is not None:
|
|
320
|
+
raise MalformedFrame("raw payload with an inline value")
|
|
321
|
+
return Raw(body)
|
|
322
|
+
if body:
|
|
323
|
+
raise MalformedFrame("frame body without the raw flag")
|
|
324
|
+
return message.value
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _reject_constant(name: str) -> NoReturn:
|
|
328
|
+
raise ValueError(f"{name} is not valid JSON")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def encode(frame: Frame) -> bytes:
|
|
332
|
+
"""将帧编码为线上字节(含 4 字节外层长度前缀)。
|
|
333
|
+
|
|
334
|
+
值为 `None` 的字段不会写出;字段名使用别名(如 `timeoutMs`)。
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
frame: 待编码的帧。
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
完整的帧字节。
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
ValueError: 载荷包含 `NaN` / `±Infinity`,或无法序列化为 JSON。
|
|
344
|
+
"""
|
|
345
|
+
message = frame.message
|
|
346
|
+
header = MESSAGE.dump_json(message, by_alias=True, exclude={k for k, v in message if v is None})
|
|
347
|
+
if b"NaN" in header or b"Infinity" in header:
|
|
348
|
+
json.loads(header, parse_constant=_reject_constant)
|
|
349
|
+
inner_len = 4 + len(header) + len(frame.body)
|
|
350
|
+
return b"".join((_U32.pack(inner_len), _U32.pack(len(header)), header, frame.body))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def decode(buf: bytes) -> Frame:
|
|
354
|
+
"""解码一帧的内部字节(即外层长度前缀之后的部分)。
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
buf: `[u32 BE header_len][header JSON][body]`。
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
解码后的帧。
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
MalformedFrame: 长度不足、header 长度越界,或 header 无法校验为已知消息。
|
|
364
|
+
"""
|
|
365
|
+
if len(buf) < 4:
|
|
366
|
+
raise MalformedFrame("frame shorter than 4 bytes")
|
|
367
|
+
(header_len,) = _U32.unpack_from(buf)
|
|
368
|
+
end = 4 + header_len
|
|
369
|
+
if end > len(buf):
|
|
370
|
+
raise MalformedFrame("header length exceeds frame")
|
|
371
|
+
try:
|
|
372
|
+
message = MESSAGE.validate_json(buf[4:end])
|
|
373
|
+
except ValidationError as e:
|
|
374
|
+
raise MalformedFrame(f"invalid header: {e}") from None
|
|
375
|
+
return Frame(message, bytes(buf[end:]))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _read_exact(stream: BinaryIO, n: int) -> bytes | None:
|
|
379
|
+
"""精确读取 `n` 字节;在读到任何字节前遇到 EOF 返回 `None`,读到一半遇到 EOF 抛 `ProtocolError`。"""
|
|
380
|
+
chunks: list[bytes] = []
|
|
381
|
+
remaining = n
|
|
382
|
+
while remaining:
|
|
383
|
+
chunk = stream.read(remaining)
|
|
384
|
+
if not chunk:
|
|
385
|
+
if remaining == n:
|
|
386
|
+
return None
|
|
387
|
+
raise ProtocolError("unexpected EOF inside frame")
|
|
388
|
+
chunks.append(chunk)
|
|
389
|
+
remaining -= len(chunk)
|
|
390
|
+
return b"".join(chunks)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def read_frame(stream: BinaryIO, max_size: int = MAX_FRAME_SIZE) -> Frame | None:
|
|
394
|
+
"""从阻塞的二进制流中读取一帧。
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
stream: 输入流。
|
|
398
|
+
max_size: 单帧内部字节数上限。
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
解码后的帧;若流在帧边界处正常结束则返回 `None`。
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
FrameTooLarge: 帧超过 `max_size`;其内容已被读出丢弃,流仍对齐。
|
|
405
|
+
MalformedFrame: 帧读取完整但无法解码;流仍对齐。
|
|
406
|
+
ProtocolError: 帧内出现 EOF,流已不可用。
|
|
407
|
+
"""
|
|
408
|
+
prefix = _read_exact(stream, 4)
|
|
409
|
+
if prefix is None:
|
|
410
|
+
return None
|
|
411
|
+
(size,) = _U32.unpack(prefix)
|
|
412
|
+
if size > max_size:
|
|
413
|
+
# 分块丢弃而不是一次性读入,避免为超限帧分配大块内存,同时保持流对齐。
|
|
414
|
+
remaining = size
|
|
415
|
+
while remaining:
|
|
416
|
+
chunk = stream.read(min(remaining, 1 << 16))
|
|
417
|
+
if not chunk:
|
|
418
|
+
raise ProtocolError("unexpected EOF inside frame")
|
|
419
|
+
remaining -= len(chunk)
|
|
420
|
+
raise FrameTooLarge(size)
|
|
421
|
+
body = _read_exact(stream, size)
|
|
422
|
+
if body is None:
|
|
423
|
+
raise ProtocolError("unexpected EOF inside frame")
|
|
424
|
+
return decode(body)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class Writer:
|
|
428
|
+
"""线程安全的帧写入器。
|
|
429
|
+
|
|
430
|
+
事件循环与线程池中的同步处理器会并发写出,整帧写入与 flush 在同一把锁内完成,
|
|
431
|
+
保证帧不会交错。
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
stream: 协议输出流。
|
|
435
|
+
|
|
436
|
+
Attributes:
|
|
437
|
+
max_size: 单帧内部字节数上限,默认 `MAX_FRAME_SIZE`。
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
def __init__(self, stream: BinaryIO) -> None:
|
|
441
|
+
self._stream = stream
|
|
442
|
+
self._lock = threading.Lock()
|
|
443
|
+
self.max_size = MAX_FRAME_SIZE
|
|
444
|
+
|
|
445
|
+
def write(self, message: Message, body: bytes = b"") -> None:
|
|
446
|
+
"""编码并写出一帧,随即 flush。
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
message: 协议消息。
|
|
450
|
+
body: 帧体。
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
FrameTooLarge: 编码后超过 `max_size`,不会写出任何字节。
|
|
454
|
+
ValueError: 载荷无法编码为合法 JSON(见 `encode`)。
|
|
455
|
+
"""
|
|
456
|
+
data = encode(Frame(message, body))
|
|
457
|
+
if len(data) - 4 > self.max_size:
|
|
458
|
+
raise FrameTooLarge(len(data) - 4)
|
|
459
|
+
with self._lock:
|
|
460
|
+
self._stream.write(data)
|
|
461
|
+
self._stream.flush()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def take_over_stdio() -> tuple[BinaryIO, BinaryIO]:
|
|
465
|
+
"""接管 stdio:fd 0/1 专供协议使用,其余输出改道到 stderr / devnull。
|
|
466
|
+
|
|
467
|
+
复制原 fd 0/1 作为协议流后,fd 0 指向 devnull,fd 1 指向 stderr,`sys.stdin` 换成
|
|
468
|
+
devnull,`sys.stdout` 换成 `sys.stderr`。这样 `print()` 及子进程的 stdout 都不会污染
|
|
469
|
+
协议流,而是进入 stderr,由宿主按行转发到日志。
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
二元组 `(proto_in, proto_out)`:协议输入流与输出流。
|
|
473
|
+
"""
|
|
474
|
+
sys.stdout.flush()
|
|
475
|
+
proto_in = os.fdopen(os.dup(0), "rb")
|
|
476
|
+
proto_out = os.fdopen(os.dup(1), "wb")
|
|
477
|
+
null = os.open(os.devnull, os.O_RDONLY)
|
|
478
|
+
os.dup2(null, 0)
|
|
479
|
+
os.close(null)
|
|
480
|
+
os.dup2(2, 1)
|
|
481
|
+
sys.stdin = open(os.devnull, encoding="utf-8")
|
|
482
|
+
sys.stdout = sys.stderr
|
|
483
|
+
return proto_in, proto_out
|
tauri_runtime/py.typed
ADDED
|
File without changes
|
tauri_runtime/router.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""命令注册:对外的分组路由 `Router`,以及运行时内部的命令表 `CommandTable`。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .command import Handler
|
|
9
|
+
|
|
10
|
+
LIFECYCLE = ("startup", "ready", "shutdown")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def join_name(prefix: str, name: str) -> str:
|
|
14
|
+
"""用 `.` 拼接命令名前缀与命令名;前缀为空时原样返回命令名。"""
|
|
15
|
+
return f"{prefix}.{name}" if prefix else name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_hook(hook: str) -> None:
|
|
19
|
+
"""校验生命周期钩子名;未知钩子名抛出 `ValueError`。"""
|
|
20
|
+
if hook not in LIFECYCLE:
|
|
21
|
+
raise ValueError(f"unknown lifecycle hook '{hook}', expected one of {LIFECYCLE}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Router:
|
|
25
|
+
"""命令、事件监听器与生命周期钩子的分组,用于按模块组织大型项目。
|
|
26
|
+
|
|
27
|
+
业务模块只需创建并填充自己的 `Router`,无需导入 `App` 实例,由入口文件统一通过
|
|
28
|
+
`App.include()` 挂载,从而避免循环导入。`prefix` 以 `.` 拼接到命令名之前,嵌套
|
|
29
|
+
`include()` 时逐级拼接;事件名与钩子不受前缀影响。
|
|
30
|
+
|
|
31
|
+
注册只做收集:处理器签名的校验与命令名重复检查在挂载到 `App` 时进行,挂载时取当时
|
|
32
|
+
已注册内容的快照,之后再向该 `Router` 添加的内容不会生效。
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```python
|
|
36
|
+
# user/commands.py
|
|
37
|
+
from tauri_runtime import Router
|
|
38
|
+
|
|
39
|
+
router = Router(prefix="user")
|
|
40
|
+
|
|
41
|
+
@router.command
|
|
42
|
+
async def login(name: str) -> str: # 前端调用名为 "user.login"
|
|
43
|
+
return f"welcome {name}"
|
|
44
|
+
|
|
45
|
+
def logout() -> None: ...
|
|
46
|
+
|
|
47
|
+
router.add_command(logout)
|
|
48
|
+
|
|
49
|
+
# main.py
|
|
50
|
+
from tauri_runtime import App
|
|
51
|
+
from user.commands import router as user_router
|
|
52
|
+
|
|
53
|
+
app = App()
|
|
54
|
+
app.include(user_router)
|
|
55
|
+
app.run()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
prefix: 命令名前缀,如 `"user"`;空字符串表示不加前缀。不能以 `.` 开头或结尾。
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, prefix: str = "") -> None:
|
|
63
|
+
if prefix.startswith(".") or prefix.endswith("."):
|
|
64
|
+
raise ValueError(f"invalid router prefix '{prefix}': must not start or end with '.'")
|
|
65
|
+
self.prefix = prefix
|
|
66
|
+
self.commands: list[tuple[str, Callable[..., Any]]] = []
|
|
67
|
+
self.listeners: list[tuple[str, Callable[..., Any]]] = []
|
|
68
|
+
self.hooks: list[tuple[str, Callable[..., Any]]] = []
|
|
69
|
+
|
|
70
|
+
# 命令式注册 ---------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def add_command(self, func: Callable[..., Any], name: str | None = None) -> None:
|
|
73
|
+
"""注册命令;`name` 省略时使用函数名,最终命令名会加上本 `Router` 的前缀。"""
|
|
74
|
+
self.commands.append((join_name(self.prefix, name or func.__name__), func))
|
|
75
|
+
|
|
76
|
+
def add_commands(self, *funcs: Callable[..., Any]) -> None:
|
|
77
|
+
"""批量注册命令,命令名均取函数名。"""
|
|
78
|
+
for func in funcs:
|
|
79
|
+
self.add_command(func)
|
|
80
|
+
|
|
81
|
+
def add_listener(self, event: str, func: Callable[..., Any]) -> None:
|
|
82
|
+
"""注册事件监听器;事件名不加前缀。"""
|
|
83
|
+
self.listeners.append((event, func))
|
|
84
|
+
|
|
85
|
+
def add_hook(self, hook: str, func: Callable[..., Any]) -> None:
|
|
86
|
+
"""注册生命周期钩子(`"startup"` / `"ready"` / `"shutdown"`)。"""
|
|
87
|
+
check_hook(hook)
|
|
88
|
+
self.hooks.append((hook, func))
|
|
89
|
+
|
|
90
|
+
def include(self, router: Router) -> None:
|
|
91
|
+
"""把另一个 `Router` 的内容并入本 `Router`,其命令名再加上本 `Router` 的前缀。"""
|
|
92
|
+
self.commands.extend((join_name(self.prefix, name), func) for name, func in router.commands)
|
|
93
|
+
self.listeners.extend(router.listeners)
|
|
94
|
+
self.hooks.extend(router.hooks)
|
|
95
|
+
|
|
96
|
+
# 装饰器 -------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def command(self, name_or_func: Any = None) -> Any:
|
|
99
|
+
"""注册命令的装饰器,用法同 `App.command`:`@router.command` 或 `@router.command("name")`。"""
|
|
100
|
+
if callable(name_or_func):
|
|
101
|
+
self.add_command(name_or_func)
|
|
102
|
+
return name_or_func
|
|
103
|
+
|
|
104
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
105
|
+
self.add_command(func, name_or_func)
|
|
106
|
+
return func
|
|
107
|
+
|
|
108
|
+
return decorator
|
|
109
|
+
|
|
110
|
+
def listen(self, event: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
111
|
+
"""注册事件监听器的装饰器,用法同 `App.listen`。"""
|
|
112
|
+
|
|
113
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
114
|
+
self.add_listener(event, func)
|
|
115
|
+
return func
|
|
116
|
+
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
119
|
+
def on(self, hook: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
120
|
+
"""注册生命周期钩子的装饰器,用法同 `App.on`。"""
|
|
121
|
+
check_hook(hook)
|
|
122
|
+
|
|
123
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
124
|
+
self.add_hook(hook, func)
|
|
125
|
+
return func
|
|
126
|
+
|
|
127
|
+
return decorator
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CommandTable:
|
|
131
|
+
"""运行时内部的命令名到 `Handler` 的映射;命令名不可重复注册。"""
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
self._handlers: dict[str, Handler] = {}
|
|
135
|
+
|
|
136
|
+
def add(self, handler: Handler) -> None:
|
|
137
|
+
"""注册处理器;命令名已存在时抛出 `ValueError`。"""
|
|
138
|
+
if handler.name in self._handlers:
|
|
139
|
+
raise ValueError(f"command '{handler.name}' already registered")
|
|
140
|
+
self._handlers[handler.name] = handler
|
|
141
|
+
|
|
142
|
+
def get(self, name: str) -> Handler | None:
|
|
143
|
+
return self._handlers.get(name)
|
|
144
|
+
|
|
145
|
+
def names(self) -> list[str]:
|
|
146
|
+
"""已注册的命令名(排序后),用于 `Hello` 握手。"""
|
|
147
|
+
return sorted(self._handlers)
|