Files
ykxmz/_tests/test_apk_contents.py
T
xmz7891 5fc2f97be7 2.1.3:搬到 Lightsail 真服务器 + 支持"没有证书的中转"
用户买了 AWS Lightsail(2G 内存 / 2 vCPU / 60G SSD / 公网 IP
13.54.30.196),中转从随身 WiFi 那台小机器搬过去。

== 为什么这次搬迁解决了一堆老问题 ==

原机器(Debian aarch64、460MB、走 4G)一直有几件搞不定的:

  * 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**(443 通、7844 不通),
    隧道每 3~10 分钟掉一次、公网跟着 530 —— 查了很久才从
    cloudflared 自己的 precheck 里看清
  * 只有 460MB 内存
  * 会掉电硬挂(实测 2026-10-08 18:42 一次非正常关机)

新机器**有公网 IP**,中转直接监听,不经隧道、不过 7844。
实测公网延迟从 **1000~2000 ms 降到 1.1 ms**。

== 代码改动 ==

1. **relay.py 的 DATA_DIR 默认值不再写死**
   原来是 /root/lanremote-relay。新机器上服务以 admin 普通用户跑
   (它没理由要 root),而 /root 是 0700 —— 部署时直接
   PermissionError: '/root/lanremote-relay'。
   改成"本脚本所在目录下的 data/",搬到哪都跟着走。

2. **客户端支持"没有证书的中转"**
   新服务器是明文 HTTP(没签证书),而两个客户端原来都往 https 走:
     * 手表端:裸 IP 会被强制补 https:// -> 永远连不上,
       报的还是 SSL 错误,很容易让人白查半天证书
     * Android 9+ 默认禁止明文流量,没 usesCleartextTraffic
       请求会被系统直接掐掉
   Windows 端本来就是对的(正则里把任意纯 IP 都当"用明文")。
   现在手表端规则和它一致,并加了 usesCleartextTraffic。

3. 清掉电脑端一段函数内的重复代码(return 之后还有一份拷贝,
   缩进仍在函数体内所以不报错,但读起来很迷惑)。

== 新服务器上的部署 ==

    /opt/ykxmz-relay/            服务端(relay/admin/landing/showcase)
    ykxmz-relay.service          监听 0.0.0.0:13245
    ykxmz-monitor.service/.timer 每 60 秒查公网,连续 3 次不通就叫预案
    rescue-lightsail.sh          四级阶梯抢救(重启中转 / 强杀重拉 /
                                 查防火墙 / 重启整机[默认关])

  预案针对新架构重写过:原来围着隧道转(重启隧道、重施 MSS/MTU),
  现在没有隧道,改成重启中转 + 查端口/防火墙。

全量回归 46 个阶段全部通过。
2026-10-08 22:29:56 +08:00

