☰
第 2 篇|搭入口层:提权、进程连接与日志
2026/11/5 6:53:27 网站建设 项目流程

系列:从 0 到 1 搭建桌面应用 UI 自动化测试框架
上一篇:第 1 篇|先盘点约束,再决定怎么做
下一篇:第 3 篇|跑通业务主链路
关键词:UAC、UIPI、AppSession、窗口反查 PID、Tee

本篇导读

第一道真正的墙是权限,第二道是进程。只要这两件事不稳,后面所有用例都是沙上建塔。本篇讲清楚三件事:

  1. 为什么必须统一提权,怎么做;
  2. 为什么 PID 不可信,怎么用窗口反查 PID;
  3. 提权后日志为什么看不见,怎么解决。

一句话:PID 不可信,窗口才是事实;多层 launcher + UIPI 场景,必须 EnumWindows 反查 + 承载校验。

流程图:一次入口执行的完整链路

否

是

不通过

通过

否

是

python run.py

IsUserAnAdmin() == 1?

ShellExecute runas 重新拉起

taskkill 清理残留 DesktopApp.exe

pytest tests/

AppSession.start()

Application.start()

EnumWindows 枚举顶层窗口

从窗口句柄反查候选 PID

排除系统窗口 / Qt 辅助窗口 / 浏览器窗口

connect(process=pid)

该 PID 承载桌面应用窗口?

返回可用 Application

1. 统一提权

原则:提权代码只能有一份,所有入口脚本都复用它。

# utils/elevation.py 的调用方式fromutils.elevationimportelevate,is_adminifnotis_admin():returnelevate(sys.argv[1:])

三个入口都遵循这个模式:

  • run.py:主测试入口;
  • run_endurance.py:稳定性测试入口;
  • tools/dump_controls.py:控件探针。

已经在管理员终端里时,用-noelevate跳过重复提权。

为什么必须提权

DesktopApp.exe 自身以 runas 启动。Windows 的 UIPI(用户界面特权隔离)规定:低权限进程不能访问高权限进程的窗口。

真实表现:

  • ProcessNotFoundError:枚举不到窗口;
  • connect 后 PID 瞬间消失;
  • descendants 访问报 AccessDenied。

这不是“偶发问题”,而是确定性行为。所以它必须成为入口的前置条件,而不是用例里的兜底逻辑。

2. 进程连接不能信 PID

桌面应用的启动链条是:

普通进程 → runas 提权 spawn 中转进程 → 真正承载窗口的进程

如果只用“最大 PID”或“第一个 DesktopApp.exe”去 connect,很容易连到:

  • launcher 中转进程:连上后没有窗口;
  • explorer.exe:打开了应用安装目录,窗口标题含应用名;
  • 浏览器进程:Allure 报告页面标题含应用名,class 是Chrome_WidgetWin_1;
  • Qt 辅助窗口:ToolSaveBits、Tooltip、MessageWnd。

正确做法:窗口反查 PID

# 核心思路(简化示意,真实实现见 utils/app_session.py)Application(backend="uia").start(cmd_line=...,work_dir=...)# 候选 PID 不是从进程列表猜,是从 EnumWindows 反查窗口句柄得到forpidincandidate_pids_from_enumwindows():try:app=Application(backend="uia").connect(process=pid)# _has_app_window:校验该 PID 的窗口 class/title/auto_id 是否命中应用特征集if_has_app_window(app):returnappexceptRuntimeError:continue

关键细节:

  1. EnumWindows 反查 PID:从窗口句柄反查进程,而不是从进程列表猜窗口;
  2. 排除系统窗口:CabinetWClass、Shell_TrayWnd、Progman、#32770等;
  3. 排除 Qt 辅助窗口:ToolSaveBits、Tooltip、MessageWnd;
  4. 排除浏览器窗口:Chrome_WidgetWin_1、Edge_WidgetWin_1;
  5. connect 后立即校验:如果该 PID 不承载桌面应用窗口,抛错换下一个。

这一步的验收标准不是“能连上”,而是“打开资源管理器、打开浏览器、启动应用后,仍然只连到正确进程”。

3. 三种进程模式

模式触发方式适用场景
会话共享默认常规场景法,整个会话只启动一次
每用例重启RESTART_PER_TEST=1需要强隔离的用例
连接外部进程APP_PID=xxxx外部调试/注入场景,close 不杀进程

APP_PID模式有两个硬约束:

  • close()只断开引用,不杀进程;
  • 禁止restart(),否则会杀掉用户手动启动的实例。

这两个约束看起来很小,但它们决定了一个调试工具能不能真正被使用。

4. 提权后的日志可见性

提权后run.py跑在独立控制台,父进程(IDE / Trae)拿不到 stdout。解法是_Tee:

class_Tee:"""同时写控制台和日志文件,每次 write 立即 flush。"""def__init__(self,*streams):self.streams=streamsdefwrite(self,text):forsinself.streams:s.write(text)s.flush()defflush(self):forsinself.streams:s.flush()

效果:外部可以轮询logs/YYYYMMDD/run_HHMMSS.log实时看进度。

5. 关键文件

职责文件
提权utils/elevation.py
应用生命周期utils/app_session.py
主入口run.py
稳定性入口run_endurance.py

6. 本步的坑

现象根因修正
pytest 枚举不到窗口测试进程低权限入口统一提权
connect 到 explorer / 浏览器 / launcher只按 PID 或标题猜进程窗口反查 PID + 承载校验
提权后看不到输出stdout 在独立控制台Tee 到日志文件
外部实例被测试杀掉close()无条件杀进程APP_PID模式只断开,不杀

7. 验收清单

  • python run.py能自动弹 UAC 并继续执行;
  • 日志文件能看到每一步进度;
  • 启动后能稳定找到承载窗口的 PID;
  • 故意打开资源管理器/浏览器,不会连错进程;
  • APP_PID模式下关闭测试不会杀掉外部实例。

8. 可复用要点

  • PID 不可信,窗口才是事实:多层 launcher / 提权启动的应用都适用。
  • 入口层只做入口的事:提权、清理、跑 pytest、出报告,不掺业务逻辑。
  • 提权会改变 I/O 边界:所有依赖 stdout 的设计都要重新验证。

关键代码示例

示例 1:统一提权入口(真实utils/elevation.py节选,12 行接入代码)

# run.py 的 3 行提权逻辑ifnotis_admin():# 把原始命令行参数透传给提权后的新进程sys.exit(elevate(sys.argv[1:]))# elevation.py 里的 elevate() 核心(ShellExecuteW runas)defelevate(args=None,window_state=1):script=Path(sys.argv[0]).resolve()cmd=f'"{script}"{subprocess.list2cmdline(args)}'rc=ctypes.windll.shell32.ShellExecuteW(None,"runas",sys.executable,cmd,str(script.parent),window_state,)return0ifrc>32else1

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询