Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
66de9bd
refactor(connect_tcp): fold _send_raw into send_message(is_send_raw)
F18-Ray Oct 25, 2026
0e34c63
feat(connect_tcp): record outbound messages and notify send listeners
F18-Ray Oct 25, 2026
5597e29
feat(connect_tcp): record only control sockets; notify side-channel r…
F18-Ray Oct 25, 2026
a6d217b
test(tcp): wait for server registration and transfer port before use
F18-Ray Oct 25, 2026
9a4ce52
Docs: add the changes log and updating docs
F18-Ray Oct 25, 2026
1c8c5b3
lint(connect_tcp): lint the code style
F18-Ray Oct 26, 2026
0098bef
tests: fix the _StallingSocket hook in test_slow_sender_does_not_reor…
F18-Ray Oct 27, 2026
9bc2202
refactor(connect_tcp): refactor the connect resumming functions more …
F18-Ray Oct 28, 2026
04e3306
tests: updating the resume tests
F18-Ray Oct 29, 2026
421c2e1
feat(connect_tcp): fix the file folder transfering the empty folder c…
F18-Ray Oct 29, 2026
cce4547
refactor(connect_tcp): reconstruct the fid allocation functions
F18-Ray Oct 29, 2026
7b84660
refactor(connect_tcp): reconstructed the fid allocation functions and…
F18-Ray Nov 2, 2026
1ff6472
feat(connect_tcp): add the _fid_routes for routing the fid between or…
F18-Ray Nov 2, 2026
14a55d1
feat(connect_tcp): fix the ack finished transfering package sending t…
F18-Ray Nov 2, 2026
bae2491
feat(connnect_tcp): fix the reciever also deleted the file transferin…
F18-Ray Nov 2, 2026
1e008bc
feat(connect_tcp): add the delete_config function in server and clien…
F18-Ray Nov 3, 2026
5002502
refactor(connect_tcp): merge the resume channels in common file trans…
F18-Ray Nov 3, 2026
d94ed01
refactor(connect_tcp): unificated the fid allocator for the file tran…
F18-Ray Nov 5, 2026
ea1ebc3
refactor(connect_tcp): fix the default ports conflicted but unchange …
F18-Ray Nov 5, 2026
6c354cb
tests: fix the tests errors
F18-Ray Nov 5, 2026
94f7372
fix(connect_tcp): compact bounded transfer sign buffers
F18-Ray Nov 8, 2026
6d6296a
fix(connect_tcp): fix the port bind errors on windows
F18-Ray Nov 9, 2026
51443ce
test: fix the port checking test errors on windows
F18-Ray Nov 9, 2026
4004f8d
fix(connect_tcp): resume using the connected control endpoint
F18-Ray Nov 11, 2026
ab06bcb
fix(connect_tcp): release paused legs when a reconnect sweeps them
F18-Ray Nov 11, 2026
fe847f5
test: wait for both broadcast receivers before reading the destination
F18-Ray Nov 11, 2026
f358ae3
docs: close the resume identity and pause-sweep change
F18-Ray Nov 11, 2026
ef33b43
fix(connect_tcp): notify broadcast listeners outside client_lock
F18-Ray Nov 12, 2026
bbf29b3
fix(connect_tcp): stop listeners on close and join the receive thread
F18-Ray Nov 12, 2026
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ Agent 先不写代码,先交计划。人类明确批准后,才进入执行
- [ ] 路径清单逐条有结论
- [ ] 全局属性有运行时证据
- [ ] 测试代码未被弱化
- [ ] 身份/续传类改动的验收覆盖配置别名与通配绑定,测试以内容或事件而非文件存在作为完成条件

**合并前**

Expand Down
4,279 changes: 2,665 additions & 1,614 deletions PyFlow/network_api/connect_tcp.py

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions PyFlow/transfer_web/web_backend/server_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,15 @@
FTP_ERROR_COMMAND = "/ftp_error"


