mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
用户这次提了一整套升级,这是最后一批。
== 一、手表端顶部快捷栏 ==
用户原话:"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页
自定义,最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"。
("返回桌面"经确认 = **让被控电脑显示桌面**,即 Win+D。)
* 一条常驻的栏,贴在屏幕顶部,切页面时不动
* **4 个格子**:3 个可自定义 + **1 个固定的「显示桌面」(改不了)**
* 7 个可选动作:传文件 / 屏幕镜像 / 鼠标控制 / 音效 / 关机 /
语音播报 / 投图到电脑
* 设置页里点一下换;也能把某一格留空
* 没连上电脑时按钮置灰(不是藏起来 —— "一直在但暂时不能用"
比"时有时无"更好理解)
实现上的几个要点:
* 快捷栏 addView 在 5 个面板**之后** —— FrameLayout 里后加的在上层,
加在前面会被面板(MATCH_PARENT)整个盖住
* 面板加了 topMargin 给栏让位,集中在 applyPanelTopMargin 里改
(每个面板各写一遍很容易漏一个,表现是"就某一页顶部被切掉")
* 第 4 格在代码里**写死** desktop,而且 QUICK_ACTIONS 里**没有**它 ——
这样它既不会出现在可选列表里,也不可能被改成别的
* 存储用 SharedPreferences。读取时**过滤不认识的 id + 补齐到 3 个**:
以后删了某个动作,老配置也不会让栏变空或下标越界
== 二、远程鼠标控制(手表上的触摸板)==
* 拖动 = 移动光标,单击 = 左键,双击 = 右键
* 下面还有专门的 左键 / 右键 / 滚轮上 / 滚轮下 按钮
* 电脑端用 SendInput 注入(见下)
== 三、弹文字窗口的位置和大小可配 ==
用户原话:"弹文字窗口 …… 位置和大小都能自定义"。
* 位置:右下(默认,保持老习惯)/ 右上 / 左上 / 左下 /
屏幕正中间 / 自定义坐标
* 字号:8~32(夹住,免得设成 200 一个通知占满屏幕)
* 宽度:可指定,0 = 按内容自适应
* 三处窗口定位统一到一个 _notice_geometry() —— 以前三处各写死一遍
右下角,改一次要改三个地方,漏一个用户就看得见
== 四、屏幕被观看时的提示条改成默认不显示 ==
用户原话:"以后不需要弹,啥窗户都没有,就可以像 UU 远程一样"。
* 默认**连窗口都不建**
* **但保留成开关**,设置里可以打开
关于这条:那条提示原来是**故意加**的(有人在看屏幕时,电脑前面的人
应该知道)。用户要求去掉,这是他自己的电脑、自己的教室,
属于正当的远程管理需求。所以能力保留、默认关闭 ——
这软件应该始终有一个"让本机用户看得见正在被控制"的选项可查。
== 五、中转后台能看到版本并远程点更新 ==
* 设备表新增「版本」列
* 每行一个「更新」按钮:点一下让那台电脑自己静默升级
* 离线设备的按钮**置灰并说明原因**(命令只会挂在队列里等它上线,
那时用户早忘了自己点过,突然重启很莫名其妙)
* 和手表那边走**同一条命令**(cmd=update, action=start),
不给"后台触发"单独写一套逻辑 —— 少一套就少一处不一致
== 六、手表端可开关「允许新设备连接」 ==
* 关掉后只有配对过的电脑能连,教室里防别人乱连
* 电脑端新增 _do_allow_new_pairing 这个 feature
* 只影响**配对新设备**,不踢现有会话
(否则老师一点"禁止",自己正连着的那台也被踢掉,成了"自杀开关")
== 修掉的几个自己造的坑 ==
1. **管理页 JS 裸换行** —— 最严重的一个。
ADMIN_JS 是 Python 三引号字符串,我在里面写 `\n` 拼多行文案,
结果是**真的换行**插进 JS,字符串字面量跨行 -> 语法非法 ->
**整个 script 块解析失败 -> 管理页所有按钮全死**(改码/删除/踢下线/全选)。
改成 String.fromCharCode(10),不依赖任何转义层。
2. **路由落进了 do_GET** —— 我拿 "没有这个管理接口" 当锚点,
但那个字符串在文件里有**两处**(do_GET 和 _route_post 各一个),
于是路由加到了 GET 块里,管理页的 POST 打不到它。
表现是"点了更新没反应"。
3. **isConnected() 根本不存在** —— 写快捷栏时凭直觉调了个方法名,
编译才发现。顺手包了一层并加上说明。
4. **两个不存在的方法名** —— onCreate 里写了
buildMousePanelIfNeeded / mousePanelHolder,编译报 cannot find symbol。
5. **未调用方法扫描的假阳性** —— GestureDetector 的
onSingleTapConfirmed / onDoubleTap / onScroll 是框架回调,
扫描把"名字只出现 1 次"当成死代码。改成**跳过 @Override 的方法**。
这种假阳性最坏的结果是有人为了让它变绿把回调删了,手势就失灵了。
6. **测试断言太严导致误报** —— "不能有写死的 240",
但 helper 里 `auto = max(240, ...)` 是自适应的兜底,故意的。
7. **屏幕共享提示那个测试断言的是旧行为** —— 没有简单删掉,
改成验两个方向:开关打开时提示条正常(位置/尺寸/出现/消失),
关闭时默认值必须是 False 且代码路径上不建窗口。
== 验证 ==
全量回归(19 关)全部 PASS,零失败
新增关卡:3a10 静默更新 / 3a11 鼠标控制 / 3a12 后台更新+弹窗 /
3a13 手表快捷栏
Java 编译通过(android-34, --release 8)
APK / 安装包 / 服务端包 全部重建,Win7 兼容校验通过
623 个 Python 文件语法通过
169 lines
6.3 KiB
Python
169 lines
6.3 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''验「后台点一下让电脑自己更新」这条链路。
|
||
|
||
== 链路 ==
|
||
|
||
管理页点「更新」
|
||
-> POST /v1/admin/update_device {deviceId}
|
||
-> Relay.request_update() 把 {"cmd":"update","action":"start"}
|
||
塞进该设备的 to_agent 队列
|
||
-> 被控端下一次长轮询拿到它
|
||
-> server.py 的 _do_update:查最新版 -> 下载 -> 校验 -> 静默装 -> 重启
|
||
|
||
== 为什么要专门测 ==
|
||
|
||
这条链路横跨三端(管理页 JS -> 中转 -> 被控端),任何一环写错,
|
||
表现都是"我点了更新,电脑没反应" —— 而用户在办公室里没法排查。
|
||
|
||
而且它**会重启别人的电脑**,所以几个边界必须卡死:
|
||
|
||
* 设备不存在 -> 明确说没有,不能静默成功
|
||
* 设备离线 -> 直接拒。命令挂在队列里等它上线才执行,
|
||
那时用户早忘了,突然重启很莫名其妙
|
||
* 重复点 -> 不重复塞(队列会堆积)
|
||
* 在线 -> 真的塞进队列,而且内容就是那条更新命令
|
||
|
||
== 上一版测试自己写错的地方 ==
|
||
|
||
我用了个 FakeDevice,`__init__` 里默认 `_on = True`,
|
||
结果"离线设备"那条用例测的其实是**在线**设备 —— 假绿。
|
||
现在 FakeDevice 显式接收 online 参数,不再有隐藏默认值。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
import sys
|
||
import threading
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT / "relay"))
|
||
|
||
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)
|
||
|
||
|
||
class FakeDevice:
|
||
"""假的被控端。online 必须显式给,不给就报错 ——
|
||
第一版给了个 True 的默认值,导致"离线"用例永远测不到离线。"""
|
||
|
||
def __init__(self, online: bool):
|
||
self.to_agent = bytearray()
|
||
self._online = bool(online)
|
||
self.lan_ips: list = []
|
||
self.port = 0
|
||
|
||
def online(self) -> bool:
|
||
return self._online
|
||
|
||
def push_to_agent(self, data: bytes) -> None:
|
||
self.to_agent.extend(data)
|
||
|
||
|
||
def main() -> int:
|
||
print("=" * 58)
|
||
print(" 后台「更新」按钮链路")
|
||
print("=" * 58)
|
||
print()
|
||
|
||
import relay as R
|
||
|
||
print("一、Relay.request_update 的边界")
|
||
r = R.Relay.__new__(R.Relay)
|
||
r.lock = threading.RLock()
|
||
r.devices = {}
|
||
|
||
res = r.request_update("不存在的设备")
|
||
check("设备不存在 -> 拒绝", res.get("ok") is False,
|
||
"拿到 %r" % res)
|
||
check(" 错误信息说清楚是没这台", "没有这台设备" in str(res.get("error")),
|
||
"拿到 %r" % res.get("error"))
|
||
|
||
check("设备 id 为空也当不存在",
|
||
r.request_update("").get("ok") is False)
|
||
check(" None 也不炸", r.request_update(None).get("ok") is False)
|
||
|
||
off = FakeDevice(online=False)
|
||
r.devices["off1"] = off
|
||
res = r.request_update("off1")
|
||
check("离线设备 -> 拒绝", res.get("ok") is False, "拿到 %r" % res)
|
||
check(" 理由提到离线", "离线" in str(res.get("error")),
|
||
"拿到 %r" % res.get("error"))
|
||
check(" 离线时**没往队列里塞东西**", len(off.to_agent) == 0,
|
||
"队列里有 %d 字节 —— 离线时不该堆命令"
|
||
% len(off.to_agent))
|
||
|
||
on = FakeDevice(online=True)
|
||
r.devices["on1"] = on
|
||
res = r.request_update("on1")
|
||
check("在线设备 -> 成功", res.get("ok") is True, "拿到 %r" % res)
|
||
check(" 队列里真的放了东西", len(on.to_agent) > 0)
|
||
raw = bytes(on.to_agent).decode("utf-8")
|
||
check(" 是换行结尾的 JSON(协议要求)", raw.endswith("\n"),
|
||
"尾部是 %r" % raw[-6:])
|
||
try:
|
||
msg = json.loads(raw.strip())
|
||
except Exception as exc: # noqa: BLE001
|
||
check(" 内容是合法 JSON", False, str(exc))
|
||
msg = {}
|
||
check(" 内容是合法 JSON", bool(msg))
|
||
check(" cmd == update", msg.get("cmd") == "update", "拿到 %r" % msg)
|
||
check(" action == start", msg.get("action") == "start",
|
||
"拿到 %r" % msg)
|
||
|
||
n_before = len(on.to_agent)
|
||
res = r.request_update("on1")
|
||
check("重复点 -> 不重复塞", len(on.to_agent) == n_before,
|
||
"队列从 %d 涨到 %d" % (n_before, len(on.to_agent)))
|
||
check(" 但仍然返回 ok(用户看到的是「已经在排队」)",
|
||
res.get("ok") is True and "排队" in str(res.get("message")),
|
||
"拿到 %r" % res)
|
||
|
||
print()
|
||
print("二、HTTP 接口接到了 Relay 方法上")
|
||
src = (ROOT / "relay" / "relay.py").read_text(encoding="utf-8")
|
||
check("有 /v1/admin/update_device 路由",
|
||
'"/v1/admin/update_device"' in src)
|
||
m = re.search(r'if path == "/v1/admin/update_device":(.{0,300})',
|
||
src, re.S)
|
||
body = m.group(1) if m else ""
|
||
check(" 它调的是 RELAY.request_update", "RELAY.request_update(" in body,
|
||
"应该走 Relay 的方法,而不是在 HTTP 处理里伸手掏 RELAY.devices")
|
||
check(" 它把 deviceId 传下去了", 'get("deviceId")' in body)
|
||
check("有 Relay.request_update 方法", "def request_update(self" in src)
|
||
|
||
print()
|
||
print("三、管理页那边")
|
||
adm = (ROOT / "relay" / "admin.py").read_text(encoding="utf-8")
|
||
check("设备行有「更新」按钮", 'data-update="%s"' in adm)
|
||
check("JS 里有 data-update 的处理",
|
||
"querySelectorAll('button[data-update]')" in adm)
|
||
check("JS 会二次确认(别误点就重启别人电脑)",
|
||
"confirm(" in adm and "更新到最新版" in adm)
|
||
check("离线设备的按钮带 disabled",
|
||
'disabled title="设备离线' in adm)
|
||
check("设备行显示了版本", 'data-l="版本"' in adm)
|
||
check("表头也有版本列", "<th>版本</th>" in adm)
|
||
# 空表提示的 colspan 要跟列数对上(加了版本列之后是 9)
|
||
check("空表 colspan 是 9(跟列数对上)", 'colspan="9"' in adm,
|
||
"加列之后忘了改 colspan,空表时表格会错位")
|
||
|
||
print()
|
||
print("=" * 58)
|
||
if FAILS:
|
||
print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS)))
|
||
return 1
|
||
print(" 全部通过")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|