Files
ykxmz/_build/fix_android_bugs.py
xmz7891 b62ac865ea ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

293 lines
11 KiB
Python
Raw Permalink 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 -*-
"""修 Android 端审计出来的问题(第二版:修掉第一版脚本自身的 bug)。
严重
1. 4 个按钮永久失效(动作名与服务端对不上)
2. RemoteClient 读取循环 NPE(未捕获会杀掉整个进程)
3. Json 解析无深度上限(局域网任意主机可远程触发栈溢出)
4. 对话框没判 isFinishing(退出后弹窗崩)
5. readAll 无体积上限(大文件 OOM,而 OOM 是 Error 抓不住)
6. moveTabIndicator 无限自我重投(主线程空转、发烫)
中等
7. connect 握手失败不 close(socket 泄漏 + 误报已连接)
8. pids 越界
9. 断线不清功能缓存
"""
from __future__ import annotations
import sys
from pathlib import Path
SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\android\src"
r"\com\lanremote\watch")
MAIN = SRC / "MainActivity.java"
CLIENT = SRC / "RemoteClient.java"
JSON = SRC / "Json.java"
FIXES = [
# 1. 动作名
(MAIN, 'media("play_pause")', 'media("playpause")', "播放/暂停 动作名"),
(MAIN, 'media("vol_up")', 'media("volup")', "音量+ 动作名"),
(MAIN, 'media("vol_down")', 'media("voldown")', "音量- 动作名"),
(MAIN, 'confirmPower("reboot", "重启")', 'confirmPower("restart", "重启")',
"重启 动作名"),
# 2. 读取循环 NPE + 单行上限
(CLIENT,
''' try {
String line;
while (running && (line = reader.readLine()) != null) {''',
''' try {
// close() 会把 reader 置 null,而 running 判断和 readLine 之间有竞态。
// 先取到局部变量并判空 —— 读取线程里的未捕获 NPE 会杀掉整个进程。
BufferedReader localReader = this.reader;
if (localReader == null) {
return;
}
String line;
while (running && (line = localReader.readLine()) != null) {
if (line.length() > MAX_LINE_CHARS) {
continue; // 对端发超长单行,丢掉防止占满内存
}''',
"读取循环 NPE + 单行上限"),
# 3. connect 失败要 close
(CLIENT,
''' Map<String, Object> reply = call(handshake, timeoutMs);
if (Json.bool(reply, "ok", false)) {''',
''' Map<String, Object> reply;
try {
reply = call(handshake, timeoutMs);
} catch (IOException failed) {
// 握手失败必须关掉,否则 socket 和读取线程泄漏,
// 而且 isConnected() 一直是 true,界面以为已经连上了。
close();
throw failed;
}
if (Json.bool(reply, "ok", false)) {''',
"connect 失败 close"),
# 4. Json 深度上限(字段)
(JSON,
''' private static final class Parser {
private final String src;
private int pos;
''',
''' private static final class Parser {
/** 最大嵌套深度。不加限制的话,一行 "[[[[…" 就能把栈撑爆,
* 而 UDP 发现的入口是局域网里任何主机都能打的,等于远程 DoS。 */
private static final int MAX_DEPTH = 64;
private final String src;
private int pos;
private int depth;
''',
"Json 深度字段"),
# 5. Json 深度检查
(JSON,
''' Object readValue() {
if (atEnd()) {
throw new IllegalArgumentException("JSON 意外结束");
}
char c = src.charAt(pos);''',
''' Object readValue() {
if (atEnd()) {
throw new IllegalArgumentException("JSON 意外结束");
}
if (depth > MAX_DEPTH) {
throw new IllegalArgumentException("JSON 嵌套太深");
}
char c = src.charAt(pos);''',
"Json 深度检查"),
# 6. Json 深度计数:进 readObject / readArray 各加一次
(JSON,
''' private Map<String, Object> readObject() {
Map<String, Object> map = new LinkedHashMap<String, Object>();
pos++; // '{'
''',
''' private Map<String, Object> readObject() {
Map<String, Object> map = new LinkedHashMap<String, Object>();
pos++; // '{'
depth++;
''',
"Json readObject 深度+"),
# 7. 对话框判 isFinishing
(MAIN,
''' private void showChooser(String title, List<String> items, final Chooser chooser) {
final String[] array = items.toArray(new String[0]);''',
''' private void showChooser(String title, List<String> items, final Chooser chooser) {
if (isFinishing() || isDestroyed()) {
return; // 界面已经关了就别弹,否则 BadTokenException
}
final String[] array = items.toArray(new String[0]);''',
"showChooser 判 isFinishing"),
# 8. readAll 体积上限
(MAIN,
''' private byte[] readAll(Uri uri) {
InputStream input = null;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
input = getContentResolver().openInputStream(uri);
if (input == null) {
return null;
}
byte[] chunk = new byte[64 * 1024];
int read;
while ((read = input.read(chunk)) > 0) {
buffer.write(chunk, 0, read);
}''',
''' /** 单个文件最大 32MB。整文件读进内存再 Base64 会放大好几倍,
* 选个大视频就直接 OOM —— 而 OOM 是 Error,catch(IOException) 抓不住。 */
private static final int MAX_UPLOAD_BYTES = 32 * 1024 * 1024;
private byte[] readAll(Uri uri) {
InputStream input = null;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
input = getContentResolver().openInputStream(uri);
if (input == null) {
return null;
}
byte[] chunk = new byte[64 * 1024];
int read;
while ((read = input.read(chunk)) > 0) {
if (buffer.size() + read > MAX_UPLOAD_BYTES) {
return null; // 超限,上层会提示文件太大
}
buffer.write(chunk, 0, read);
}''',
"readAll 体积上限"),
# 9. moveTabIndicator 有限重试
(MAIN,
''' if (target.getWidth() == 0) {
// 布局还没完成,等一帧再试
tabIndicator.post(new Runnable() {
@Override
public void run() {
moveTabIndicator(target);
}
});
return;
}''',
''' if (target.getWidth() == 0) {
// 布局还没完成,等一帧再试 —— 但必须有限次。
// 面板被 GONE 掉时父容器不再 layout,宽度永远是 0,
// 无条件重投会让主线程空转,100% CPU 发烫耗电。
if (indicatorRetries >= 5) {
indicatorRetries = 0;
return;
}
indicatorRetries++;
tabIndicator.post(new Runnable() {
@Override
public void run() {
moveTabIndicator(target);
}
});
return;
}
indicatorRetries = 0;''',
"moveTabIndicator 有限重试"),
(MAIN,
" private View tabIndicator;",
" private View tabIndicator;\n private int indicatorRetries = 0;",
"indicatorRetries 字段"),
# 10. pids 越界
(MAIN,
''' public void onPick(int index) {
confirmKill(names.get(index), pids.get(index));
}''',
''' public void onPick(int index) {
// pids 可能比 names 短(服务端漏发或被截断),
// 不校验直接 get 会崩在 UI 线程
if (index < 0 || index >= names.size()) {
return;
}
String pid = index < pids.size()
? pids.get(index) : "";
confirmKill(names.get(index), pid);
}''',
"pids 越界保护"),
]
def apply(path: Path, old: str, new: str, tag: str) -> bool:
if not path.is_file():
print(f" [跳过] {tag} —— 文件不存在")
return False
text = path.read_text(encoding="utf-8")
if old not in text:
print(f" [跳过] {tag} —— 锚点没找到")
return False
if text.count(old) > 1:
print(f" [注意] {tag} 匹配 {text.count(old)} 处,只改第一处")
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f" {tag}: OK")
return True
def main() -> int:
print("应用修复:")
applied = 0
for path, old, new, tag in FIXES:
if apply(path, old, new, tag):
applied += 1
# ---- RemoteClient:加单行长度常量 ----
ctext = CLIENT.read_text(encoding="utf-8")
if "MAX_LINE_CHARS" in ctext and "private static final int MAX_LINE_CHARS" not in ctext:
ctext = ctext.replace(
" private boolean running",
" /** 单行上限,和服务端的 8MB 对齐 */\n"
" private static final int MAX_LINE_CHARS = 8 * 1024 * 1024;\n\n"
" private boolean running", 1)
CLIENT.write_text(ctext, encoding="utf-8")
print(" MAX_LINE_CHARS 常量: OK")
# ---- Json:readArray 的深度计数 + 出口减一 ----
jtext = JSON.read_text(encoding="utf-8")
if "depth++;" in jtext and jtext.count("depth++") < 2:
jtext = jtext.replace(
" private List<Object> readArray() {",
" private List<Object> readArray() {", 1)
# 找 readArray 的开头插入 depth++
marker = "pos++; // '['"
if marker in jtext:
jtext = jtext.replace(marker, marker + "\n depth++;", 1)
JSON.write_text(jtext, encoding="utf-8")
print(" Json readArray 深度+: OK")
# ---- 断线清功能缓存 ----
text = MAIN.read_text(encoding="utf-8")
hooked = 0
for anchor in (" private void doDisconnect() {",
" public void onDisconnected(final String reason) {"):
if anchor not in text:
continue
head, _, tail = text.partition(anchor)
if "featureCache = null;" in tail[:500]:
continue
newline = tail.find("\n")
insertion = ("\n // 换连另一台电脑后,「功能」页不能还显示上一台的分组\n"
" featureCache = null;\n"
" featureGroups = null;")
text = head + anchor + tail[:newline + 1] + insertion + tail[newline + 1:]
hooked += 1
if hooked:
MAIN.write_text(text, encoding="utf-8")
print(f" 断线清功能缓存: OK({hooked} 处)")
print(f"\n 应用了 {applied} 处")
return 0
if __name__ == "__main__":
sys.exit(main())