Files
ykxmz/_build/fix_future_imports.py
T
xmz7891 b76014709e 修用户报的三个问题 + 揪出我自己检查工具的盲区
用户报:
  1. 官网 502
  2. 局域网连接:输入配对码提示连上了但没反应,得推出去点设备才能连
  3. 被控端安装时要能设中转服务器;之后双击 exe 也该能改配对码 /
     开关新设备 / 中转地址 —— 但现在双击没反应

────────────────────────────────────────
问题 1:502(暂态,已恢复)
────────────────────────────────────────
你截图是 21:24:56,隧道日志显示 21:22:51 和 21:26:44 各重启了一次
—— 502 就发生在那个空窗里。现在三个服务都 active、10 条连接、
本机 200(3ms)、公网 200(2.6s)。

(重启是谁触发的还没查清,watchdog 的嫌疑最大,下一轮跟进。)

────────────────────────────────────────
问题 2:局域网连上后没反应(真 bug,已修)
────────────────────────────────────────
手表上有**四个**面板,靠手工 setVisibility 切换:

    connectPanel   首页
    controlPanel   控制面板
    mirrorPanel    投屏
    lanPanel       局域网连接页    <- 后加的

而 showControlPanel() 只藏了前两个:

    connectPanel.setVisibility(GONE);
    controlPanel.setVisibility(VISIBLE);
    if (mirrorPanel != null) mirrorPanel.setVisibility(GONE);
    // lanPanel 没藏!

于是从「局域网连接」页填完码、连接成功之后:
    connect() 成功 -> showControlPanel() -> 控制面板显示了
    但 lanPanel 还盖在最上层 -> 用户看到的还是局域网页面

表现就是用户说的「提示连上了,但没反应,得推出去到设备列表
点击设备才能连接」—— 推出去把 lanPanel 收掉,才看到底下。

**根子在于"每处切换都手写一串 setVisibility"**,加第四个面板时
必然漏。改成统一入口:

    private void showOnly(View keep)   // 藏掉所有面板,只留一个

6 个切换点全部走它。以后加面板只要在 showOnly 里加一行。

────────────────────────────────────────
问题 3:双击 exe 没反应(功能缺失,已补)
────────────────────────────────────────
原来是两件事叠在一起:

**3a. 装完之后双击永远静默**(main() 最后几行)

    if not store.get("setup_done", False):
        return run_setup(store)
    return run_silent(store, None)      # <- 什么都不弹

改成:没参数 + 已设置过 -> **打开设置窗口**。
后台运行走显式 --silent(开机自启和安装包都用它,不受影响)。

**3b. 设置窗口里没有中转服务器**

SetupWindow 原来只有 配对码 / 开机自启 / 放行防火墙。
现在加了:
    * 中转服务器地址(可不填;只在一个 WiFi 里用就不需要)
    * 允许新设备接入(开关,只在"设置"模式给)

并且给 SetupWindow 加了 mode:
    "first"     第一次设置(防火墙 + 自启 + 转静默,老行为不变)
    "settings"  已装好,只改配置

**3c. 顺带修了一个会静默失败的东西**

改完发现打包出来的 exe 一跑就 ModuleNotFoundError:
No module named 'lanremote.ui' —— **PyInstaller 的 --hidden-import
里没有 ui**。它是 tkinter 那套设置窗口的实现,而静默那条路
不需要它,静态分析就找不到。源码跑得好好的,只有 exe 会崩。

已加 --hidden-import "lanremote.ui",并写了
_tests/test_double_click_window.py 针对**打包好的 exe** 验:
    ✅ 进程没崩 / 窗口弹出来了 / 标题是「ykxmz · 设置」

────────────────────────────────────────
额外揪出来的:我的检查工具一直在骗我
────────────────────────────────────────
反复被同一个问题咬了三次:补丁脚本为了加 import os / import re,
把它插到了 `from __future__` 前面,整个文件语法错。

而**我前面几次说的「全仓语法检查 0 个问题」是假绿**:

    ast.parse(src)      -> 通过(只解析,不做编译期检查)
    compile(src, ...)   -> SyntaxError: from __future__ imports
                           must occur at the beginning of the file

`__future__` 的位置是**编译期**检查,ast.parse 根本不管。
所以改成用 compile(),并写成
_tests/check_python_syntax.py(回归阶段 3a7),
它还会专门把"ast.parse 会放过、compile 会拦"的文件标出来。

实测:全仓 533 个文件,用 compile() 扫出 1 个坏的(ui.py),
而它正是让"双击没反应"的最后一个原因。

────────────────────────────────────────
还有一个重要发现
────────────────────────────────────────
**你一直在跑的是 10-06 的旧 exe**:

    C:\Users\shu\Desktop\ykxmz\ykxmz.exe    10-06 17:43  <- 你双击的这个
    LanRemote\dist_exe\ykxmz\ykxmz.exe      10-07 21:5x  <- 修复都在这

它的 silent.log 正在反复报:
    [21:15:59] 连接断了,重开一次再试(The read operation timed out)
    [21:16:42] 中转连接出问题:RelayError: 请求失败: ...
    [21:18:34] 已登记到中转:配对码 111111