148 lines
5.3 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 -*-
"""检查手表 APK 是否真的把「遥控诊断」合并进去了。
合并前是两个 APK(手表端 + 独立的遥控诊断 App)。
合并后只需要一个:主程序里有「设备诊断」按钮。
这里从 APK 的 dex 和清单里核实,而不是只信源码。
"""
from __future__ import annotations
import re
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 别写死版本号 —— 一升级就坏(这坑踩过好几回了)。
# 直接扫 dist 里最新的手表 APK。
def _latest_apk() -> Path:
candidates = sorted(
(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.3.apk"
APK = _latest_apk()
DIAG_APK = ROOT / "dist" / "LanRemote-Diag-1.1.0.apk"
AAPT = (Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
/ "android-sdk" / "build-tools" / "34.0.0" / "aapt.exe")
PASSED = 0
FAILED = 0
def check(name: str, ok: bool, detail: str = "") -> None:
global PASSED, FAILED
if ok:
PASSED += 1
print(f" [PASS] {name}")
else:
FAILED += 1
print(f" [FAIL] {name} {detail}")
def main() -> int:
print("=" * 68)
print("一、手表 APK 里有没有内置诊断")
print("=" * 68)
if not APK.is_file():
print(f" 找不到 {APK}")
print(" 请先运行 android\\build_apk.ps1")
return 1
with zipfile.ZipFile(APK) as archive:
dex = archive.read("classes.dex")
names = {item.decode() for item in
re.findall(rb"Lcom/lanremote/[A-Za-z0-9_/]+;", dex)}
print(f" dex 中的类: {len(names)} 个")
check("APK 里有 Diagnostics 类(诊断逻辑已内置)",
"Lcom/lanremote/watch/Diagnostics;" in names,
str(sorted(names)))
check("APK 里有 DiscoveryCore(诊断与主程序共用同一份发现实现)",
"Lcom/lanremote/watch/DiscoveryCore;" in names)
check("APK 里有 MainActivity", "Lcom/lanremote/watch/MainActivity;" in names)
# 注意:不能用 "diag" 做子串判断 —— Diagnostics 里也含 diag。
# 要查的是旧的独立包名 com/lanremote/diag/。
legacy = [n for n in names if n.startswith("Lcom/lanremote/diag/")]
check("不含旧的独立诊断包 com.lanremote.diag",
not legacy, str(legacy))
print()
print("=" * 68)
print("二、清单权限(诊断读 WiFi 名称需要定位权限)")
print("=" * 68)
# aapt 不认中文路径,先拷到纯 ASCII 路径
with tempfile.TemporaryDirectory() as tmp:
simple = Path(tmp) / "watch.apk"
simple.write_bytes(APK.read_bytes())
try:
proc = subprocess.run(
[str(AAPT), "dump", "badging", str(simple)],
capture_output=True, timeout=60)
badging = proc.stdout.decode("utf-8", errors="replace")
except Exception as exc: # noqa: BLE001
badging = ""
print(f" aapt 调用失败: {exc}")
if badging:
for permission, why in (
("android.permission.ACCESS_FINE_LOCATION", "读 WiFi 名称"),
("android.permission.INTERNET", "网络通信"),
("android.permission.CHANGE_WIFI_MULTICAST_STATE", "UDP 广播"),
):
check(f"声明了 {permission.split('.')[-1]}({why})",
permission in badging)
check("没有申请定位以外的敏感权限",
"READ_CONTACTS" not in badging
and "READ_SMS" not in badging
and "CAMERA" not in badging
and "RECORD_AUDIO" not in badging)
else:
check("能读取 APK 清单", False, "aapt 没有输出")
print()
print("=" * 68)
print("三、构建脚本不再依赖独立诊断 APK")
print("=" * 68)
package = (ROOT / "_build" / "package.py").read_text(encoding="utf-8")
check("package.py 不再要求 Diag APK",
"DIAG_APK" not in package, "仍引用 DIAG_APK")
check("package.py 说明里写明诊断已合并",
"合并" in package)
src = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
/ "MainActivity.java").read_text(encoding="utf-8")
# 用户要求:"安卓端中转设置下面,把那个设备诊断给删了,
# 换成输入配对码,输入对应配对码就可以连接对应电脑"
# 所以这里反过来断言:不该再有设备诊断入口,而是配对码输入。
import re as _re
code = _re.sub(r"//[^\n]*", "", src)
check("主界面不再有「设备诊断」入口", "设备诊断" not in code)
check("主界面不再调用 Diagnostics.build",
"Diagnostics.build(" not in code)
check("主界面不再调用 Diagnostics.manualTest",
"Diagnostics.manualTest(" not in code)
check("换成了「输入配对码连接」入口",
'bigButton("输入配对码连接"' in code)
check("配对码连接走中转(connectByCode)",
"private void connectByCode(" in code)
print()
print("=" * 68)
print(f"通过 {PASSED} 项,失败 {FAILED} 项")
print("=" * 68)
return 1 if FAILED else 0
if __name__ == "__main__":
sys.exit(main())