procproxy 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.
- procproxy-0.1.0.dist-info/METADATA +489 -0
- procproxy-0.1.0.dist-info/RECORD +45 -0
- procproxy-0.1.0.dist-info/WHEEL +5 -0
- procproxy-0.1.0.dist-info/entry_points.txt +2 -0
- procproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
- procproxy-0.1.0.dist-info/top_level.txt +1 -0
- proxycore/__init__.py +7 -0
- proxycore/__main__.py +65 -0
- proxycore/api.py +226 -0
- proxycore/backend/__init__.py +49 -0
- proxycore/backend/base.py +159 -0
- proxycore/backend/null.py +51 -0
- proxycore/backend/windivert/__init__.py +657 -0
- proxycore/backend/windivert/filter.py +123 -0
- proxycore/backend/windivert/pidmap.py +289 -0
- proxycore/backend/windivert/pkt.py +424 -0
- proxycore/canproxy.py +229 -0
- proxycore/checkroute.py +247 -0
- proxycore/cli.py +605 -0
- proxycore/config.py +1010 -0
- proxycore/dns/__init__.py +46 -0
- proxycore/dns/cache.py +209 -0
- proxycore/dns/modes.py +451 -0
- proxycore/dns/udp.py +606 -0
- proxycore/forward.py +572 -0
- proxycore/ipc.py +637 -0
- proxycore/log.py +298 -0
- proxycore/pathsetup.py +112 -0
- proxycore/probe.py +168 -0
- proxycore/proc.py +342 -0
- proxycore/rules.py +730 -0
- proxycore/runtime.py +654 -0
- proxycore/service/__init__.py +536 -0
- proxycore/service/__main__.py +124 -0
- proxycore/tunnel/__init__.py +8 -0
- proxycore/tunnel/relay.py +393 -0
- proxycore/tunnel/sessions.py +315 -0
- proxycore/tunnel/sink.py +325 -0
- proxycore/types.py +165 -0
- proxycore/upstream/__init__.py +52 -0
- proxycore/upstream/base.py +150 -0
- proxycore/upstream/chain.py +92 -0
- proxycore/upstream/http_connect.py +148 -0
- proxycore/upstream/socks4.py +106 -0
- proxycore/upstream/socks5.py +266 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""双向转发(事件驱动,不用 `time.sleep` 轮询)。
|
|
2
|
+
|
|
3
|
+
实现:基于跨平台 `selectors` 的 reactor(Windows 上用 select)。
|
|
4
|
+
|
|
5
|
+
语义要求:
|
|
6
|
+
* 半关闭:一端 FIN 后把另一向 `shutdown(SHUT_WR)`,在对端也 FIN 或超时前保持另一向;
|
|
7
|
+
* 背压:只在 `send()` 能全部写走时继续读,否则挂 写事件 等可写(不忙等);
|
|
8
|
+
* 计数:`bytes_up`(客户端→目标)、`bytes_down`(目标→客户端);
|
|
9
|
+
* 空闲与半关闭超时。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
import selectors
|
|
16
|
+
import socket
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
CHUNK = 65536
|
|
21
|
+
# 单连接单方向允许堆积的缓冲上限(背压阈值)
|
|
22
|
+
MAX_BUFFER = 4 * 1024 * 1024
|
|
23
|
+
|
|
24
|
+
REASON_OK = "ok"
|
|
25
|
+
REASON_IDLE_TIMEOUT = "idle-timeout"
|
|
26
|
+
REASON_HALF_CLOSE_TIMEOUT = "half-close-timeout"
|
|
27
|
+
REASON_CLIENT_RESET = "client-reset"
|
|
28
|
+
REASON_UPSTREAM_RESET = "upstream-reset"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class _Direction:
|
|
33
|
+
"""一个方向的转发状态。"""
|
|
34
|
+
|
|
35
|
+
src: socket.socket
|
|
36
|
+
dst: socket.socket
|
|
37
|
+
is_up: bool
|
|
38
|
+
buffer: bytearray = field(default_factory=bytearray)
|
|
39
|
+
src_closed: bool = False
|
|
40
|
+
dst_shutdown: bool = False
|
|
41
|
+
done: bool = False
|
|
42
|
+
rst: bool = False
|
|
43
|
+
bytes_moved: int = 0
|
|
44
|
+
|
|
45
|
+
def flush(self) -> bool:
|
|
46
|
+
"""尽力写走缓冲;返回 True 表示缓冲已空。"""
|
|
47
|
+
while self.buffer:
|
|
48
|
+
try:
|
|
49
|
+
sent = self.dst.send(self.buffer)
|
|
50
|
+
except BlockingIOError:
|
|
51
|
+
return False
|
|
52
|
+
except OSError:
|
|
53
|
+
raise
|
|
54
|
+
if sent <= 0: # pragma: no cover - 对端已关闭
|
|
55
|
+
return False
|
|
56
|
+
del self.buffer[:sent]
|
|
57
|
+
self.bytes_moved += sent
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class _Watch:
|
|
63
|
+
"""selector 的注册单位:按 socket 注册,不能按方向注册。
|
|
64
|
+
|
|
65
|
+
socketpair 是双工的:一个 socket 既是"某条方向的读端",又是"另一条方向的写端"。
|
|
66
|
+
旧实现把 `EVENT_WRITE` 挂在 `direction.src` 上,而数据是往 `direction.dst` 写的,
|
|
67
|
+
于是"等对端可写"实际等的是读端,两个 socket 都几乎永远可写,`select()`
|
|
68
|
+
立刻返回,整个循环退化成 100% CPU 忙等,真正的背压从未被等待。
|
|
69
|
+
实测后果:1MB 回程在有背压时只送出去 128KB 就收工(见 `test_relay_1mb_*`)。
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
sock: socket.socket
|
|
73
|
+
read_from: _Direction # 这个 socket 是它的 src
|
|
74
|
+
write_to: _Direction # 这个 socket 是它的 dst
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _finish(direction: _Direction) -> bool:
|
|
78
|
+
"""缓冲区已空 → 补发 FIN 并宣告这一向结束;返回"是否已结束"。
|
|
79
|
+
|
|
80
|
+
FIN 必须跟在数据后面。 旧实现在读到 EOF 时先 `shutdown(SHUT_WR)` 再 flush,
|
|
81
|
+
结果 FIN 超车插到还没发出去的数据前面:上游立刻看到 EOF 提前收工,剩下的数据
|
|
82
|
+
在 Windows 上又会因"关闭后再 send"报 `10058` 直接丢掉,响应被截断。
|
|
83
|
+
"""
|
|
84
|
+
if direction.buffer:
|
|
85
|
+
return False # 还没写完,FIN 得等
|
|
86
|
+
if not direction.dst_shutdown:
|
|
87
|
+
_shutdown_write(direction.dst)
|
|
88
|
+
direction.dst_shutdown = True
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def relay_bidirectional(
|
|
93
|
+
client: socket.socket,
|
|
94
|
+
upstream: socket.socket,
|
|
95
|
+
*,
|
|
96
|
+
idle_timeout: float = 300.0,
|
|
97
|
+
half_close_grace: float = 30.0,
|
|
98
|
+
response_grace: float = 2.0,
|
|
99
|
+
drain_grace: float = 2.0,
|
|
100
|
+
chunk: int = CHUNK,
|
|
101
|
+
) -> tuple[int, int, str]:
|
|
102
|
+
"""把 `client` 与 `upstream` 双向对接,返回 `(bytes_up, bytes_down, reason)`。
|
|
103
|
+
|
|
104
|
+
`response_grace`:客户端干净 FIN 之后,再给"响应"这么多时间。
|
|
105
|
+
这样既不会像"等双向都 FIN"那样在长连接上卡到 idle 超时,
|
|
106
|
+
也不会像"立刻退出"那样把还没到达的响应掐掉。
|
|
107
|
+
|
|
108
|
+
`drain_grace`:对端还有未被读走的残留字节时,最多再等这么久。
|
|
109
|
+
防止"发完就 FIN、不读响应"的客户端把连接挂到 `half_close_grace`。
|
|
110
|
+
"""
|
|
111
|
+
directions = [
|
|
112
|
+
_Direction(src=client, dst=upstream, is_up=True),
|
|
113
|
+
_Direction(src=upstream, dst=client, is_up=False),
|
|
114
|
+
]
|
|
115
|
+
for sock in (client, upstream):
|
|
116
|
+
with contextlib.suppress(OSError):
|
|
117
|
+
sock.setblocking(False)
|
|
118
|
+
|
|
119
|
+
up, down = directions[0], directions[1]
|
|
120
|
+
watch = [
|
|
121
|
+
_Watch(sock=client, read_from=up, write_to=down),
|
|
122
|
+
_Watch(sock=upstream, read_from=down, write_to=up),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
sel = selectors.DefaultSelector()
|
|
126
|
+
for entry in watch:
|
|
127
|
+
sel.register(entry.sock, _mask_for(entry), entry)
|
|
128
|
+
|
|
129
|
+
last_activity = time.monotonic()
|
|
130
|
+
half_closed_at: float | None = None
|
|
131
|
+
request_closed_at: float | None = None # 客户端干净 FIN 的时刻
|
|
132
|
+
response_seen = False
|
|
133
|
+
upstream_quiet_since: float | None = None # 上游恢复安静的时刻(响应已发完)
|
|
134
|
+
unread_since: float | None = None # 首次发现"对端还有未读字节"的时刻
|
|
135
|
+
reason = REASON_OK
|
|
136
|
+
|
|
137
|
+
def request_still_pending() -> bool:
|
|
138
|
+
"""请求方向是否还有"没送出去"的东西(缓冲没写空 / 源还没读到 EOF)。
|
|
139
|
+
|
|
140
|
+
为什么必须有这个判断:
|
|
141
|
+
`request_closed_at` 只记录"收到客户端 FIN"的时刻,而 FIN 不等于"数据已送达"
|
|
142
|
+
并发/高负载下,1MB 的尾部可能还堆在我们自己的发送缓冲里没 flush 出去。
|
|
143
|
+
如果这时候按 `response_grace` 直接收工,尾部就丢了,
|
|
144
|
+
表现是偶发的"数据短一截 / sha256 对不上"(这种 bug 最难查)。
|
|
145
|
+
所以:只要请求方向还有待发数据,任何"温和收工"都不许生效。
|
|
146
|
+
"""
|
|
147
|
+
return bool(up.buffer) or not up.src_closed
|
|
148
|
+
|
|
149
|
+
def response_still_pending() -> bool:
|
|
150
|
+
"""回程方向是否还有"没送到客户端"的东西。
|
|
151
|
+
|
|
152
|
+
与 `request_still_pending()` 同源:任何"温和收工"都必须等两个方向的缓冲
|
|
153
|
+
都写空。只检查请求方向的话,上游一 FIN 就会带着满缓冲的回程收工,同样是截断。
|
|
154
|
+
"""
|
|
155
|
+
return bool(down.buffer)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
while True:
|
|
159
|
+
now = time.monotonic()
|
|
160
|
+
if now - last_activity > idle_timeout:
|
|
161
|
+
reason = REASON_IDLE_TIMEOUT
|
|
162
|
+
break
|
|
163
|
+
pending = request_still_pending() or response_still_pending()
|
|
164
|
+
if (
|
|
165
|
+
request_closed_at is not None
|
|
166
|
+
and now - request_closed_at > response_grace
|
|
167
|
+
and not response_seen
|
|
168
|
+
and not pending
|
|
169
|
+
):
|
|
170
|
+
# 客户端已发完、请求也确实全部送达、且一直没响应 → 正常收工(不等 idle 超时)
|
|
171
|
+
break
|
|
172
|
+
if upstream_quiet_since is not None and now - upstream_quiet_since > response_grace and not pending:
|
|
173
|
+
# 响应已经发完并转发给客户端,上游也安静了 → 正常收工
|
|
174
|
+
break
|
|
175
|
+
if unread_since is not None and now - unread_since > drain_grace and not pending:
|
|
176
|
+
# 对端一直不读走残留字节(例如"发完就 FIN、不读响应"的客户端)
|
|
177
|
+
# → 不能无限等,按 drain_grace 收工,避免把连接挂到 half_close_grace。
|
|
178
|
+
break
|
|
179
|
+
if half_closed_at is not None and now - half_closed_at > half_close_grace:
|
|
180
|
+
reason = REASON_HALF_CLOSE_TIMEOUT
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
# 先把积压数据推出去(非阻塞,能写多少写多少)
|
|
184
|
+
# 注意变量名别叫 entry,那是 `_Watch`(上面注册用的),mypy 会串型。
|
|
185
|
+
for direction in directions:
|
|
186
|
+
if not direction.buffer:
|
|
187
|
+
continue
|
|
188
|
+
try:
|
|
189
|
+
direction.flush()
|
|
190
|
+
except OSError:
|
|
191
|
+
reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
|
|
192
|
+
return _result(directions, reason)
|
|
193
|
+
if direction.src_closed and _finish(direction):
|
|
194
|
+
direction.done = True
|
|
195
|
+
if all(d.done for d in directions):
|
|
196
|
+
return _result(directions, REASON_OK)
|
|
197
|
+
|
|
198
|
+
# 等"要写的那个 socket"可写,不是等读端(见 `_Watch`)
|
|
199
|
+
_sync_masks(sel, watch)
|
|
200
|
+
|
|
201
|
+
events = sel.select(timeout=0.25)
|
|
202
|
+
if not events:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
alive = True
|
|
206
|
+
for key, event_mask in events:
|
|
207
|
+
entry = key.data
|
|
208
|
+
assert isinstance(entry, _Watch)
|
|
209
|
+
if event_mask & selectors.EVENT_WRITE:
|
|
210
|
+
direction = entry.write_to
|
|
211
|
+
try:
|
|
212
|
+
drained = direction.flush()
|
|
213
|
+
except OSError:
|
|
214
|
+
reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
|
|
215
|
+
return _result(directions, reason)
|
|
216
|
+
if drained:
|
|
217
|
+
if (
|
|
218
|
+
not direction.is_up
|
|
219
|
+
and request_closed_at is not None
|
|
220
|
+
and response_seen
|
|
221
|
+
):
|
|
222
|
+
# 回程缓冲写空 → 开始计时"上游安静"
|
|
223
|
+
upstream_quiet_since = time.monotonic()
|
|
224
|
+
if direction.src_closed and _finish(direction):
|
|
225
|
+
direction.done = True
|
|
226
|
+
if all(d.done for d in directions):
|
|
227
|
+
return _result(directions, REASON_OK)
|
|
228
|
+
direction = entry.read_from
|
|
229
|
+
if event_mask & selectors.EVENT_READ and not direction.src_closed:
|
|
230
|
+
try:
|
|
231
|
+
data = direction.src.recv(chunk)
|
|
232
|
+
except BlockingIOError:
|
|
233
|
+
continue
|
|
234
|
+
except OSError:
|
|
235
|
+
# 连接被重置(RST)而不是干净 FIN
|
|
236
|
+
direction.src_closed = True
|
|
237
|
+
direction.rst = True
|
|
238
|
+
data = b""
|
|
239
|
+
if data:
|
|
240
|
+
last_activity = time.monotonic()
|
|
241
|
+
if not direction.is_up:
|
|
242
|
+
response_seen = True # 已经看到响应
|
|
243
|
+
upstream_quiet_since = None
|
|
244
|
+
if _append(direction, data):
|
|
245
|
+
try:
|
|
246
|
+
flushed = direction.flush()
|
|
247
|
+
except OSError:
|
|
248
|
+
reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
|
|
249
|
+
return _result(directions, reason)
|
|
250
|
+
if flushed and not direction.is_up and request_closed_at is not None:
|
|
251
|
+
# 响应已全部写回客户端 → 开始计时"上游安静"
|
|
252
|
+
upstream_quiet_since = time.monotonic()
|
|
253
|
+
else:
|
|
254
|
+
# 读到 EOF:把 FIN 传给对端,然后按"这一向已结束"处理
|
|
255
|
+
direction.src_closed = True
|
|
256
|
+
# 顺序不能反:先 flush 再 FIN。缓冲区没写空时 `_finish()`
|
|
257
|
+
# 会拒绝发 FIN,等写事件把缓冲推空后再补,否则 FIN 超车,
|
|
258
|
+
# 上游提前收工,剩下的数据在 Windows 上还会因"关闭后再 send"
|
|
259
|
+
# 报 10058 丢干净(实测 1MB 只回来 128KB)。
|
|
260
|
+
try:
|
|
261
|
+
direction.flush()
|
|
262
|
+
except OSError:
|
|
263
|
+
reason = REASON_UPSTREAM_RESET if direction.is_up else REASON_CLIENT_RESET
|
|
264
|
+
return _result(directions, reason)
|
|
265
|
+
direction.done = _finish(direction)
|
|
266
|
+
|
|
267
|
+
if direction.rst:
|
|
268
|
+
# RST:没有 FIN 语义,等另一半自然结束或超时
|
|
269
|
+
if half_closed_at is None:
|
|
270
|
+
half_closed_at = time.monotonic()
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
if all(d.done for d in directions):
|
|
274
|
+
return _result(directions, REASON_OK)
|
|
275
|
+
|
|
276
|
+
if direction.is_up:
|
|
277
|
+
# 客户端干净 FIN 且请求已全部转发:
|
|
278
|
+
# 给响应留 `response_grace`,而不是傻等对端也 FIN。
|
|
279
|
+
if _peer_has_unread(direction.dst) or request_still_pending():
|
|
280
|
+
# 关键:对端(客户端)写端虽然收到 FIN,但内核里可能还有
|
|
281
|
+
# 未读走的字节(并发 sendall 的最后一段);
|
|
282
|
+
# 或者是我们自己还有没 flush 出去的待发数据。
|
|
283
|
+
# 这两种情况下收工都会丢尾部(表现为 1MB sha256 不一致)。
|
|
284
|
+
# 但也不能无限等(有客户端"发完就 FIN、根本不读"),
|
|
285
|
+
# 所以记一个起始时刻,由 drain_grace 兜底。
|
|
286
|
+
if unread_since is None:
|
|
287
|
+
unread_since = time.monotonic()
|
|
288
|
+
if half_closed_at is None:
|
|
289
|
+
half_closed_at = time.monotonic()
|
|
290
|
+
continue
|
|
291
|
+
unread_since = None
|
|
292
|
+
if request_closed_at is None:
|
|
293
|
+
request_closed_at = time.monotonic()
|
|
294
|
+
else:
|
|
295
|
+
# 上游 FIN:响应到此为止;
|
|
296
|
+
# 只有"客户端此前已 FIN + 没有残留未读 + 请求方向确实发完了"才能立刻收工
|
|
297
|
+
if (
|
|
298
|
+
request_closed_at is not None
|
|
299
|
+
and not _peer_has_unread(direction.dst)
|
|
300
|
+
and not request_still_pending()
|
|
301
|
+
and not response_still_pending() # 回程缓冲没写空就不许收工
|
|
302
|
+
):
|
|
303
|
+
return _result(directions, REASON_OK)
|
|
304
|
+
if half_closed_at is None:
|
|
305
|
+
half_closed_at = time.monotonic()
|
|
306
|
+
if not alive: # pragma: no cover - 防御性分支
|
|
307
|
+
break
|
|
308
|
+
finally:
|
|
309
|
+
sel.close()
|
|
310
|
+
|
|
311
|
+
return _result(directions, reason)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _append(direction: _Direction, data: bytes) -> bool:
|
|
315
|
+
"""写入待发缓冲;返回 True 表示需要尝试 flush。"""
|
|
316
|
+
if len(direction.buffer) + len(data) > MAX_BUFFER:
|
|
317
|
+
# 背压:超过上限时只保留到上限(多余丢弃会破坏 TCP 语义),
|
|
318
|
+
# 因此这里改为阻塞式写走,保证不丢数据(fd 已是非阻塞,用循环 + 短暂等待)。
|
|
319
|
+
direction.buffer.extend(data)
|
|
320
|
+
_drain_blocking(direction)
|
|
321
|
+
return False
|
|
322
|
+
direction.buffer.extend(data)
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _drain_blocking(direction: _Direction) -> None:
|
|
327
|
+
"""缓冲超限时的兜底:带超时地写走(避免无界内存增长)。"""
|
|
328
|
+
deadline = time.monotonic() + 30.0
|
|
329
|
+
sel = selectors.DefaultSelector()
|
|
330
|
+
try:
|
|
331
|
+
sel.register(direction.dst, selectors.EVENT_WRITE)
|
|
332
|
+
while direction.buffer and time.monotonic() < deadline:
|
|
333
|
+
if direction.flush():
|
|
334
|
+
return
|
|
335
|
+
sel.select(timeout=0.5)
|
|
336
|
+
finally:
|
|
337
|
+
sel.close()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _mask_for(entry: _Watch) -> int:
|
|
341
|
+
"""这个 socket 现在要等什么:读它(某条方向的 src)还是写它(另一条方向的 dst)。"""
|
|
342
|
+
mask = 0
|
|
343
|
+
if not entry.read_from.src_closed:
|
|
344
|
+
mask |= selectors.EVENT_READ
|
|
345
|
+
if entry.write_to.buffer: # 有积压才等可写,否则就是忙等
|
|
346
|
+
mask |= selectors.EVENT_WRITE
|
|
347
|
+
return mask
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _sync_masks(sel: selectors.BaseSelector, watch: list[_Watch]) -> None:
|
|
351
|
+
for entry in watch:
|
|
352
|
+
mask = _mask_for(entry)
|
|
353
|
+
try:
|
|
354
|
+
if mask:
|
|
355
|
+
sel.modify(entry.sock, mask, entry)
|
|
356
|
+
else:
|
|
357
|
+
sel.unregister(entry.sock)
|
|
358
|
+
except (KeyError, ValueError): # pragma: no cover - 已注销
|
|
359
|
+
if mask:
|
|
360
|
+
with contextlib.suppress(KeyError, ValueError):
|
|
361
|
+
sel.register(entry.sock, mask, entry)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _peer_has_unread(sock: socket.socket) -> bool:
|
|
365
|
+
"""对端 socket 里是否还有未被我们读走的字节。
|
|
366
|
+
|
|
367
|
+
用途:决定"收到 FIN 后能不能立刻收工"。
|
|
368
|
+
只要内核缓冲里还有数据,收工就会把它丢掉(并发写入场景下表现为偶发截断)。
|
|
369
|
+
非阻塞 peek 失败时返回 False(保守放行,由超时兜底)。
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
peek = getattr(sock, "recv", None)
|
|
373
|
+
if peek is None: # pragma: no cover - 理论不可达
|
|
374
|
+
return False
|
|
375
|
+
data = sock.recv(1, socket.MSG_PEEK)
|
|
376
|
+
return bool(data)
|
|
377
|
+
except BlockingIOError:
|
|
378
|
+
return False
|
|
379
|
+
except OSError:
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _result(directions: list[_Direction], reason: str) -> tuple[int, int, str]:
|
|
384
|
+
up = next((d.bytes_moved for d in directions if d.is_up), 0)
|
|
385
|
+
down = next((d.bytes_moved for d in directions if not d.is_up), 0)
|
|
386
|
+
return up, down, reason
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _shutdown_write(sock: socket.socket) -> None:
|
|
390
|
+
# 对端已 RST 时 shutdown 会报 ENOTCONN/WSAENOTCONN,这类错误没有补救动作;
|
|
391
|
+
# 统一忽略(之前的分支两个方向都是 pass,等价于吞掉所有 OSError)。
|
|
392
|
+
with contextlib.suppress(OSError):
|
|
393
|
+
sock.shutdown(socket.SHUT_WR)
|