这就是我修的那个 126 秒长轮询问题 —— 旧版超时太短反复掉线。
新版把客户端超时改成 180 秒了。

**所以这三个修复要装上新版才生效。**
2026-10-07 21:38:22 +08:00

174 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''修所有「__future__ 不在文件最前面」的文件,并加一道永久防线。
== 为什么反复踩 ==
我这个会话里已经**第三次**被同一件事咬:
Python 要求 `from __future__ import ...` 必须是文件里
**第一条语句**(docstring 之后)。而我的补丁脚本为了给文件
加 `import os` / `import re`,图省事写成:
m = re.search(r"^(import |from )", text, re.M)
text = text[:m.start()] + "import os\n" + text[m.start():]
—— 第一个 import 恰好就是 `from __future__`,
于是新 import 被塞到它前面,整个文件语法错:
SyntaxError: from __future__ imports must occur at the
beginning of the file
症状还特别隐蔽:
* 源码直接跑 -> 启动就崩
* **打包成 exe 之后** -> 只在走到那个模块时才崩,
用户看到的是"双击没反应"
== 这个脚本做两件事 ==
1. 全仓扫一遍,把所有"__future__ 不在最前面"的文件修好
(把 __future__ 提到所有 import 之前,docstring 之后)
2. 把这条检查加进回归套件(_tests/check_future_import.py),
以后任何脚本再犯,全量回归立刻红 —— 不会再等到打包之后。
'''
from __future__ import annotations
import ast
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PATTERNS = ["_tests/*.py", "_build/*.py", "relay/*.py", "windows/*.py",
"windows/lanremote/*.py", "installer/*.py", "android/*.py"]
def fix_file(path: Path) -> str:
"""返回 'ok' / 'fixed' / 'error:...'"""
text = path.read_text(encoding="utf-8")
try:
# ⚠ 必须用 compile():ast.parse 不做 __future__ 位置检查,
# 用它探测会误判成"没问题"(这一轮就是这么被骗了好几次)。
compile(text, str(path), "exec")
return "ok"
except SyntaxError as exc:
if "__future__" not in (exc.msg or ""):
return "ok" # 别的语法错不归这个脚本管
lines = text.splitlines()
# 找到 __future__ 那几行
fut_idx = [i for i, ln in enumerate(lines)
if re.match(r"^\s*from __future__ import ", ln)]
if not fut_idx:
return "ok"
# 把它之前的所有 import 行摘出来,插到它后面
first = fut_idx[0]
before = lines[:first]
# docstring(如果第一段是字符串)要留在最前面
keep = []
moved = []
i = 0
# 跳过 shebang / coding / 注释 / docstring
while i < len(before):
s = before[i].strip()
if (not s or s.startswith("#") or s.startswith('"""')
or s.startswith("'''") or s.startswith('r"""')
or s.startswith("r'''")):
keep.append(before[i])
# docstring 可能是多行,简单处理:找到闭合
if s.startswith(('"""', "'''", 'r"""', "r'''")):
quote = '"""' if '"""' in s else "'''"
if s.count(quote) < 2:
i += 1
while i < len(before):
keep.append(before[i])
if quote in before[i]:
break
i += 1
i += 1
continue
if re.match(r"^\s*(import |from )", before[i]):
moved.append(before[i])
i += 1
continue
# 其它(比如赋值)就停在 docstring 之后、import 之前 —— 不动它
break
rest_before = before[i:]
fut_block = lines[first:]
# 找到 __future__ 连续块结束
end = 0
while end < len(fut_block) and (
re.match(r"^\s*from __future__ import ", fut_block[end])
or not fut_block[end].strip()):
end += 1
fut_lines = fut_block[:end]
after = fut_block[end:]
new_lines = keep + fut_lines + ([""] if moved else []) + moved \
+ rest_before + after
new_text = "\n".join(new_lines) + "\n"
try:
compile(new_text, str(path), "exec")
except SyntaxError as exc:
return "error: 修完还是语法错:第 %s 行 %s" % (exc.lineno, exc.msg)
path.write_text(new_text, encoding="utf-8", newline="\n")
return "fixed"
def main() -> int:
files = []
for pat in PATTERNS:
files += sorted(ROOT.glob(pat))
fixed = []
errors = []
checked = 0
for f in files:
if not f.is_file():
continue
checked += 1
r = fix_file(f)
if r == "fixed":
fixed.append(str(f.relative_to(ROOT)))
elif r != "ok":
errors.append("%s: %s" % (f.relative_to(ROOT), r))
print(" 检查 %d 个文件" % checked)
if fixed:
print(" 修好 %d 个:" % len(fixed))
for x in fixed:
print(" %s" % x)
else:
print(" ✓ 没有需要修的")
if errors:
print(" ⚠ %d 个修不了:" % len(errors))
for x in errors:
print(" %s" % x)
return 1
# 复查
still = []
for f in files:
if not f.is_file():
continue
try:
compile(f.read_text(encoding="utf-8"), str(f), "exec")
except SyntaxError as exc:
if "__future__" in (exc.msg or ""):
still.append(str(f.relative_to(ROOT)))
if still:
print(" ✗ 还有 %s" % still)
return 1
print(" ✓ 全仓 __future__ 位置都正确")
return 0
if __name__ == "__main__":
sys.exit(main())