def _transfer_rows(progress):
"""JSON rows for one progress store (``fid -> [transferred, total, speed, path, peer]``).
def _transfer_rows(progress, *, uploading):
"""JSON rows for uploads keyed by ``(receiver, fid)`` or downloads by ``fid``.

Returns:
list[dict]: One row per running transfer, ordered by file name.
"""
rows = []
for fid, entry in list(progress.items()):
for key, entry in list(progress.items()):
fid = key[1] if uploading else key
rows.append(
{
"fid": fid,
Expand Down Expand Up @@ -683,8 +684,8 @@ def _transfers_payload(self):
"speed_enabled": bool(server.is_enable_real_time_network_speed),
"upload_network_speed": server.upload_network_speed,
"recv_network_speed": server.recv_network_speed,
"uploading": _transfer_rows(server.file_upload_progress),
"downloading": _transfer_rows(server.file_recv_progress),
"uploading": _transfer_rows(server.file_upload_progress, uploading=True),
"downloading": _transfer_rows(server.file_recv_progress, uploading=False),
}

def _client_list_for(self, addr):
Expand Down
11 changes: 6 additions & 5 deletions PyFlow/transfer_web/web_front/client_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,15 @@
return client.client_socket in client._encrypted_sockets


def _transfer_rows(progress):
"""JSON rows for one progress store (``fid -> [transferred, total, speed, path, peer]``).
def _transfer_rows(progress, *, uploading):
"""JSON rows for uploads keyed by ``(receiver, fid)`` or downloads by ``fid``.

Returns:
list[dict]: One row per running transfer, ordered by file name.
"""
rows = []
for fid, entry in list(progress.items()):
for key, entry in list(progress.items()):
fid = key[1] if uploading else key
rows.append(
{
"fid": fid,
Expand Down Expand Up @@ -1523,9 +1524,9 @@
except _ServerRequestError as e:
return jsonify({"ok": False, "error": str(e)}), e.status
# ``folder`` names the arrival folder of the started download.
return jsonify(
{"ok": True, "folder": self._ftp_arrival_folder(destination), **result}
)

Check failure on line 1529 in PyFlow/transfer_web/web_front/client_backend.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (unformatted)

PyFlow/transfer_web/web_front/client_backend.py:1527:28: unformatted: File would be reformatted

@app.get("/api/extensions_ui")
def api_get_extensions_ui():
Expand Down Expand Up @@ -1585,8 +1586,8 @@
"speed_enabled": bool(client.is_enable_real_time_network_speed),
"upload_network_speed": client.upload_network_speed,
"recv_network_speed": client.recv_network_speed,
"uploading": _transfer_rows(client.file_upload_progress),
"downloading": _transfer_rows(client.file_recv_progress),
"uploading": _transfer_rows(client.file_upload_progress, uploading=True),
"downloading": _transfer_rows(client.file_recv_progress, uploading=False),
}

# ------------------------------------------------------------------- run
Expand Down
72 changes: 72 additions & 0 deletions docs/plans/connect-port-fallback-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 轻量增量报告:固定 client_port 的 connect 阶段端口冲突降级

状态:已实施,待人工审查;未创建 git 提交、未合并、未推送。

## 1. 提交意图

配置了固定 `client_port` 的 `TCP_Client_Base.connect()` 在 `bind()` 成功、`connect()` 因本端端口冲突失败时(Windows `WSAEADDRINUSE` 10048),改为用新 socket 从 OS 分配端口重试一次,而不是直接返回 False。

## 2. 改动内容

- `PyFlow/network_api/connect_tcp.py`
- 新增模块级 `_is_local_port_conflict(exc)`:识别 `errno.EADDRINUSE` 与 Windows `winerror == 10048`。与既有 `_is_closed_socket_error` 同层、同风格。
- `TCP_Client_Base.connect()`:`bind` + `connect` 收进一个固定端口重试循环。任一步失败且命中端口冲突时,关闭当前 socket、记录日志、丢弃 `local_address`,下一轮从 OS 分配端口重连;非端口冲突或已无固定端口时原样抛出。

## 3. 为什么这样改

对应失败用例 `test_reconnected_handshake_rejects_reply_from_old_control_socket` 在 Windows CI 的 `assert client.connect()`:

```
client_port 固定 -> SO_REUSEADDR 允许 bind 已被 TIME_WAIT 占用的端口
-> connect 才报 WinError 10048
-> 原实现只降级 bind 失败(8803-8814),connect 失败落入通用 except -> return False
```

日志中没有 "client port ... is not available" 一行,证明 `bind` 已通过,缺陷在 `connect` 这一侧。POSIX 上同一冲突由 `bind` 报出,原逻辑已覆盖,所以这是 Windows 独有的缺口。

## 4. 数据流

```text
connect()
local_address = (client_host, client_port) # client_port 为 None 时跳过 bind
loop:
new socket + SO_REUSEADDR + timeout
bind(local_address)? -> OSError 且 _is_local_port_conflict -> close, local_address=None, 重试
connect((host, port)) -> OSError 且 _is_local_port_conflict -> close, local_address=None, 重试
-> 其它 OSError -> 抛出,沿用外层 timeout/refused/通用分支
break
之后: _stopped 检查 -> running=True -> 控制 socket 登记 -> receive 线程 -> 模式协商
```

固定端口可用时行为完全不变(首轮 bind 成功、connect 成功,循环只跑一次)。

## 5. 局部验证

1. **新增分支冒烟**(临时脚本,已删除):真实 server + 真实 client,`client_port` 固定;monkeypatch `socket.socket.connect` 使**首次** connect 抛 `OSError(EADDRINUSE)`。

```
connect(): True
attempts: [('127.0.0.1', 50970), ('0.0.0.0', 0)]
final local: ('127.0.0.1', 39008)
fallback used: True
bound fixed port on 1st attempt: True
```

第一次尝试绑定了固定端口 50970 并在 connect 处失败;第二次不 bind、由内核分配 39008 后连接成功。

2. **既有回归**:
- `test/integration/network_api/test_tcp_transfer_resume.py -k "configured_port_is_taken or upload_resumes_after_a_reconnect or download_resumes_after_connect"` → 3 passed。
- `test/unit/network_api/test_TCP_client.py` + 整个 `test_tcp_transfer_resume.py` → 50 passed in 107s。
- 修复前同两套件在本机 3.14 也已通过(该缺陷只在 Windows 触发),故本轮新增的唯一证据是上面的 1。

## 6. 明确未覆盖

- **Windows 真机未验证**:本机是 Linux,10048 由 monkeypatch 模拟,不是真实内核行为。合入前建议在 Windows CI 上跑 `test_reconnected_handshake_*` 确认。
- 只降级一次。若 OS 分配端口后仍冲突(极不可能),原错误照抛。
- 未改动 `_release_client_socket()`。它依赖 "socket 尚未 shutdown" 才能靠 `SO_LINGER(1,0)` 触发 RST;测试用 `shutdown(SHUT_RDWR)` 前置,那条路径仍会留下 TIME_WAIT,本修复让客户端能绕过去,但没有消除根因。
- 从 OS 分配端口后,本端地址变化,按地址匹配的断点续传不会命中旧记录(与既有 bind 失败降级语义一致,已有注释说明)。

## 7. 风险与待确认

- 风险:低。单函数、单路径、~40 行,固定端口可用时零行为变化。
- 待确认:是否接受 "只降级一次、不重试固定端口"。另一种做法是固定端口冲突时先短退避再重试固定端口,但那会把重连延迟拉长到 TIME_WAIT 量级(Windows 约 2 分钟),不合适。
Loading
Loading