diff --git a/README.md b/README.md index 14bbf11..26fe05b 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ | 装在哪 | 是什么 | 文件 | |---|---|---| -| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.5.exe` | -| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.5.zip` | -| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.5.apk` | +| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.6.exe` | +| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.6.zip` | +| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.6.apk` | **只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。 @@ -81,7 +81,7 @@ ### 第一步:电脑上装被控端 -双击 `ykxmz-setup-2.1.5.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。 +双击 `ykxmz-setup-2.1.6.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。 第一次运行会弹出窗口,填两样: @@ -94,12 +94,12 @@ > 想再打开窗口,**再双击一次程序图标**就行,不会起第二个进程。 > 想彻底退出,窗口上点「退出」。 -> 不想用安装包的话,`ykxmz-agent-2.1.5.zip` 是绿色版: +> 不想用安装包的话,`ykxmz-agent-2.1.6.zip` 是绿色版: > 解压出来直接跑 `ykxmz.exe`,整个文件夹拷走就能换台机器用,不写注册表。 ### 第二步:手表上装 App -把 `ykxmz-watch-2.1.5.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 +把 `ykxmz-watch-2.1.6.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 打开 App,首页有四个入口: @@ -223,7 +223,7 @@ 中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行, Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。 -把 `ykxmz-server-2.1.5.zip` 解压到那台机器上,然后一条命令: +把 `ykxmz-server-2.1.6.zip` 解压到那台机器上,然后一条命令: ```bash bash deploy.sh @@ -456,7 +456,7 @@ windows\build_exe.ps1 ```powershell android\build_apk.ps1 # 流程:aapt2 → javac → d8 → zipalign → apksigner -# 产物:dist\ykxmz-watch-2.1.5.apk +# 产物:dist\ykxmz-watch-2.1.6.apk ``` 最低支持 Android 7.0(API 24),targetSdk 34。 diff --git a/_build/CHANGELOG.md b/_build/CHANGELOG.md index 610cf23..90d5cfa 100644 --- a/_build/CHANGELOG.md +++ b/_build/CHANGELOG.md @@ -1,3 +1,43 @@ +## 2.1.6 + +- **电脑端静默自动更新** —— 手表上点一下,电脑自己升到最新版, + 全程不弹任何窗口,**中转地址、配对码、所有设置一个都不变**。 + + 流程:手表点按钮 → 电脑端问中转"最新是哪版" → 下载并校验 → + 起一个后台帮手 → 自己退出 → 帮手等它退出后静默装好 → 重新拉起。 + + 几个关键点: + + - **配置为什么不会丢**:配置存在 `%ProgramData%\ykxmz\data\`, + 不在程序目录里。安装器升级只替换程序文件,不碰这个目录。 + - **更新源是用户自己的中转站**,不是作者的服务器,也不是任何 + 写死的地址。中转没这功能(老版本服务端)时会明确提示 + "先手动升级一次服务端"。 + - **校验和不匹配就放弃这一轮**,保持现状(现状至少能用)。 + 自更新最怕的就是装上一个坏程序、然后它还自己继续更新。 + + 手表首页新增「电脑端版本」按钮,点开就能看到 + "电脑端 2.1.4 → 2.1.5",确认后才动手。 + +- **远程鼠标控制**:移动、左键、右键、中键、滚轮(含横向)。 + + 这是"能直接操控电脑"的功能,所以参数校验写得很严: + 非法动作、非法按键、非数字坐标全都会被拒。 + +- **一键显示桌面**(等于 Win+D)。 + +- **Windows 7 兼容性校验关卡**:打包时和回归里都会检查 + (内嵌的是不是 3.8 运行库、**有没有**那个 Win7 上不存在的 DLL、 + exe 是不是 32 位),不过就拒绝出包。 + +- 技术说明: + - 鼠标坐标用 SendInput 的 **0..65535 归一化**,而且**必须除以 + (屏幕宽-1)**。少了那个 -1,最右边和最下边几像素点不到。 + - `INPUT` 结构在 32/64 位下大小不同(28 / 40),代码按进程位数算。 + - 目标窗口若是管理员权限的(UAC 提升过的),普通权限的注入会被 + 系统拦掉 —— 这是 Windows 的设计,不是 bug。被控端以管理员运行 + 就没问题。 + ## 2.1.5 - **修「Windows 7 装完报 dll 缺失」** —— 学校电脑装不上。 diff --git a/_build/add_site.py b/_build/add_site.py new file mode 100644 index 0000000..e9d394f --- /dev/null +++ b/_build/add_site.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +r'''在一台服务器上加一个新网站(nginx 虚拟主机 + 证书)。 + +== 为什么需要这个 == + +一台服务器上跑多个网站是常态。做法是:**一个 nginx 占 80/443, +每个域名一个 server 块**,nginx 按 Host 头分流到各自的本地端口。 + +手写这套配置每次都要复制粘贴改半天,还容易漏: + * 忘了签证书 -> 浏览器报不安全 + * 后端端口写错 -> 502 + * 忘了把后端绑到 127.0.0.1 -> 服务直接暴露在公网 + +所以做成一键的。 + +== 用法 == + + python _build/add_site.py 域名 后端地址 [--php] [--root 目录] + + 例: + # 反代到一个本地服务 + python _build/add_site.py blog.xxmxxs.top 127.0.0.1:9000 + + # WordPress 那种 PHP 站(走 php-fpm 的 unix socket 或端口) + python _build/add_site.py xxmxxs.top 127.0.0.1:9000 --php --root /opt/1panel/www/sites/xxmxxs.top/index + +== 设计上的几个注意 == + +1. **默认只回源 127.0.0.1** + 后端服务必须只绑本地。写 0.0.0.0 的话,别人直接打 IP:端口 + 就绕过 nginx 和 Cloudflare 了 —— 那正是用户不想要的。 + +2. **证书用 certbot 的 nginx 插件签** + 它自己改配置、自己加 443。前提是**这个域名的 A 记录已经指过来** + (而且最好是橙色云朵,这样 certbot 的 HTTP-01 挑战能经 Cloudflare 回源)。 + +3. **不动别的站点** + 只新建 /etc/nginx/sites-available/<域名>,再链到 sites-enabled。 + 已有的站点(比如遥控中转 ykxmz)一个字都不碰。 + +4. **默认 server 的坑** + 如果有人用 IP 直接访问,nginx 会用**第一个** server 块回应。 + 所以配置文件开头有个显式的 default server 把它们挡掉 —— + 这样 IP:port 扫不到任何真实站点。 +''' +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "_build")) +from lssh import run, upload # noqa: E402 + +TEMPLATE = """# {domain} —— 由 _build/add_site.py 生成 +# +# 后端: {backend} +# 说明: nginx 按 server_name 分流;后端只应该监听 127.0.0.1, +# 这样外面只能通过域名(也就是经过 nginx + Cloudflare)访问。 + +server {{ + listen 80; + listen [::]:80; + server_name {domain}; + + access_log /var/log/nginx/{safe}_access.log; + error_log /var/log/nginx/{safe}_error.log; + +{body} + server_tokens off; + client_max_body_size {max_body}; +}} +""" + +PROXY_BODY = """ location / {{ + proxy_pass http://{backend}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 300s; + proxy_connect_timeout 30s; + }} +""" + +PHP_BODY = """ root {root}; + index index.php index.html; + + location / {{ + try_files $uri $uri/ /index.php?$args; + }} + + location ~ \\.php$ {{ + include snippets/fastcgi-php.conf; + fastcgi_pass {backend}; + fastcgi_read_timeout 300s; + }} + + location ~ /\\.ht {{ + deny all; + }} +""" + + +def main() -> int: + ap = argparse.ArgumentParser(description="加一个网站") + ap.add_argument("domain", help="域名,例如 blog.xxmxxs.top") + ap.add_argument("backend", help="后端地址,例如 127.0.0.1:9000") + ap.add_argument("--php", action="store_true", + help="PHP 站(用 fastcgi_pass 而不是 proxy_pass)") + ap.add_argument("--root", default="", help="PHP 站的根目录") + ap.add_argument("--max-body", default="64m", help="上传大小上限,默认 64m") + ap.add_argument("--no-cert", action="store_true", + help="只配 80,先不签证书") + args = ap.parse_args() + + domain = args.domain.strip() + backend = args.backend.strip() + safe = domain.replace(".", "_").replace("-", "_") + + # ⚠ 后端必须是本地地址。写 0.0.0.0 或公网地址的话, + # 等于把后端服务直接暴露出去,nginx 和 Cloudflare 都白搭。 + if not (backend.startswith("127.0.0.1:") or backend.startswith("localhost:") + or backend.startswith("unix:")): + print(" ⚠ 后端 %r 不是本地地址。" % backend) + print(" 后端必须只监听 127.0.0.1 —— 否则别人直接打 " + "IP:端口就绕过了 nginx 和 Cloudflare。") + print(" 要继续的话请显式确认(现在中止)。") + return 1 + + if args.php and not args.root: + print(" ✗ --php 需要同时给 --root(网站文件目录)") + return 1 + + body = (PHP_BODY.format(root=args.root, backend=backend) + if args.php else PROXY_BODY.format(backend=backend)) + conf = TEMPLATE.format(domain=domain, backend=backend, safe=safe, + body=body, max_body=args.max_body) + + print("=" * 58) + print(" 加站点 %s -> %s" % (domain, backend)) + print("=" * 58) + + print() + print(" ── 1. 写配置 ──") + import tempfile + tmp = Path(tempfile.gettempdir()) / ("nginx-%s.conf" % safe) + tmp.write_text(conf, encoding="utf-8", newline="\n") + upload(str(tmp), "/tmp/%s.conf" % safe) + out = run( + "sudo mv /tmp/{s}.conf /etc/nginx/sites-available/{d} && " + "sudo ln -sf /etc/nginx/sites-available/{d} " + "/etc/nginx/sites-enabled/{d} && " + "sudo nginx -t 2>&1 | tail -2".format(s=safe, d=domain), + timeout=180, quiet=True) + for ln in out[1].strip().splitlines(): + print(" " + ln) + if "successful" not in out[1]: + print(" ✗ nginx 配置检查没过,撤回") + run("sudo rm -f /etc/nginx/sites-enabled/%s" % domain, + timeout=60, quiet=True) + return 1 + + print() + print(" ── 2. 重载 nginx ──") + out = run("sudo systemctl reload nginx && sleep 1 && " + "systemctl is-active nginx", timeout=120, quiet=True) + print(" nginx: " + out[1].strip()) + + if not args.no_cert: + print() + print(" ── 3. 签 Let's Encrypt 证书(顺带加上 443 和跳转)──") + out = run( + "sudo certbot --nginx -d {d} --non-interactive --agree-tos " + "--register-unsafely-without-email --redirect " + "--keep-until-expiring 2>&1 | tail -8".format(d=domain), + timeout=600, quiet=True) + for ln in out[1].strip().splitlines(): + print(" " + ln) + + print() + print(" ── 4. 复核 ──") + # ⚠ 这里**不能用 .format()** —— 远端命令里有 curl 的 `%{http_code}`, + # 而 .format() 会把 `{http_code}` 当占位符,抛 + # "KeyError/IndexError: http_code"。 + # 和之前用 % 格式化踩的是**同一类坑**(内容里有格式符)。 + # 凡是把域名等变量拼进含 `{...}` 的远端命令,一律用 replace。 + probe = ( + 'printf " %-34s %s\\n" "本机带 Host 头" ' + '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 10 ' + '-H \'Host: __D__\' http://127.0.0.1:80/)"; ' + 'printf " %-34s %s\\n" "本机 443 带 SNI" ' + '"$(curl -sk -o /dev/null -w \'%{http_code}\' --max-time 10 ' + '--resolve __D__:443:127.0.0.1 https://__D__/)"; ' + 'printf " %-34s %s\\n" "公网 443" ' + '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 20 ' + 'https://__D__/)"' + ).replace("__D__", domain) + out = run(probe, timeout=180, quiet=True) + for ln in out[1].strip().splitlines(): + print(ln) + + print() + print(" 已加好。注意:") + print(" * 这个域名的 DNS 要指向这台服务器(Cloudflare 橙色云朵)") + print(" * 后端 %s 必须只监听 127.0.0.1" % backend) + print(" * 不要动 /etc/nginx/sites-available/ykxmz(那是遥控中转)", + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/add_update_endpoint.py b/_build/add_update_endpoint.py new file mode 100644 index 0000000..0abb3c8 --- /dev/null +++ b/_build/add_update_endpoint.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +r'''给中转加「最新版本查询」接口 —— 被控端静默自动更新的第一步。 + +== 为什么由中转来回答 == + +被控端已经知道中转地址(那是它的配置),而中转的 downloads/ 目录里 +就放着各版本的成品。让中转直接回答"最新是哪版、在哪下、校验和多少", +就不用把任何地址写死在客户端里 —— 换个域名/换台服务器都不用改代码。 + +== 新增 == + + GET /v1/update/latest?platform=windows + + 200 { + "ok": true, + "platform": "windows", + "version": "2.1.5", + "name": "ykxmz-setup-2.1.5.exe", + "url": "/download/ykxmz-setup-2.1.5.exe", + "size": 9395078, + "sha256": "....", + "notes": "..." <- 可选,取 CHANGELOG 里那一节 + } + + 没找到任何包时: + 200 {"ok": false, "error": "还没有可用的安装包"} + +== url 给相对路径 == + +给相对路径(`/download/xxx.exe`),让被控端用**它自己配置的中转地址** +拼成完整 URL。这样: + * 中转换域名/换 IP 不用改任何东西 + * 走 Cloudflare 还是直连由被控端那边的配置决定 + +== sha256 要缓存 == + +安装包 9MB,每次请求都算一遍 sha256 大概 30 毫秒 —— 不算慢, +但这个接口会被所有被控端周期性调用(每台每小时一次的话, +一个学校几十台就是几十次),没必要重复算。 +按「文件名 + 大小 + 修改时间」做键缓存。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +RELAY = ROOT / "relay" / "relay.py" + + +def main() -> int: + raw = RELAY.read_bytes() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + + if "_latest_package" in text: + print(" 已经加过了") + return 0 + + # ⚠ **先把所有锚点都验一遍,再动手改。** + # + # 第一版是把替换都做在内存里、最后才 write_bytes —— + # 结果中途某个锚点没匹配上就 `return 1`, + # 前面几处改动**全丢了**(脚本还打印了"✓ 已加"), + # 而文件其实一个字没变。特别容易误判成"加好了"。 + # + # 教训:**批处理脚本要幺先验后改,幺写一段落一段。** + ANCHORS = { + "import_json": "\nimport json", + "list_downloads": ' items.sort(key=lambda item: item["mtime"], reverse=True)\n return items\n', + "downloads_route": ' if path == "/v1/downloads":', + } + missing = [k for k, v in ANCHORS.items() if v not in text] + if missing: + print(" ✗ 这些锚点找不到,一个字都不改:%s" % ", ".join(missing)) + return 1 + print(" ✓ 锚点全部就位") + + # ---- 1. import hashlib ---- + if "\nimport hashlib" not in text: + text = text.replace(ANCHORS["import_json"], + "\nimport hashlib\nimport json", 1) + print(" ✓ 加了 import hashlib") + + # ---- 2. 插到 _list_downloads 之后 ---- + anchor = ANCHORS["list_downloads"] + + helper = anchor + ''' + +# 最新安装包的信息缓存。键 = (文件名, 大小, 修改时间)。 +_PKG_CACHE: dict = {} + + +def _sha256_of(path: Path) -> str: + """算文件的 sha256(分块读,别把 9MB 全塞内存)。""" + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(256 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def _latest_package(platform: str = "windows") -> dict: + """找 downloads/ 里最新的安装包,给被控端自更新用。 + + `platform` 现在只有 windows;留着参数是为了以后加别的平台 + (手表/服务端不通过这个接口自更新,它们各有各的更新方式)。 + + 返回 {} 表示"还没有可用的包"。 + """ + # 各平台的成品名前缀 + prefix = { + "windows": "ykxmz-setup-", + }.get(platform) + if not prefix: + return {} + + best = None # (版本元组, 路径) + try: + if not DOWNLOADS_DIR.is_dir(): + return {} + for entry in DOWNLOADS_DIR.iterdir(): + if not entry.is_file(): + continue + name = entry.name + if not name.startswith(prefix) or not name.endswith(".exe"): + continue + if not _safe_download_name(name): + continue + # 从文件名里抠版本号 + mid = name[len(prefix):-len(".exe")] + parts = mid.split(".") + if len(parts) != 3 or not all(p.isdigit() for p in parts): + continue + ver = tuple(int(p) for p in parts) + # ⚠ 按**数字段**比大小,不能按字符串 —— + # 否则 "1.9.0" > "2.1.4"(字符 '9' > '1')。 + if best is None or ver > best[0]: + best = (ver, entry) + except OSError: + return {} + + if best is None: + return {} + + ver, path = best + try: + st = path.stat() + except OSError: + return {} + + key = (path.name, st.st_size, int(st.st_mtime)) + cached = _PKG_CACHE.get(key) + if cached: + digest = cached + else: + try: + digest = _sha256_of(path) + except OSError: + return {} + # 只留最近几个,别让字典涨 + _PKG_CACHE.clear() + _PKG_CACHE[key] = digest + + return { + "platform": platform, + "version": ".".join(str(p) for p in ver), + "name": path.name, + "url": "/download/" + quote(path.name), + "size": st.st_size, + "sha256": digest, + "mtime": st.st_mtime, + } +''' + text = text.replace(anchor, helper, 1) + print(" ✓ 加了 _latest_package / _sha256_of") + + # ---- 3. 路由 ---- + route_anchor = ANCHORS["downloads_route"] + + route = ''' if path == "/v1/update/latest": + # 被控端自更新用:最新版是哪版、在哪下、校验和多少。 + # 公开接口 —— 文件名和哈希本来就在下载页上人人可见, + # 不含任何敏感信息。 + platform = (urllib.parse.parse_qs( + urllib.parse.urlparse(self.path).query) + .get("platform", ["windows"])[0] or "windows") + pkg = _latest_package(platform) + if not pkg: + self._json(200, {"ok": False, + "error": "还没有可用的安装包"}) + else: + self._json(200, dict(pkg, ok=True)) + return + +''' + route_anchor + text = text.replace(route_anchor, route, 1) + print(" ✓ 加了 /v1/update/latest 路由") + + RELAY.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) + print(" ✓ 写回 relay.py") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/add_update_test_stage.py b/_build/add_update_test_stage.py new file mode 100644 index 0000000..093a86d --- /dev/null +++ b/_build/add_update_test_stage.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +r'''往 _tests/verify_all.ps1 里加两个关卡。 + +== 加哪两个 == + + 3a10 静默自动更新链路(_tests/test_update_flow.py) + 3a11 远程鼠标 + 显示桌面(_tests/test_mouse_control.py) + +== 这个脚本自己踩过的坑,写下来免得再犯 == + +往 server.py 插方法时,我两次把 **dispatch 的兜底 return** +一起卷进了新方法里: + + if cmd == P.CMD_XXX: + return self._do_xxx(msg) + + return {"ok": False, "error": f"未知命令: {cmd}"} <- dispatch 的兜底 + + def _do_xxx(...): <- 我的新方法 + ... + return {"ok": False, "error": f"未知命令: {cmd}"} <- 被卷进来了! + + 结果 pyflakes 报 "undefined name 'cmd'",而 dispatch 丢了兜底分支。 + + 这次改 PowerShell 里插关卡用**更笨但更稳**的办法: + 直接在固定锚点**之前**插,而且插完立刻读回来看一眼结构。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +VERIFY = ROOT / "_tests" / "verify_all.ps1" + +STAGES = ''' +# ---------------------------------------------------------------- 3a10 +Banner "3a10/7 静默自动更新链路" +# 用户要求:"手表端点击一个更新按钮,电脑端就可以自动进行静默更新, +# 其他什么设置都不变 …… 不弹任何窗口出来" +# 这条链是:中转 /v1/update/latest -> 被控端 updater.py 下载校验 +# -> 分离 .cmd 帮手 -> 静默装 -> 重启。 +# +# 这里验协议常量、版本比较(按数字段,不是字符串)、下载校验、 +# 以及**服务端 dispatch 的结构没被补丁弄坏**。 +$stageOut = & python (Join-Path $root "_tests\\test_update_flow.py") 2>&1 + Show-Fails $stageOut +Record "静默自动更新" ($LASTEXITCODE -eq 0) + +# ---------------------------------------------------------------- 3a11 +Banner "3a11/7 远程鼠标 + 一键显示桌面" +# 鼠标注入是最"有力量"的功能,参数校验写错的话表现是"手表点了没反应", +# 教室里没法排查。这里盯坐标归一化(除以 宽-1 那条规则)、 +# 滚轮夹取、Win+D 的事件顺序、以及非法参数必须被拒。 +$stageOut = & python (Join-Path $root "_tests\\test_mouse_control.py") 2>&1 + Show-Fails $stageOut +Record "远程鼠标控制" ($LASTEXITCODE -eq 0) + +''' + +ANCHOR = '# ---------------------------------------------------------------- 3a5' + + +def main() -> int: + raw = VERIFY.read_bytes() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + + if "静默自动更新链路" in text: + print(" 已经加过了") + return 0 + + if ANCHOR not in text: + print(" ✗ 找不到锚点 %r" % ANCHOR) + return 1 + print(" ✓ 锚点就位") + + text = text.replace(ANCHOR, STAGES.lstrip("\n") + "\n" + ANCHOR, 1) + VERIFY.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) + print(" ✓ 加了 3a10 / 3a11 两个关卡") + + # 读回来确认结构:两个 Record 都在、锚点还在 + back = VERIFY.read_text(encoding="utf-8-sig") + for token in ('Record "静默自动更新"', 'Record "远程鼠标控制"', ANCHOR): + ok = token in back + print(" [%s] 读回确认:%s" % ("PASS" if ok else "FAIL", + token[:40])) + if not ok: + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/check_1panel_conflict.py b/_build/check_1panel_conflict.py new file mode 100644 index 0000000..23b7807 --- /dev/null +++ b/_build/check_1panel_conflict.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +r'''查 1Panel 和我装的 nginx 会不会打架。 + +== 为什么要查 == + +发现服务器上有 1Panel。1Panel 默认会**自己装一套 OpenResty(nginx)** +放在 /opt/1panel/apps/openresty/ 下,并且**接管 80/443**。 + +而我为了给遥控中转做反代,用 apt 装了**系统 nginx**,也在听 80/443。 + +两个 nginx 抢同一组端口,只有一个能起来 —— 另一个会 +`Address already in use`。所以现在实际生效的到底是谁的配置, +必须搞清楚,否则: + + * 我给 yk.xxmxxs.top 配的证书和反代可能**根本没生效** + (之前测通了,说明生效的是我的那份) + * 但用户在 1Panel 里建站,可能**建了个寂寞**(1Panel 的 OpenResty 起不来) + +== 查什么 == + + 1. 系统里到底有几个 nginx/OpenResty 可执行文件 + 2. 现在听 80/443 的是哪个进程(看 exe 路径) + 3. 1Panel 的应用目录里有没有 openresty、有没有建过站 + 4. 1Panel 面板自己在哪个端口 + 5. 有没有已经存在的 WordPress 容器 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "_build")) +from lssh import run # noqa: E402 + +REMOTE = r''' +echo "==== 1. 有几个 nginx / openresty ====" +for p in /usr/sbin/nginx /usr/local/openresty/nginx/sbin/nginx \ + /opt/1panel/apps/openresty/openresty/bin/openresty; do + [ -x "$p" ] && echo " 有: $p" +done +command -v nginx openresty 2>/dev/null | sed 's/^/ PATH 里: /' + +echo +echo "==== 2. 现在听 80/443 的是谁(看可执行文件路径)====" +sudo ss -tlnp 2>/dev/null | grep -E ':(80|443) ' | sed 's/^/ /' + +echo +echo "==== 3. 1Panel 的 nginx 有没有在跑 ====" +for s in openresty nginx 1panel; do + st=$(systemctl is-active $s 2>/dev/null) + [ -n "$st" ] && echo " $s: $st" +done +echo " --- 1Panel 应用目录 ---" +sudo ls -1 /opt/1panel/apps/ 2>/dev/null | sed 's/^/ /' || echo " (没有 /opt/1panel/apps)" +echo " --- 1Panel 的 www 站点目录 ---" +sudo ls -1 /opt/1panel/www/sites/ 2>/dev/null | sed 's/^/ /' || echo " (还没建过站)" + +echo +echo "==== 4. 1Panel 面板自己在哪 ====" +sudo ss -tlnp 2>/dev/null | grep -i 1panel | sed 's/^/ /' +sudo grep -h "Port\|port" /opt/1panel/conf/app.yaml 2>/dev/null | head -3 | sed 's/^/ /' + +echo +echo "==== 5. 现有的容器 ====" +sudo docker ps --format ' {{.Names}} {{.Image}} {{.Status}} {{.Ports}}' 2>/dev/null || echo " (docker 命令不可用)" + +echo +echo "==== 6. 系统 nginx 的配置来自哪 ====" +systemctl show nginx -p FragmentPath 2>/dev/null | sed 's/^/ /' +nginx -v 2>&1 | sed 's/^/ /' + +echo +echo "==== 7. 1Panel 有没有装 php/mysql 应用 ====" +sudo ls -1 /opt/1panel/apps/ 2>/dev/null | while read a; do + echo " 应用 $a:" + sudo ls -1 /opt/1panel/apps/$a 2>/dev/null | sed 's/^/ /' +done +''' + + +def main() -> int: + print("=" * 58) + print(" 1Panel 和系统 nginx 会不会打架") + print("=" * 58) + print() + out = run(REMOTE, timeout=240, quiet=True) + print(out[1] if isinstance(out, tuple) else out) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/check_server_detail.py b/_build/check_server_detail.py new file mode 100644 index 0000000..dc6154f --- /dev/null +++ b/_build/check_server_detail.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +r'''把上次查询里两个可疑点查清楚。 + +== 疑点一:证书目录为什么是空的 == + +我明明给 yk.xxmxxs.top 签过 Let's Encrypt 证书。 +但 `/etc/letsencrypt/live/` 列出来是空的 —— +多半是**权限**:那个目录是 0700 root,普通用户 ls 不到。 + +== 疑点二:3306 在监听,但没找到 mariadb/mysql == + +`ss` 显示 127.0.0.1:3306 有人在听,但 `command -v mariadb` 没找到。 +可能是: + * 装了但可执行文件叫别的名字 + * 是别的东西占了 3306(比如某个容器) +得看是哪个进程在听。 + +== 疑点三:内存用量涨了 == + +之前看是 414MB,现在 1037MB。得知道是谁吃的 —— +WordPress + MySQL 还要再吃 300~500MB,这台只有 2GB,得先弄清余量。 + +== 顺带 == + +`grep -r /etc/nginx/sites-enabled/` 没输出 server_name, +是因为那里全是**指向 sites-available 的符号链接**, +而 `grep -r` **默认不跟符号链接**。不是配置丢了。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "_build")) +from lssh import run # noqa: E402 + +REMOTE = r''' +echo "==== 1. 证书(带 sudo)====" +sudo ls -1 /etc/letsencrypt/live/ 2>&1 | sed 's/^/ /' +echo " --- yk.xxmxxs.top 的证书 ---" +sudo openssl x509 -in /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem \ + -noout -subject -dates 2>&1 | sed 's/^/ /' +echo +echo "==== 2. nginx 站点(跟符号链接)====" +sudo grep -h server_name /etc/nginx/sites-available/ykxmz 2>/dev/null | sed 's/^/ /' +echo " sites-enabled:" +sudo ls -la /etc/nginx/sites-enabled/ 2>&1 | tail -n +2 | sed 's/^/ /' +echo +echo "==== 3. 谁在听 3306 ====" +sudo ss -tlnp 2>/dev/null | grep 3306 | sed 's/^/ /' +echo +echo "==== 4. 数据库到底装没装 ====" +dpkg -l 2>/dev/null | grep -iE 'mariadb|mysql' | head -8 | sed 's/^/ /' +echo " (上面没输出就是没装 —— 那 3306 是别的东西占的)" +echo +echo "==== 5. 内存被谁吃了 ====" +ps -eo rss,comm --sort=-rss 2>/dev/null | head -10 | sed 's/^/ /' +echo +echo " --- systemd 服务占用 ---" +systemd-cgtop -n 1 -b 2>/dev/null | head -8 | sed 's/^/ /' +echo +echo "==== 6. 有没有 swap ====" +free -m | sed 's/^/ /' +swapon --show 2>/dev/null | sed 's/^/ /' || echo " 没有 swap" +''' + + +def main() -> int: + print("=" * 58) + print(" 查清三个疑点") + print("=" * 58) + print() + out = run(REMOTE, timeout=240, quiet=True) + print(out[1] if isinstance(out, tuple) else out) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/check_wp_ready.py b/_build/check_wp_ready.py new file mode 100644 index 0000000..b4125c8 --- /dev/null +++ b/_build/check_wp_ready.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +r'''看服务器上装 WordPress 缺什么。 + +== 背景 == + +用户想把 xxmxxs.top 拿来做 WordPress(原来是 Cloudflare Workers 占着)。 +WordPress 要 PHP + 数据库,而服务器上现在只跑着 nginx(给遥控中转用)。 + +== 为什么写成文件 == + +这个查询里有一堆 `$(...)`、`$b`、`2>/dev/null`。 +用 `python -c "..."` 传的话,**PowerShell 会先在本机把它们求值** +(报"无法将 systemctl 项识别为 cmdlet"、"未能找到路径 C:\dev\null")。 +这个坑今天踩了太多次 —— 凡是超过两行的远端命令,一律写成文件。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "_build")) +from lssh import run # noqa: E402 + +REMOTE = r''' +echo "==== 磁盘 ====" +df -h / | tail -1 +echo +echo "==== 内存 ====" +free -m | head -2 +echo +echo "==== nginx 站点 ====" +ls -1 /etc/nginx/sites-enabled/ 2>/dev/null +echo " server_name:" +grep -rh server_name /etc/nginx/sites-enabled/ 2>/dev/null | sed 's/^/ /' +echo +echo "==== PHP ====" +if command -v php >/dev/null 2>&1; then + php -v | head -1 + echo " php-fpm: $(systemctl is-active php8.3-fpm 2>/dev/null || systemctl is-active php8.2-fpm 2>/dev/null || echo 没装)" +else + echo " 没装 PHP" +fi +echo +echo "==== 数据库 ====" +if command -v mariadb >/dev/null 2>&1; then + echo " mariadb 客户端在: $(command -v mariadb)" + echo " mariadb 服务: $(systemctl is-active mariadb 2>/dev/null)" +elif command -v mysql >/dev/null 2>&1; then + echo " mysql 客户端在" + echo " mysql 服务: $(systemctl is-active mysql 2>/dev/null)" +else + echo " 没装数据库" +fi +echo +echo "==== apt 源里有 php-fpm 吗 ====" +apt-cache policy php-fpm 2>/dev/null | head -3 | sed 's/^/ /' +echo +echo "==== 服务状态 ====" +for s in nginx ykxmz-relay ykxmz-monitor.timer certbot.timer; do + printf ' %-24s %s\n' "$s" "$(systemctl is-active $s 2>&1)" +done +echo +echo "==== 证书目录 ====" +ls -1 /etc/letsencrypt/live/ 2>/dev/null | sed 's/^/ /' || echo " (空)" +echo +echo "==== 监听端口 ====" +ss -tln 2>/dev/null | grep -E ':(80|443|3306) ' | sed 's/^/ /' +''' + + +def main() -> int: + print("=" * 58) + print(" 服务器 WordPress 就绪度") + print("=" * 58) + print() + out = run(REMOTE, timeout=240, quiet=True) + print(out[1] if isinstance(out, tuple) else out) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/commit_msg_216.txt b/_build/commit_msg_216.txt new file mode 100644 index 0000000..b3fd0f7 --- /dev/null +++ b/_build/commit_msg_216.txt @@ -0,0 +1,106 @@ +2.1.6:静默自动更新 + 远程鼠标控制 + 一键显示桌面 + +用户这次提了一整套升级。这个提交先落地三块(其余几块在做)。 + +== 一、静默自动更新 == + +用户原话:"手表端点击一个更新按钮,电脑端就可以自动进行静默更新, +其他什么设置都不变 …… 不弹任何窗口出来"。 + +链路: + + 手表点按钮 -> 被控端问中转"最新是哪版" + -> 下载到 %TEMP%\ykxmz-update\ + -> 校验 sha256 + -> 起一个**分离的 .cmd 帮手** + -> 自己退出 + -> 帮手等它退出 -> 静默跑安装器 -> 重新拉起 + +关键设计说明: + + * **为什么要有那个 .cmd 帮手**:Windows 上正在运行的 exe 是锁着的, + 安装器覆盖不了,所以必须"先退出、再装、再起来",而这三步 + 不能由被控端自己做(它一退出就没人执行后面的了)。 + 用 DETACHED_PROCESS 起的助手不受父进程退出影响。 + + * **配置为什么不会丢**:配置在 %ProgramData%\ykxmz\data\, + **不在程序目录里**。安装器升级只替换 {app} 下的文件, + 不碰 ProgramData —— 中转地址、配对码、各种开关原样保留。 + 这是这个功能能做的前提。 + + * **更新源是用户自己的中转站**,不是作者的服务器,也不是任何 + 写死的公网地址。被控端问的是它自己配置的那个地址。 + 中转没有这个接口(老版本服务端)时会明确提示"先手动升级一次服务端"。 + + * **sha256 必须校验**:自更新最大的风险是下到一半断了或被换了, + 装上去是个坏程序、而且它还会自己再更新 —— 螺旋下去很难救。 + 对不上就**放弃这一轮**,保持现状(现状至少能用)。 + +中转新增:GET /v1/update/latest?platform=windows +返回 version / name / url / size / sha256。版本比较按**数字段** +(不能按字符串,否则 "1.9.0" > "2.1.4")。sha256 按 +(文件名,大小,mtime) 缓存,避免每次重算 9MB。 + +手表端:"电脑端版本:点这里查看" 按钮,查完显示 +"电脑端 2.1.4 → 2.1.5(点这里更新)",确认后才真的更新。 + +== 二、远程鼠标控制 == + +新增 windows/lanremote/input.py,用 SendInput 模拟鼠标键盘。 + + * **为什么用 SendInput 而不是 mouse_event**:后者是 Win95 时代的 + API,会被 UIPI 拦,而且没有 dwExtraInfo(多个注入源会互相干扰)。 + SendInput 还能一次打包多个事件,按下+抬起之间不会插进别的输入。 + + * **坐标是 0..65535 归一化**,而且**必须除以 (屏幕宽-1)**。 + 少了那个 -1,最右边和最下边几像素点不到 —— 表现为 + "鼠标到不了屏幕边缘",任务栏在底下的机器上尤其明显。 + + * **INPUT 结构在 32/64 位下大小不同**(28 / 40), + ctypes 里按进程位数算,不能写死。 + + * 权限说明:目标窗口如果是管理员权限的(UAC 提升过的), + 普通权限的注入会被 UIPI 拦掉。这不是 bug,是 Windows 的设计。 + 被控端以管理员运行就都能点。 + +== 三、一键显示桌面 == + +用户要求快捷栏里那个"不能改"的按钮就是这个功能,等于 Win+D。 +和鼠标控制共用 input.py。 + +== 顺手修的一个自己造的坑 == + +往 server.py 插方法时,**两次**把 dispatch 的兜底 +`return {"ok": False, "error": f"未知命令: {cmd}"}` 一起卷进了新方法里: + + * pyflakes 报 "undefined name 'cmd'" + * 而且 dispatch 丢了兜底分支(未知命令会返回 None) + +第一次靠 pyflakes 抓到,第二次也是。为了以后不再犯, +`_tests/test_update_flow.py` 里加了**结构断言**: +兜底必须在带 CMD_UPDATE 的那个方法体里,而各新方法里不能出现它。 + +(那条断言第一版写得太死 —— 要求"紧跟 CMD_UPDATE 分支", + 后来往 dispatch 里插了新分支就误报了。改成验结构真相。) + +== 验证 == + + 全量回归(含新增的 3a10 / 3a11 两关)全部 PASS + 静默自动更新:协议 + updater 15 项 + 服务端结构 8 项 + 远程鼠标:坐标归一化 / 滚轮夹取 / Win+D 事件顺序 / 非法参数 22 项 + 610 个 Python 文件语法通过 + 实际注入鼠标成功(move / click / scroll / show_desktop) + +== 还没做的(下一批) == + + 手表顶部快捷栏(可自定义最多 4 个,1 个固定"显示桌面") + 屏幕镜像去掉提示条、去掉所有弹窗 + 托盘图标改成设置项 + 「允许新设备连接」开关(中转后台 + 安卓端) + 弹文字窗口位置/大小可自定义 + 中转后台设备列表显示版本 + 远程点更新 + +== 顺手清理 == + +_tests/out_app/ 下面那堆 .class 是 Java 编译检查的产物, +以前误提交进仓库了(152 个文件)。这次从索引里移除并加进 .gitignore。 diff --git a/_build/package_remote.py b/_build/package_remote.py index 940d59c..1c462cc 100644 --- a/_build/package_remote.py +++ b/_build/package_remote.py @@ -17,7 +17,7 @@ import zipfile from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") -VERSION = "2.1.5" +VERSION = "2.1.6" NAME = f"ykxmz-{VERSION}" AGENT_SRC = ROOT / "dist_exe" / "ykxmz" diff --git a/_build/wire_mouse_cmd.py b/_build/wire_mouse_cmd.py new file mode 100644 index 0000000..d678248 --- /dev/null +++ b/_build/wire_mouse_cmd.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +r'''把「远程鼠标控制」和「一键显示桌面」接进协议 + 服务端。 + +== 为什么这两个一起做 == + +它们共用同一个底座(lanremote/input.py 的 SendInput 封装)。 +分开做的话底座要写两遍。 + + {"cmd":"mouse","action":"move","x":800,"y":450} + {"cmd":"mouse","action":"click","button":"left"} + {"cmd":"mouse","action":"click","button":"right","double":true} + {"cmd":"mouse","action":"down","button":"left"} + {"cmd":"mouse","action":"up","button":"left"} + {"cmd":"mouse","action":"scroll","amount":3} (正数向上) + {"cmd":"mouse","action":"scroll","amount":-3,"horizontal":true} + + {"cmd":"show_desktop"} -> Win+D,用户要求的快捷栏固定按钮 + +== 为什么 move 用绝对坐标而不是"相对位移" == + +手表上那块的触摸位置和电脑屏幕不是 1:1 的(表盘才多大)。 +相对位移要手表自己算倍率,算错了鼠标就飘。 +绝对坐标由手表把触摸点按比例映射到 [0,宽]x[0,高], +电脑端只管落到那个像素 —— 责任清晰,也不会累积漂移。 + +== 安全 == + +注入鼠标键盘的能力很强,所以**必须登录配对之后才能用** —— +这一点由现有的握手逻辑保证(没通过配对,一条命令都不执行, +见 _do_pair 和命令分发最前面那道校验)。这里不做额外放松。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PROTO = ROOT / "windows" / "lanremote" / "protocol.py" +SERVER = ROOT / "windows" / "lanremote" / "server.py" + +PROTO_ADD = '''CMD_UPDATE = "update" +# 远程鼠标控制:{"action":"move|click|down|up|scroll", ...} +CMD_MOUSE = "mouse" +# 一键显示桌面(Win+D)。用户要求的快捷栏固定按钮。 +CMD_SHOW_DESKTOP = "show_desktop"''' + + +def main() -> int: + # ---- protocol.py ---- + raw = PROTO.read_bytes() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + if "CMD_MOUSE" in text: + print(" protocol.py 已经加过了") + else: + old = 'CMD_UPDATE = "update"' + if old not in text: + print(" ✗ protocol.py 找不到 CMD_UPDATE") + return 1 + text = text.replace(old, PROTO_ADD, 1) + PROTO.write_bytes((b"\xef\xbb\xbf" if bom else b"") + + text.encode("utf-8")) + print(" ✓ protocol.py 加了 CMD_MOUSE / CMD_SHOW_DESKTOP") + + # ---- server.py ---- + raw = SERVER.read_bytes() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + if "_do_mouse" in text: + print(" server.py 已经加过了") + return 0 + + anchors = { + "imports": "from . import updater as U", + "update_branch": " if cmd == P.CMD_UPDATE:\n return self._do_update(msg)", + "unknown": ' return {"ok": False, "error": f"未知命令: {cmd}"}', + } + missing = [k for k, v in anchors.items() if v not in text] + if missing: + print(" ✗ server.py 这些锚点找不到:%s" % ", ".join(missing)) + return 1 + print(" ✓ server.py 锚点全部就位") + + text = text.replace(anchors["imports"], + anchors["imports"] + "\nfrom . import input as IN", 1) + + text = text.replace( + anchors["update_branch"], + anchors["update_branch"] + ''' + + if cmd == P.CMD_MOUSE: + return self._do_mouse(msg) + + if cmd == P.CMD_SHOW_DESKTOP: + try: + IN.show_desktop() + except IN.InputError as exc: + return {"ok": False, "error": str(exc)} + return {"ok": True, "done": "show_desktop"}''', 1) + + handler = ''' + + # ---------------- 远程鼠标 / 键盘注入 ---------------- + + def _do_mouse(self, msg: dict) -> dict: + """处理 {"cmd":"mouse","action":...}。 + + 坐标系是**屏幕像素**(相对虚拟屏幕左上角),由手表按比例映射好 + 再发过来 —— 电脑端不做缩放猜测,免得两边算法不一致导致漂移。 + """ + action = str(msg.get("action") or "").lower() + try: + if action == "move": + # 分辨率可能被改过(投屏切换等),每次移动前刷一下缓存。 + # 不刷的话,插上投影仪之后鼠标会跑到屏幕外。 + IN.refresh_screen() + IN.move_to(int(msg.get("x", 0)), int(msg.get("y", 0))) + return {"ok": True} + + if action == "moveby": + IN.move_by(int(msg.get("dx", 0)), int(msg.get("dy", 0))) + return {"ok": True} + + if action == "click": + IN.click(str(msg.get("button") or "left"), + double=bool(msg.get("double"))) + return {"ok": True} + + if action == "down": + IN.down(str(msg.get("button") or "left")) + return {"ok": True} + + if action == "up": + IN.up(str(msg.get("button") or "left")) + return {"ok": True} + + if action == "scroll": + IN.scroll(int(msg.get("amount", 0)), + horizontal=bool(msg.get("horizontal"))) + return {"ok": True} + + if action == "desktop": + IN.show_desktop() + return {"ok": True} + + return {"ok": False, "error": "不认识的鼠标动作:%s" % action} + except IN.InputError as exc: + return {"ok": False, "error": str(exc)} + except (TypeError, ValueError) as exc: + return {"ok": False, "error": "参数不对:%s" % exc} + +''' + text = text.replace(anchors["unknown"], + handler + "\n" + anchors["unknown"], 1) + + SERVER.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) + print(" ✓ server.py 加了 _do_mouse") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/wire_update_cmd.py b/_build/wire_update_cmd.py new file mode 100644 index 0000000..df427bf --- /dev/null +++ b/_build/wire_update_cmd.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +r'''把「静默自动更新」接进协议和服务端。 + +== 加两条命令 == + + {"cmd":"update","action":"check"} -> 查最新版,不改动任何东西 + {"cmd":"update","action":"start"} -> 真的要更新(下载+静默安装+重启) + +== 为什么"查"和"做"分成两步 == + +手表点一下按钮,先看到"有新版本 2.1.5(当前 2.1.4,约 9 MB)", +再决定要不要升。直接一步到位的话,用户不知道自己触发了什么。 + +== start 为什么要开线程 == + +安装包 9 MB,在学校的网上下载可能要几十秒。 +如果同步下载,手表那边的请求会一直挂着;而挂太久用户以为卡死了。 + +所以:立刻回一句"开始更新了",然后在后台线程里下载 + 校验 + 应用。 +进度通过 server 自己的日志广播给正在看的手表。 + +== 应用之后进程必须退出 == + +Windows 上正在跑的 exe 是锁着的,安装器覆盖不了。 +所以 updater 会先起一个**分离的 .cmd 帮手**(不受本进程退出影响), +帮手等本进程退出 -> 静默装 -> 重新拉起。 +这里的顺序不能反。 +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PROTO = ROOT / "windows" / "lanremote" / "protocol.py" +SERVER = ROOT / "windows" / "lanremote" / "server.py" + + +def patch_protocol() -> bool: + raw = PROTO.read_bytes() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + if "CMD_UPDATE" in text: + print(" protocol.py 已经加过了") + return True + anchor = 'CMD_INFO = "info"' + if anchor not in text: + print(" ✗ protocol.py 找不到 CMD_INFO") + return False + text = text.replace( + anchor, + anchor + "\n" + '# 静默自动更新:从**用户自己的中转站**取新版本包,' + '静默装好并重启。\n' + '# action=check 只看不动;action=start 真的更新。\n' + 'CMD_UPDATE = "update"', 1) + PROTO.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) + print(" ✓ protocol.py 加了 CMD_UPDATE") + return True + + +def patch_server() -> bool: + raw = SERVER.read_bytes() + bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + if "_do_update" in text: + print(" server.py 已经加过了") + return True + + anchors = { + "info": 'if cmd == P.CMD_INFO:\n return {"ok": True, "info": self.actions.info()}', + "unknown": ' return {"ok": False, "error": f"未知命令: {cmd}"}', + "imports": "from . import protocol as P", + } + missing = [k for k, v in anchors.items() if v not in text] + if missing: + print(" ✗ server.py 这些锚点找不到:%s" % ", ".join(missing)) + return False + print(" ✓ server.py 锚点全部就位") + + # ---- import updater ---- + text = text.replace( + anchors["imports"], + anchors["imports"] + "\nfrom . import updater as U", 1) + + # ---- 命令分发 ---- + text = text.replace( + anchors["info"], + anchors["info"] + ''' + + if cmd == P.CMD_UPDATE: + return self._do_update(msg)''', 1) + + # ---- 实现 ---- + handler = ''' + + # ---------------- 静默自动更新 ---------------- + + def _do_update(self, msg: dict) -> dict: + """处理 {"cmd":"update","action":...}。 + + 取包的地方是**用户自己配的中转站**(store 里的 relay_url), + 不是任何写死的公网地址 —— 这一点是刻意的,见 updater.py 的说明。 + """ + from . import __version__ as my_version + + action = str(msg.get("action") or "check").lower() + relay = (self.store.relay_url() or "").strip() + + if action == "check": + try: + info = U.check(relay, my_version) + except U.UpdateError as exc: + return {"ok": False, "error": str(exc)} + if not info.get("supported"): + return {"ok": False, + "error": info.get("error") or "不支持自动更新"} + if "error" in info: + return {"ok": False, "error": info["error"]} + return { + "ok": True, + "current": my_version, + "latest": info["latest"], + "needUpdate": bool(info["needUpdate"]), + "size": info.get("size", 0), + "name": info.get("name", ""), + } + + if action != "start": + return {"ok": False, "error": "action 只能是 check 或 start"} + + # ---- 真的更新 ---- + # 先查一遍(在调用方线程里查,失败能立刻报回去) + try: + info = U.check(relay, my_version) + except U.UpdateError as exc: + return {"ok": False, "error": str(exc)} + if not info.get("supported") or "error" in info: + return {"ok": False, + "error": info.get("error") or "不支持自动更新"} + if not info["needUpdate"]: + return {"ok": True, "alreadyLatest": True, "current": my_version, + "message": "电脑端已经是最新版本 %s" % my_version} + + # 下载和应用放到后台线程,先回一句让手表知道开始了 + threading.Thread( + target=self._run_update, + args=(info, my_version), + name="ykxmz-update", daemon=True).start() + + return { + "ok": True, + "started": True, + "from": my_version, + "to": info["latest"], + "size": info.get("size", 0), + "message": "正在下载 %s(%.1f MB),下完会自动装好并重启," + "设置和配对码都不变" + % (info["latest"], (info.get("size") or 0) / 1048576.0), + } + + def _run_update(self, info: dict, my_version: str) -> None: + """后台线程:下载 -> 校验 -> 起帮手 -> 自己退出。""" + def log(text: str) -> None: + try: + self._log(text) + except Exception: # noqa: BLE001 + pass + + try: + log("[更新] 开始下载 %s(%.1f MB)" + % (info["latest"], (info.get("size") or 0) / 1048576.0)) + work = Path(tempfile.gettempdir()) / "ykxmz-update" + last = [0] + + def progress(got: int, total: int) -> None: + # 每 25% 报一次,别把日志刷爆 + if total: + pct = int(got * 100 / total) + if pct - last[0] >= 25: + last[0] = pct + log("[更新] 已下载 %d%%(%.1f/%.1f MB)" + % (pct, got / 1048576.0, total / 1048576.0)) + + pkg = U.download(info["url"], work, + info.get("sha256", ""), info.get("size", 0), + progress=progress) + log("[更新] 下载完成并校验通过:%s" % pkg.name) + + # 应用:起分离的 .cmd 帮手,然后**本进程必须退出** + app_exe = Path(sys.executable if getattr(sys, "frozen", False) + else sys.argv[0]).resolve() + logfile = U.apply_update(pkg, app_exe) + log("[更新] 已交给更新帮手(%s),被控端即将退出并重启" % logfile) + U.cleanup_old_downloads() + + # 给日志和网络一点时间落完,再退出 + time.sleep(2) + os._exit(0) + except U.UpdateError as exc: + log("[更新] 失败:%s(保持当前版本 %s)" % (exc, my_version)) + except Exception as exc: # noqa: BLE001 + log("[更新] 意外失败:%s: %s" % (type(exc).__name__, exc)) + +''' + text = text.replace(anchors["unknown"], + handler + "\n" + anchors["unknown"], 1) + + # ---- 补 import ---- + need = [] + head = text.split("class ", 1)[0] + for mod, line in (("tempfile", "import tempfile"), + ("sys", "import sys"), + ("threading", "import threading"), + ("time", "import time"), + ("os", "import os"), + ("Path", "from pathlib import Path")): + if line not in head: + need.append(line) + if need: + anchor = "from . import protocol as P" + text = text.replace(anchor, "\n".join(need) + "\n" + anchor, 1) + print(" ✓ 补了 import: %s" % ", ".join(need)) + + SERVER.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) + print(" ✓ server.py 加了 _do_update / _run_update") + return True + + +def main() -> int: + print("=" * 58) + print(" 接「静默自动更新」进协议和服务端") + print("=" * 58) + print() + ok = patch_protocol() + print() + ok = patch_server() and ok + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/out_app/com/lanremote/watch/DeviceStore.class b/_tests/out_app/com/lanremote/watch/DeviceStore.class deleted file mode 100644 index c3e1452..0000000 Binary files a/_tests/out_app/com/lanremote/watch/DeviceStore.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$1.class deleted file mode 100644 index 67eafc3..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$1.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$10.class b/_tests/out_app/com/lanremote/watch/MainActivity$10.class deleted file mode 100644 index 7a6bdce..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$10.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$100.class b/_tests/out_app/com/lanremote/watch/MainActivity$100.class deleted file mode 100644 index f19477a..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$100.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$101.class b/_tests/out_app/com/lanremote/watch/MainActivity$101.class deleted file mode 100644 index 4f131b6..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$101.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$102.class b/_tests/out_app/com/lanremote/watch/MainActivity$102.class deleted file mode 100644 index 5e60210..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$102.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$103.class b/_tests/out_app/com/lanremote/watch/MainActivity$103.class deleted file mode 100644 index a702695..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$103.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$104.class b/_tests/out_app/com/lanremote/watch/MainActivity$104.class deleted file mode 100644 index 44ea6b8..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$104.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$105.class b/_tests/out_app/com/lanremote/watch/MainActivity$105.class deleted file mode 100644 index 740c545..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$105.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$106.class b/_tests/out_app/com/lanremote/watch/MainActivity$106.class deleted file mode 100644 index f3cb5df..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$106.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$107.class b/_tests/out_app/com/lanremote/watch/MainActivity$107.class deleted file mode 100644 index b810662..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$107.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$108.class b/_tests/out_app/com/lanremote/watch/MainActivity$108.class deleted file mode 100644 index 4ac9c6c..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$108.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$109.class b/_tests/out_app/com/lanremote/watch/MainActivity$109.class deleted file mode 100644 index b89f598..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$109.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$11.class b/_tests/out_app/com/lanremote/watch/MainActivity$11.class deleted file mode 100644 index c5eacb7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$11.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$110.class b/_tests/out_app/com/lanremote/watch/MainActivity$110.class deleted file mode 100644 index f87b291..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$110.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$111.class b/_tests/out_app/com/lanremote/watch/MainActivity$111.class deleted file mode 100644 index eb848f3..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$111.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class deleted file mode 100644 index 2a6fedc..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$112$1.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$112.class b/_tests/out_app/com/lanremote/watch/MainActivity$112.class deleted file mode 100644 index 3f7c91f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$112.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$113.class b/_tests/out_app/com/lanremote/watch/MainActivity$113.class deleted file mode 100644 index cace6bb..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$113.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$114.class b/_tests/out_app/com/lanremote/watch/MainActivity$114.class deleted file mode 100644 index a5085b4..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$114.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$115.class b/_tests/out_app/com/lanremote/watch/MainActivity$115.class deleted file mode 100644 index 73b530f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$115.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$116.class b/_tests/out_app/com/lanremote/watch/MainActivity$116.class deleted file mode 100644 index 26dddfa..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$116.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$117.class b/_tests/out_app/com/lanremote/watch/MainActivity$117.class deleted file mode 100644 index 69e4281..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$117.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$118.class b/_tests/out_app/com/lanremote/watch/MainActivity$118.class deleted file mode 100644 index 43b0d59..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$118.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$119.class b/_tests/out_app/com/lanremote/watch/MainActivity$119.class deleted file mode 100644 index a031b3a..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$119.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$12.class b/_tests/out_app/com/lanremote/watch/MainActivity$12.class deleted file mode 100644 index 40c67f6..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$12.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$120.class b/_tests/out_app/com/lanremote/watch/MainActivity$120.class deleted file mode 100644 index b361dea..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$120.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$121.class b/_tests/out_app/com/lanremote/watch/MainActivity$121.class deleted file mode 100644 index eef7159..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$121.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$122.class b/_tests/out_app/com/lanremote/watch/MainActivity$122.class deleted file mode 100644 index a9565c2..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$122.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$123.class b/_tests/out_app/com/lanremote/watch/MainActivity$123.class deleted file mode 100644 index f66a773..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$123.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$124.class b/_tests/out_app/com/lanremote/watch/MainActivity$124.class deleted file mode 100644 index 7f332e7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$124.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$125.class b/_tests/out_app/com/lanremote/watch/MainActivity$125.class deleted file mode 100644 index 0918097..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$125.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$126.class b/_tests/out_app/com/lanremote/watch/MainActivity$126.class deleted file mode 100644 index 890d620..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$126.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$127.class b/_tests/out_app/com/lanremote/watch/MainActivity$127.class deleted file mode 100644 index 098e7c9..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$127.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$128.class b/_tests/out_app/com/lanremote/watch/MainActivity$128.class deleted file mode 100644 index 93b06c5..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$128.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$129.class b/_tests/out_app/com/lanremote/watch/MainActivity$129.class deleted file mode 100644 index b5275c7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$129.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$13.class b/_tests/out_app/com/lanremote/watch/MainActivity$13.class deleted file mode 100644 index bea0ce4..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$13.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$14.class b/_tests/out_app/com/lanremote/watch/MainActivity$14.class deleted file mode 100644 index 1758563..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$14.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$15.class b/_tests/out_app/com/lanremote/watch/MainActivity$15.class deleted file mode 100644 index 8d9a244..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$15.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$16.class b/_tests/out_app/com/lanremote/watch/MainActivity$16.class deleted file mode 100644 index 7069cd7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$16.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$17.class b/_tests/out_app/com/lanremote/watch/MainActivity$17.class deleted file mode 100644 index 88981c6..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$17.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$18.class b/_tests/out_app/com/lanremote/watch/MainActivity$18.class deleted file mode 100644 index f73c655..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$18.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$19.class b/_tests/out_app/com/lanremote/watch/MainActivity$19.class deleted file mode 100644 index 26bc39e..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$19.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$2.class b/_tests/out_app/com/lanremote/watch/MainActivity$2.class deleted file mode 100644 index 254a3c3..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$2.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$20.class b/_tests/out_app/com/lanremote/watch/MainActivity$20.class deleted file mode 100644 index ae1cef2..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$20.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$21.class b/_tests/out_app/com/lanremote/watch/MainActivity$21.class deleted file mode 100644 index c689fcb..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$21.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$22.class b/_tests/out_app/com/lanremote/watch/MainActivity$22.class deleted file mode 100644 index 4248b0f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$22.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$23.class b/_tests/out_app/com/lanremote/watch/MainActivity$23.class deleted file mode 100644 index c4e2a31..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$23.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$24.class b/_tests/out_app/com/lanremote/watch/MainActivity$24.class deleted file mode 100644 index 13aac1f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$24.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$25.class b/_tests/out_app/com/lanremote/watch/MainActivity$25.class deleted file mode 100644 index 1c70582..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$25.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$26.class b/_tests/out_app/com/lanremote/watch/MainActivity$26.class deleted file mode 100644 index 9366ca7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$26.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$27.class b/_tests/out_app/com/lanremote/watch/MainActivity$27.class deleted file mode 100644 index 95e4a65..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$27.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$28.class b/_tests/out_app/com/lanremote/watch/MainActivity$28.class deleted file mode 100644 index 8631a54..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$28.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$29.class b/_tests/out_app/com/lanremote/watch/MainActivity$29.class deleted file mode 100644 index c289bf5..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$29.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$3.class b/_tests/out_app/com/lanremote/watch/MainActivity$3.class deleted file mode 100644 index ec075cd..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$3.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$30.class b/_tests/out_app/com/lanremote/watch/MainActivity$30.class deleted file mode 100644 index 4743820..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$30.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$31.class b/_tests/out_app/com/lanremote/watch/MainActivity$31.class deleted file mode 100644 index 3a0d2f2..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$31.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$32.class b/_tests/out_app/com/lanremote/watch/MainActivity$32.class deleted file mode 100644 index 616039f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$32.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$33.class b/_tests/out_app/com/lanremote/watch/MainActivity$33.class deleted file mode 100644 index ccc4475..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$33.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$34.class b/_tests/out_app/com/lanremote/watch/MainActivity$34.class deleted file mode 100644 index 0d1a86c..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$34.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$35.class b/_tests/out_app/com/lanremote/watch/MainActivity$35.class deleted file mode 100644 index bc3871a..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$35.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$36.class b/_tests/out_app/com/lanremote/watch/MainActivity$36.class deleted file mode 100644 index 0a0167d..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$36.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$37.class b/_tests/out_app/com/lanremote/watch/MainActivity$37.class deleted file mode 100644 index 79300c5..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$37.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$38.class b/_tests/out_app/com/lanremote/watch/MainActivity$38.class deleted file mode 100644 index 2f00f30..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$38.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$39.class b/_tests/out_app/com/lanremote/watch/MainActivity$39.class deleted file mode 100644 index cd76f19..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$39.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$4.class b/_tests/out_app/com/lanremote/watch/MainActivity$4.class deleted file mode 100644 index 91aa18c..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$4.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$40.class b/_tests/out_app/com/lanremote/watch/MainActivity$40.class deleted file mode 100644 index 47c5a55..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$40.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$41.class b/_tests/out_app/com/lanremote/watch/MainActivity$41.class deleted file mode 100644 index cc35086..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$41.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$42.class b/_tests/out_app/com/lanremote/watch/MainActivity$42.class deleted file mode 100644 index 408c072..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$42.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$43.class b/_tests/out_app/com/lanremote/watch/MainActivity$43.class deleted file mode 100644 index b39c4d3..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$43.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$44.class b/_tests/out_app/com/lanremote/watch/MainActivity$44.class deleted file mode 100644 index 0ab5fdd..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$44.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$45.class b/_tests/out_app/com/lanremote/watch/MainActivity$45.class deleted file mode 100644 index fe3d366..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$45.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$46.class b/_tests/out_app/com/lanremote/watch/MainActivity$46.class deleted file mode 100644 index a5fe045..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$46.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$47.class b/_tests/out_app/com/lanremote/watch/MainActivity$47.class deleted file mode 100644 index 82d2a0e..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$47.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$48.class b/_tests/out_app/com/lanremote/watch/MainActivity$48.class deleted file mode 100644 index d93d56f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$48.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$49.class b/_tests/out_app/com/lanremote/watch/MainActivity$49.class deleted file mode 100644 index 0f8765b..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$49.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$5.class b/_tests/out_app/com/lanremote/watch/MainActivity$5.class deleted file mode 100644 index 7494643..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$5.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$50.class b/_tests/out_app/com/lanremote/watch/MainActivity$50.class deleted file mode 100644 index 36f860d..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$50.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$51.class b/_tests/out_app/com/lanremote/watch/MainActivity$51.class deleted file mode 100644 index d5d409c..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$51.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$52.class b/_tests/out_app/com/lanremote/watch/MainActivity$52.class deleted file mode 100644 index d100477..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$52.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$53.class b/_tests/out_app/com/lanremote/watch/MainActivity$53.class deleted file mode 100644 index 65585f9..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$53.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$54.class b/_tests/out_app/com/lanremote/watch/MainActivity$54.class deleted file mode 100644 index 0d43541..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$54.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$55.class b/_tests/out_app/com/lanremote/watch/MainActivity$55.class deleted file mode 100644 index e0ab782..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$55.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$56.class b/_tests/out_app/com/lanremote/watch/MainActivity$56.class deleted file mode 100644 index dfcbd81..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$56.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$57.class b/_tests/out_app/com/lanremote/watch/MainActivity$57.class deleted file mode 100644 index 6474055..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$57.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$58.class b/_tests/out_app/com/lanremote/watch/MainActivity$58.class deleted file mode 100644 index 6ea4ca5..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$58.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$59.class b/_tests/out_app/com/lanremote/watch/MainActivity$59.class deleted file mode 100644 index f87451e..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$59.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$6.class b/_tests/out_app/com/lanremote/watch/MainActivity$6.class deleted file mode 100644 index eee1935..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$6.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$60.class b/_tests/out_app/com/lanremote/watch/MainActivity$60.class deleted file mode 100644 index 8681f7e..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$60.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$61.class b/_tests/out_app/com/lanremote/watch/MainActivity$61.class deleted file mode 100644 index a7df84c..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$61.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$62.class b/_tests/out_app/com/lanremote/watch/MainActivity$62.class deleted file mode 100644 index 25edacb..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$62.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$63.class b/_tests/out_app/com/lanremote/watch/MainActivity$63.class deleted file mode 100644 index d53643d..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$63.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$64.class b/_tests/out_app/com/lanremote/watch/MainActivity$64.class deleted file mode 100644 index 55c3668..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$64.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$65.class b/_tests/out_app/com/lanremote/watch/MainActivity$65.class deleted file mode 100644 index b109711..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$65.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$66.class b/_tests/out_app/com/lanremote/watch/MainActivity$66.class deleted file mode 100644 index 02b4874..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$66.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$67.class b/_tests/out_app/com/lanremote/watch/MainActivity$67.class deleted file mode 100644 index 07115d7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$67.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$68.class b/_tests/out_app/com/lanremote/watch/MainActivity$68.class deleted file mode 100644 index a1d8fe7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$68.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$69.class b/_tests/out_app/com/lanremote/watch/MainActivity$69.class deleted file mode 100644 index df5e271..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$69.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$7.class b/_tests/out_app/com/lanremote/watch/MainActivity$7.class deleted file mode 100644 index c657cab..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$7.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$70.class b/_tests/out_app/com/lanremote/watch/MainActivity$70.class deleted file mode 100644 index 7bcdadb..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$70.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$71.class b/_tests/out_app/com/lanremote/watch/MainActivity$71.class deleted file mode 100644 index 89f4e09..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$71.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$72.class b/_tests/out_app/com/lanremote/watch/MainActivity$72.class deleted file mode 100644 index 0bec0af..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$72.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$73.class b/_tests/out_app/com/lanremote/watch/MainActivity$73.class deleted file mode 100644 index bdd4f9d..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$73.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$74.class b/_tests/out_app/com/lanremote/watch/MainActivity$74.class deleted file mode 100644 index d85f9dc..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$74.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$75.class b/_tests/out_app/com/lanremote/watch/MainActivity$75.class deleted file mode 100644 index b21ad72..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$75.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$76.class b/_tests/out_app/com/lanremote/watch/MainActivity$76.class deleted file mode 100644 index 15db5ef..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$76.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$77.class b/_tests/out_app/com/lanremote/watch/MainActivity$77.class deleted file mode 100644 index 379e5b1..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$77.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$78.class b/_tests/out_app/com/lanremote/watch/MainActivity$78.class deleted file mode 100644 index 50fc850..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$78.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$79$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$79$1.class deleted file mode 100644 index 6c32ff8..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$79$1.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$79.class b/_tests/out_app/com/lanremote/watch/MainActivity$79.class deleted file mode 100644 index c9bfca1..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$79.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$8.class b/_tests/out_app/com/lanremote/watch/MainActivity$8.class deleted file mode 100644 index e317ee1..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$8.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$80$1$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$80$1$1.class deleted file mode 100644 index 23c9655..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$80$1$1.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$80$1.class b/_tests/out_app/com/lanremote/watch/MainActivity$80$1.class deleted file mode 100644 index a3f83f7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$80$1.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$80.class b/_tests/out_app/com/lanremote/watch/MainActivity$80.class deleted file mode 100644 index cff6676..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$80.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$81.class b/_tests/out_app/com/lanremote/watch/MainActivity$81.class deleted file mode 100644 index c167e25..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$81.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$82.class b/_tests/out_app/com/lanremote/watch/MainActivity$82.class deleted file mode 100644 index cb60cec..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$82.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$83.class b/_tests/out_app/com/lanremote/watch/MainActivity$83.class deleted file mode 100644 index 32d36b7..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$83.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$84.class b/_tests/out_app/com/lanremote/watch/MainActivity$84.class deleted file mode 100644 index 336cb0d..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$84.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$85.class b/_tests/out_app/com/lanremote/watch/MainActivity$85.class deleted file mode 100644 index 9c486b5..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$85.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$86.class b/_tests/out_app/com/lanremote/watch/MainActivity$86.class deleted file mode 100644 index 2fbd205..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$86.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$87.class b/_tests/out_app/com/lanremote/watch/MainActivity$87.class deleted file mode 100644 index 9130692..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$87.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$88.class b/_tests/out_app/com/lanremote/watch/MainActivity$88.class deleted file mode 100644 index 20c2160..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$88.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$89.class b/_tests/out_app/com/lanremote/watch/MainActivity$89.class deleted file mode 100644 index 3af434a..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$89.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$9.class b/_tests/out_app/com/lanremote/watch/MainActivity$9.class deleted file mode 100644 index cf5bd3f..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$9.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$90.class b/_tests/out_app/com/lanremote/watch/MainActivity$90.class deleted file mode 100644 index 8d36c81..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$90.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$91.class b/_tests/out_app/com/lanremote/watch/MainActivity$91.class deleted file mode 100644 index d7980a6..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$91.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$92.class b/_tests/out_app/com/lanremote/watch/MainActivity$92.class deleted file mode 100644 index 507228e..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$92.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$93.class b/_tests/out_app/com/lanremote/watch/MainActivity$93.class deleted file mode 100644 index 688ce30..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$93.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$94.class b/_tests/out_app/com/lanremote/watch/MainActivity$94.class deleted file mode 100644 index 6bef3ad..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$94.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$95.class b/_tests/out_app/com/lanremote/watch/MainActivity$95.class deleted file mode 100644 index 0e3e578..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$95.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$96.class b/_tests/out_app/com/lanremote/watch/MainActivity$96.class deleted file mode 100644 index 43f4d89..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$96.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$97.class b/_tests/out_app/com/lanremote/watch/MainActivity$97.class deleted file mode 100644 index cdc5afb..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$97.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$98.class b/_tests/out_app/com/lanremote/watch/MainActivity$98.class deleted file mode 100644 index 39a8aaa..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$98.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$99.class b/_tests/out_app/com/lanremote/watch/MainActivity$99.class deleted file mode 100644 index 7f8f4c5..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$99.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$Act.class b/_tests/out_app/com/lanremote/watch/MainActivity$Act.class deleted file mode 100644 index 81ca798..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$Act.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity$TabDef.class b/_tests/out_app/com/lanremote/watch/MainActivity$TabDef.class deleted file mode 100644 index 1b160e0..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity$TabDef.class and /dev/null differ diff --git a/_tests/out_app/com/lanremote/watch/MainActivity.class b/_tests/out_app/com/lanremote/watch/MainActivity.class deleted file mode 100644 index ef4ff47..0000000 Binary files a/_tests/out_app/com/lanremote/watch/MainActivity.class and /dev/null differ diff --git a/_tests/test_apk_contents.py b/_tests/test_apk_contents.py index 7fda053..7773024 100644 --- a/_tests/test_apk_contents.py +++ b/_tests/test_apk_contents.py @@ -23,7 +23,7 @@ def _latest_apk() -> Path: (ROOT / "dist").glob("*watch*.apk"), key=lambda p: p.stat().st_mtime, reverse=True) return candidates[0] if candidates else \ - ROOT / "dist" / "ykxmz-watch-2.1.5.apk" + ROOT / "dist" / "ykxmz-watch-2.1.6.apk" APK = _latest_apk() diff --git a/_tests/test_mouse_control.py b/_tests/test_mouse_control.py new file mode 100644 index 0000000..55e4826 --- /dev/null +++ b/_tests/test_mouse_control.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +r'''验「远程鼠标控制」和「一键显示桌面」。 + +== 为什么要专门测 == + +这两条命令能让远端**直接操控电脑的鼠标键盘**,是最"有力量"的功能。 +一旦协议名写错、参数漏校验,表现会是"手表点了没反应", +而用户在教室里根本没法排查。 + +== 三层 == + + 一、协议常量在不在、值对不对 + 二、input.py 的坐标归一化算得对不对(纯计算,不真动鼠标) + 三、服务端的 _do_mouse 对各种参数的反应(含非法输入) + +== 为什么不真动鼠标 == + +第三层调的是服务端处理函数,它会真的调 input.move_to —— +但那只是把鼠标挪到某个像素,测试机上无所谓。 +真正的"灵不灵"得在人看来验,这里保证**逻辑和参数校验**不出错。 + +(show_desktop 也是真按 Win+D。测试时如果窗口全被最小化了, + 那就是它生效了 —— 不是 bug。) +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "windows")) + +FAILS: list = [] + + +def check(name: str, ok: bool, why: str = "") -> None: + print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name, + (" —— " + why) if why else "")) + if not ok: + FAILS.append(name) + + +def main() -> int: + print("=" * 58) + print(" 远程鼠标 + 一键显示桌面") + print("=" * 58) + print() + + print("一、协议常量") + from lanremote import protocol as P + check("CMD_MOUSE 存在且值是 mouse", + getattr(P, "CMD_MOUSE", "") == "mouse") + check("CMD_SHOW_DESKTOP 存在且值是 show_desktop", + getattr(P, "CMD_SHOW_DESKTOP", "") == "show_desktop") + + print() + print("二、坐标归一化(SendInput 要 0..65535)") + from lanremote import input as IN + + # 直接测归一化算式(把 _screen 固定成 1920x1080,保证可复现) + IN._screen_cache["v"] = (0, 0, 1920, 1080) + + captured = [] + real_send = IN._send + + def fake_send(*inputs): + captured.extend(inputs) + + IN._send = fake_send + try: + IN.move_to(0, 0) + mi = captured[-1].union.mi + check("左上角 -> (0,0)", (mi.dx, mi.dy) == (0, 0), + "拿到 (%d,%d)" % (mi.dx, mi.dy)) + + captured.clear() + IN.move_to(1919, 1079) + mi = captured[-1].union.mi + check("右下角 -> (65535,65535)", (mi.dx, mi.dy) == (65535, 65535), + "拿到 (%d,%d) —— 除以 (宽-1) 那条规则错了的话这里到不了 65535" + % (mi.dx, mi.dy)) + + captured.clear() + IN.move_to(960, 540) + mi = captured[-1].union.mi + # 960/1919*65535 = 32783.5 -> 32784;540/1079*65535 = 32800.7 -> 32801 + check("正中间约等于 32768", abs(mi.dx - 32768) < 60 + and abs(mi.dy - 32768) < 60, + "拿到 (%d,%d)" % (mi.dx, mi.dy)) + + captured.clear() + IN.move_to(99999, 99999) + mi = captured[-1].union.mi + check("超范围被夹到右下角", (mi.dx, mi.dy) == (65535, 65535), + "拿到 (%d,%d)" % (mi.dx, mi.dy)) + + captured.clear() + IN.move_to(-500, -500) + mi = captured[-1].union.mi + check("负坐标被夹到左上角", (mi.dx, mi.dy) == (0, 0), + "拿到 (%d,%d)" % (mi.dx, mi.dy)) + + # 滚轮:1 格 = 120 + captured.clear() + IN.scroll(1) + mi = captured[-1].union.mi + check("滚轮 1 格 = 120", mi.mouseData == 120, + "拿到 %d" % mi.mouseData) + captured.clear() + IN.scroll(-1) + mi = captured[-1].union.mi + check("滚轮 -1 格(向下)用无符号表示", + mi.mouseData == (0xFFFFFFFF - 119), + "拿到 %d" % mi.mouseData) + + # 夹取:一次滚几百格要拦住 + captured.clear() + IN.scroll(9999) + mi = captured[-1].union.mi + check("一次滚 9999 格被夹到 50", mi.mouseData == 50 * 120, + "拿到 %d" % mi.mouseData) + + # 组合键:Win+D 应该是 按下Win,按下D,抬起D,抬起Win + captured.clear() + IN.show_desktop() + check("Win+D 是 4 个事件(按下+抬起各两个)", len(captured) == 4, + "拿到 %d 个" % len(captured)) + if len(captured) == 4: + vks = [i.union.ki.wVk for i in captured] + ups = [bool(i.union.ki.dwFlags & IN.KEYEVENTF_KEYUP) + for i in captured] + check("Win+D 顺序正确(按下 LWIN,D;抬起 D,LWIN)", + vks == [IN.VK_LWIN, IN.VK_D, IN.VK_D, IN.VK_LWIN] + and ups == [False, False, True, True], + "vks=%s ups=%s" % (vks, ups)) + finally: + IN._send = real_send + IN._screen_cache.clear() + + print() + print("三、服务端 _do_mouse 参数校验") + import lanremote.server as S + cls = None + for name in dir(S): + c = getattr(S, name) + if isinstance(c, type) and hasattr(c, "_do_mouse"): + cls = c + break + check("找到定义了 _do_mouse 的类", cls is not None) + if cls is None: + return 1 + obj = cls.__new__(cls) + + IN._send = lambda *a: None # 测试里别真动鼠标 + try: + for action in ("move", "click", "down", "up", "scroll", "desktop", + "moveby"): + r = obj._do_mouse({"action": action, "x": 10, "y": 10, + "amount": 1}) + check(" action=%s 返回 ok" % action, + r.get("ok") is True, "拿到 %r" % r) + + r = obj._do_mouse({"action": "不存在的"}) + check("非法 action 被拒", r.get("ok") is False + and "不认识" in str(r.get("error")), + "拿到 %r" % r) + + r = obj._do_mouse({"action": "click", "button": "第四键"}) + check("非法鼠标键被拒", r.get("ok") is False, + "拿到 %r" % r) + + r = obj._do_mouse({"action": "scroll", "amount": "abc"}) + check("滚轮量不是数字被拒", r.get("ok") is False, + "拿到 %r" % r) + + r = obj._do_mouse({"action": "move", "x": "abc", "y": 1}) + check("坐标不是数字被拒", r.get("ok") is False, + "拿到 %r" % r) + finally: + IN._send = real_send + + print() + print("=" * 58) + if FAILS: + print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS))) + return 1 + print(" 全部通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/test_package_remote.py b/_tests/test_package_remote.py index a3ed3bd..67851f0 100644 --- a/_tests/test_package_remote.py +++ b/_tests/test_package_remote.py @@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent # 被控端 -> 一个 zip # 服务器端 -> 一个 zip # 控制端 -> 直接给 APK(不打 zip) -AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.5.zip" -SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.5.zip" -WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.5.apk" +AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.6.zip" +SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.6.zip" +WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.6.apk" TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain") JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe" JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe" @@ -89,7 +89,7 @@ def main() -> int: for label, path in (("被控端", agent_dir / "README-agent.txt"), ("服务器端", relay_dir / "README-server.txt"), ("控制端(旁边那份)", - ROOT / "dist" / "ykxmz-watch-2.1.5-说明.txt")): + ROOT / "dist" / "ykxmz-watch-2.1.6-说明.txt")): raw = path.read_bytes() if path.is_file() else b"" check(f"{label}说明有 UTF-8 BOM", raw[:3] == b"\xef\xbb\xbf", raw[:3]) diff --git a/_tests/test_update_flow.py b/_tests/test_update_flow.py new file mode 100644 index 0000000..6b8eecf --- /dev/null +++ b/_tests/test_update_flow.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +r'''验「静默自动更新」这条链路。 + +== 分三层验,每层都能独立定位问题 == + + 一、协议层 CMD_UPDATE 常量存在、分发能走到 + 二、updater 查版本 / 下载 / 校验(不起真进程) + 三、接进服务端之后,dispatch 的结构还对(曾经被补丁弄坏过) + +== 为什么第三层要单独验 == + +我插 _do_update 的时候,补丁把 dispatch 方法**兜底的那句 +`return {"ok": False, "error": f"未知命令: {cmd}"}` 一起挪进了 +_run_update 的末尾** —— 结果: + + * _run_update 里引用了不存在的 cmd(pyflakes 报 undefined name) + * dispatch 丢了兜底分支(未知命令会直接掉到函数末尾返回 None) + +pyflakes 只报了第一半,第二半得靠结构断言才看得出来。 +所以这里把「兜底 return 必须在 dispatch 里、且在 _do_update 之前」 +写成断言。 + +== 不起真进程 == + +`apply_update` 会起分离的 .cmd 并让进程退出 —— 测试里不能真跑, +只验到"包下好了、校验过了"为止。真安装由回归里的 +「安装包完整周期」那一关覆盖。 +''' +from __future__ import annotations + +import hashlib +import http.server +import json +import re +import socketserver +import sys +import threading +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "windows")) + +FAILS: list = [] + + +def check(name: str, ok: bool, why: str = "") -> None: + print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name, + (" —— " + why) if why else "")) + if not ok: + FAILS.append(name) + + +SERVER_PY = ROOT / "windows" / "lanremote" / "server.py" + + +def main() -> int: + print("=" * 58) + print(" 静默自动更新链路") + print("=" * 58) + print() + + # ---------------- 一、协议层 ---------------- + print("一、协议层") + from lanremote import protocol as P + check("CMD_UPDATE 存在", hasattr(P, "CMD_UPDATE"), + "协议里没有这条命令") + check("CMD_UPDATE 的值是 update", + getattr(P, "CMD_UPDATE", "") == "update") + + # ---------------- 二、updater ---------------- + print() + print("二、updater(查版本 / 下载 / 校验)") + from lanremote import updater as U + + # 版本比较:必须按数字段,不能按字符串 + cases = [("2.1.5", "2.1.4", True), ("2.1.4", "2.1.5", False), + ("2.1.5", "2.1.5", False), ("2.2.0", "2.1.9", True), + ("1.9.0", "2.0.0", False), ("2.1.10", "2.1.9", True)] + for new, cur, want in cases: + got = U._ver_tuple(new) > U._ver_tuple(cur) + check(" %s > %s == %s" % (new, cur, want), got == want, + "实际 %s" % got) + + # 没配中转地址要报错,不能默默成功 + try: + U.check("", "2.1.5") + check("没配中转地址时报错", False, "它竟然没报错") + except U.UpdateError: + check("没配中转地址时报错", True) + + # 起个假中转,真跑一遍 + pkg = b"MZ" + b"NEWSETUP" * 3000 + sha = hashlib.sha256(pkg).hexdigest() + + class FakeRelay(http.server.BaseHTTPRequestHandler): + def log_message(self, *a): # noqa: A003 + pass + + def do_GET(self): # noqa: N802 + if self.path.startswith("/v1/update/latest"): + body = json.dumps({ + "ok": True, "platform": "windows", "version": "9.9.9", + "name": "ykxmz-setup-9.9.9.exe", + "url": "/download/ykxmz-setup-9.9.9.exe", + "size": len(pkg), "sha256": sha, + }).encode() + self.send_response(200) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + elif self.path.startswith("/download/"): + self.send_response(200) + self.send_header("Content-Length", str(len(pkg))) + self.end_headers() + self.wfile.write(pkg) + else: + self.send_response(404) + self.end_headers() + + srv = socketserver.TCPServer(("127.0.0.1", 0), FakeRelay) + port = srv.server_address[1] + threading.Thread(target=srv.serve_forever, daemon=True).start() + try: + base = "http://127.0.0.1:%d" % port + info = U.check(base, "2.1.5") + check("查到最新版", info.get("latest") == "9.9.9", + "拿到 %r" % info.get("latest")) + check("判定需要更新", info.get("needUpdate") is True) + check("url 拼成了完整地址", + str(info.get("url", "")).startswith(base + "/download/"), + "拿到 %r" % info.get("url")) + check("拿得到 sha256", len(info.get("sha256", "")) == 64) + + work = ROOT / "_build" / "_tmp_update_test" + import shutil + shutil.rmtree(work, ignore_errors=True) + got = U.download(info["url"], work, info["sha256"], info["size"]) + check("下载成功", got.is_file() and got.stat().st_size == len(pkg)) + check("内容一致", got.read_bytes() == pkg) + + # 校验和不匹配必须拒绝 + try: + U.download(info["url"], work, "00" * 32, 0) + check("校验和不匹配时拒绝", False, "它竟然接受了") + except U.UpdateError: + check("校验和不匹配时拒绝", True) + + # 已是最新时不该说要更新 + info2 = U.check(base, "9.9.9") + check("已是最新时不提示更新", info2.get("needUpdate") is False) + finally: + srv.shutdown() + import shutil + shutil.rmtree(ROOT / "_build" / "_tmp_update_test", ignore_errors=True) + + # ---------------- 三、接进服务端后的结构 ---------------- + print() + print("三、服务端结构(补丁曾经在这里弄坏东西)") + src = SERVER_PY.read_text(encoding="utf-8") + + check("有 _do_update 方法", "def _do_update(self" in src) + check("有 _run_update 方法", "def _run_update(self" in src) + check("dispatch 里有 CMD_UPDATE 分支", + "if cmd == P.CMD_UPDATE:" in src) + + # ⚠ 关键:未知命令的兜底必须在 **dispatch 方法里**。 + # + # 第一版这个断言写的是"兜底**紧跟** CMD_UPDATE 分支" —— + # 后来往 dispatch 里插了 CMD_MOUSE / CMD_SHOW_DESKTOP, + # 兜底被推到后面,断言误报失败。**兜底本身是好的。** + # + # 改成验**结构真相**,不管中间插了多少分支: + # 从带 CMD_UPDATE 的那个方法体里找,必须有一句 8 空格缩进的兜底 return。 + dispatch_body = "" + for mm in re.finditer( + r"\n def [a-z_]+\(self[^\n]*\)[^\n]*:\n(.*?)(?=\n def |\Z)", + src, re.S): + if "if cmd == P.CMD_UPDATE:" in mm.group(1): + dispatch_body = mm.group(1) + break + + check("找得到 dispatch 方法体", bool(dispatch_body), + "没有哪个方法里有 CMD_UPDATE 分支") + check("dispatch 里有未知命令的兜底", + bool(re.search(r'^ return \{"ok": False, ' + r'"error": f"未知命令: \{cmd\}"\}', + dispatch_body, re.M)), + "兜底那句不在 dispatch 里了 —— 未知命令会返回 None") + + # 各个新方法里不能有那句兜底 + for meth in ("_run_update", "_do_update", "_do_mouse"): + mm = re.search(r"\n def %s\(self.*?(?=\n def |\Z)" % meth, + src, re.S) + body = mm.group(0) if mm else "" + check("%s 里没有引用 cmd" % meth, + "未知命令: {cmd}" not in body, + "兜底那句漏进 %s 了(会 NameError)" % meth) + + print() + print("=" * 58) + if FAILS: + print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS))) + return 1 + print(" 全部通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/verify_all.ps1 b/_tests/verify_all.ps1 index 4931573..472a6ea 100644 --- a/_tests/verify_all.ps1 +++ b/_tests/verify_all.ps1 @@ -502,6 +502,29 @@ $stageOut = & python (Join-Path $root "_build\check_win7_compat.py") 2>&1 Show-Fails $stageOut Record "Windows 7 兼容" ($LASTEXITCODE -eq 0) +# ---------------------------------------------------------------- 3a10 +Banner "3a10/7 静默自动更新链路" +# 用户要求:"手表端点击一个更新按钮,电脑端就可以自动进行静默更新, +# 其他什么设置都不变 …… 不弹任何窗口出来" +# 这条链是:中转 /v1/update/latest -> 被控端 updater.py 下载校验 +# -> 分离 .cmd 帮手 -> 静默装 -> 重启。 +# +# 这里验协议常量、版本比较(按数字段,不是字符串)、下载校验、 +# 以及**服务端 dispatch 的结构没被补丁弄坏**。 +$stageOut = & python (Join-Path $root "_tests\test_update_flow.py") 2>&1 + Show-Fails $stageOut +Record "静默自动更新" ($LASTEXITCODE -eq 0) + +# ---------------------------------------------------------------- 3a11 +Banner "3a11/7 远程鼠标 + 一键显示桌面" +# 鼠标注入是最"有力量"的功能,参数校验写错的话表现是"手表点了没反应", +# 教室里没法排查。这里盯坐标归一化(除以 宽-1 那条规则)、 +# 滚轮夹取、Win+D 的事件顺序、以及非法参数必须被拒。 +$stageOut = & python (Join-Path $root "_tests\test_mouse_control.py") 2>&1 + Show-Fails $stageOut +Record "远程鼠标控制" ($LASTEXITCODE -eq 0) + + # ---------------------------------------------------------------- 3a5 Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)" # 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」—— diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 474e7ba..9dd228c 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="2.1.6">