Files
vibe-mote/elevate_setup.py

436 lines
20 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 -*-
"""一次性优化(需要管理员,由主程序用 UAC 拉起 —— **只会弹这一次**)。
干三件事:
1. **清掉 Frida 的历史残留**。Frida 每次挂载不属于自己的进程时,都会尝试注册
一个临时服务 `frida-<pid>-x86/x86_64` 来注入;注册服务需要管理员,于是每
次尝试都会弹一次 UAC;没批准或中途中断就留下一条死服务。残留会随重启不断累积,
攒到几十条服务 + 一堆 frida-helper 进程都很常见。
2. **装一个「最高权限 + 登录时触发」的计划任务来做开机自启**。
为什么不用更简单的 HKCU Run 项:普通权限启动时,Frida 装它的助手要提权,
于是**每次开机都要弹一次 UAC**。改成最高权限的计划任务后,开机由任务计划
程序以管理员身份静默启动 —— Frida 要的权限都是现成的,**再也不会弹**。
3. 删掉 HKCU Run 里的普通权限自启项,避免两份同时启动抢遥控器的蓝牙通道。
过程写进同目录的 elevate_setup.log,非提权的主程序会读它并显示给你看。
"""
from __future__ import annotations
import os
import subprocess
import sys
import time
import winreg
import xml.sax.saxutils as sx
HERE = os.path.dirname(os.path.abspath(__file__))
LOG = os.path.join(HERE, "elevate_setup.log")
XML = os.path.join(HERE, "_task.xml")
TASK_NAME = "VibeMote"
VBS = os.path.join(HERE, "启动遥控器.vbs")
RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
CREATE_NO_WINDOW = 0x08000000
def log(msg):
line = f"[{time.strftime('%H:%M:%S')}] {msg}"
print(line, flush=True)
try:
with open(LOG, "a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception:
pass
def is_admin():
try:
import ctypes
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except Exception:
return False
def _dec(b: bytes) -> str:
"""子进程输出编码不能写死:schtasks/sc 吐 GBK,python 子进程吐 UTF-8。
写死一个就会把中文变成「涓嶅彲鐢」那种乱码。"""
for enc in ("utf-8", "gbk", "mbcs"):
try:
return b.decode(enc)
except (UnicodeDecodeError, LookupError):
continue
return b.decode("utf-8", "replace")
def run(cmd, timeout=60):
try:
p = subprocess.run(cmd, capture_output=True, timeout=timeout,
creationflags=CREATE_NO_WINDOW)
return p.returncode, _dec(p.stdout or b"") + _dec(p.stderr or b"")
except Exception as e:
return -1, repr(e)
# ---------------------------------------------------------------- 1. 清残留
def cleanup_frida():
total_killed = 0
for exe in ("frida-helper-x86.exe", "frida-helper-x86_64.exe"):
rc, out = run(["taskkill", "/IM", exe, "/F"])
killed = out.lower().count("success") + out.count("成功")
total_killed += killed
if total_killed:
log(f"已结束 {total_killed} 个 frida-helper 进程")
else:
log("没有 frida-helper 进程在跑")
names = []
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Services") as k:
i = 0
while True:
try:
names.append(winreg.EnumKey(k, i))
except OSError:
break
i += 1
except OSError as e:
log(f"读服务列表失败:{e}")
return
targets = [n for n in names if n.lower().startswith("frida-")]
log(f"发现 {len(targets)} 条 frida-* 残留服务,开始删除…")
ok = 0
for n in targets:
rc, _ = run(["sc.exe", "delete", n])
if rc == 0:
ok += 1
log(f"已删除 {ok}/{len(targets)} 条")
# ---------------------------------------------------------------- 2. 装任务
def task_xml(run_level="HighestAvailable"):
user = os.environ.get("USERDOMAIN", "") + "\\" + os.environ.get("USERNAME", "")
# ★ 必须带 `--boot`:它标记"这一份是开机自启拉起来的",
# 程序靠它关掉"首次启动自动拉起虚拟声卡安装器"那条路(见 app.py 里 auto_ok 的注释)。
# 漏了的话,开机时如果声卡不在了就会在开机那一刻弹安装器 + UAC —— 那正是
# 用户最不想要的"开机弹窗"。
# ★ 另外给登录触发器加了**每 5 分钟重复一次**(见下面 XML):这台机器上真出现过
# "客户端不知什么时候没了、开机自启也没把它拉回来"(任务只在登录那一刻跑一次)。
# 有了重复,进程若异常退出,最多 5 分钟就会自己回来;已经在跑时新进程看到端口在听
# 会立刻退出,等于什么都没做(不弹窗、不刷日志)。
args = sx.escape(f'"{VBS}" --silent --boot')
return f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>VibeMote - Google TV remote bridge (voice key + key mapping)</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<Delay>PT8S</Delay>
<Repetition>
<Interval>PT5M</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
</LogonTrigger>
<BootTrigger>
<Enabled>false</Enabled>
</BootTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>{sx.escape(user)}</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>{run_level}</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>3</Count>
</RestartOnFailure>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>wscript.exe</Command>
<Arguments>{args}</Arguments>
</Exec>
</Actions>
</Task>
"""
def looks_like_dev_copy():
"""这个目录是不是**开发副本**(源码仓库)而不是用户装好的那一份。
为什么需要(真机踩过,很坑):开发副本被跑起来、并且在**它的**界面上点了「一键优化」,
于是**计划任务被改成指向开发目录** —— 从此开机启动的是开发副本(配置还不一样),
用户看到的是"程序怎么又不对了 / 收音又有问题"。仓库里有这些开发脚本、用户那边没有,
拿它们当指纹最省事。
"""
for name in ("_package.py", "_build_offline.py", "_verify_zips_match_head.py"):
if os.path.exists(os.path.join(HERE, name)):
return True
return False
def install_task(run_level="HighestAvailable", name=TASK_NAME):
# ★ 守卫:开发目录不许改写/安装自启任务(否则会把用户那份的开机启动顶掉)。
# 确实需要在本机开发目录测试时,设环境变量 VIBEMOTE_ALLOW_DEV_TASK=1 绕过。
if looks_like_dev_copy() and os.environ.get("VIBEMOTE_ALLOW_DEV_TASK") != "1":
log("✗ 这个目录看起来是**开发副本**(里面有 _package.py 等开发脚本)——")
log(" 因此不动计划任务,免得把用户那份的开机启动顶掉。")
log(" 真要在开发目录里装任务:设环境变量 VIBEMOTE_ALLOW_DEV_TASK=1 再跑。")
return False
# 任务定义用 UTF-16 写,schtasks 读 XML 时按声明解码,中文路径不会乱
with open(XML, "w", encoding="utf-16") as f:
f.write(task_xml(run_level))
rc, out = run(["schtasks", "/create", "/tn", name, "/xml", XML, "/f"])
head = (out or "").strip().splitlines()
log(f"计划任务:返回码 {rc} {' / '.join(head[:2]) if head else ''}")
try:
os.remove(XML)
except OSError:
pass
return rc == 0
# ---------------------------------------------------------------- 3. 清 Run 项
def remove_run_entry():
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY, 0,
winreg.KEY_SET_VALUE) as k:
try:
winreg.DeleteValue(k, TASK_NAME)
log("已删除 HKCU Run 里的自启项(改用计划任务,避免两份同时启动)")
except FileNotFoundError:
log("HKCU Run 里本来就没有自启项")
except OSError as e:
log(f"处理 Run 项失败(不影响计划任务):{e}")
# ------------------------------------------------------- 4. 用新代码重启客户端
UI_PORT = 8787
def port_owners(port):
"""谁正在监听这个端口(解析 netstat,不依赖任何第三方库)。"""
rc, out = run(["netstat", "-ano"])
pids = []
for line in out.splitlines():
parts = line.split()
if len(parts) >= 5 and parts[1].endswith(f":{port}") \
and parts[3].upper() == "LISTENING":
try:
pids.append(int(parts[4]))
except ValueError:
pass
return sorted(set(pids))
def restart_client():
"""结束旧客户端,再用计划任务拉起当前目录里的新代码。
为什么必须由管理员进程来做:客户端自己是最高权限跑的,**普通权限杀不掉它**
(Stop-Process / taskkill 在普通权限下都是 Access is denied)。所以「换了代码怎么
生效」这件事,正好借这一次提权一起办掉;之后想重启,用界面「状态 → 重启」
即可 —— 应用内的 restart-app 动作会以自身权限拉起新实例并接管,不再需要 UAC。
"""
old = port_owners(UI_PORT)
if not old:
log("没有正在运行的客户端(跳过重启)")
for pid in old:
log(f"结束旧客户端 PID {pid}")
run(["taskkill", "/PID", str(pid), "/T", "/F"])
for _ in range(40):
if not port_owners(UI_PORT):
break
time.sleep(0.25)
rc, out = run(["schtasks", "/run", "/tn", TASK_NAME])
log(f"用计划任务重新启动客户端:返回码 {rc}")
for _ in range(40):
if port_owners(UI_PORT):
log(f"客户端已起来(PID {port_owners(UI_PORT)}),界面 http://127.0.0.1:{UI_PORT}/")
return True
time.sleep(0.5)
log("客户端没在 20 秒内起来,可双击「启动遥控器.vbs」手动拉起")
return False
def _ps(cmd, timeout=120):
rc, out = run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", cmd],
timeout=timeout)
for line in (out or "").splitlines():
if line.strip():
log(" " + line.strip())
return rc, out or ""
def reset_bluetooth():
"""禁用再启用蓝牙电台 —— 拆掉"被系统 HID 攥住"的那条 BLE 连接。
BLE 外设只接受**一个**连接。Windows 的 HID 栈有时会把那条唯一的连接攥住不放,
遥控器因此不再广播,而程序要自己建一条 GATT 连接才能收语音 → "永远连不上"
(表现:日志一直刷连接超时,但按键还有用)。拔电池/关开蓝牙开关能解开,
这个动作等于"用代码关开一次蓝牙电台"。
⚠⚠ 这里踩过一个**很严重**的坑:原先禁用+启用写在同一条 PowerShell 里、都带
`-ErrorAction Stop` —— 只要"启用"那一步抛错(或者提权进程在中途被杀),
电台就**留在禁用状态**,用户看到的是「**我的蓝牙设备全没了**」。
所以现在:① 禁用、启用分成两次独立调用,各自成败互不影响;
② 启用**重试 3 次**;③ 最后**验证**电台状态,没回来就明确告诉他去哪儿手动启用。
"""
# 蓝牙电台在 USB 上(如 Intel USB\VID_8087&PID_0033);BTHLE\ 那些是设备,不能停。
# 注意:禁用后 Status 会变成 Error/Unknown,所以"启用"这一步不能再用 Status -eq 'OK' 过滤。
pick = ("$d = @(Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | "
"Where-Object { $_.InstanceId -like 'USB*' });"
"if ($d.Count -eq 0) { Write-Output 'NO-RADIO'; exit 3 };"
"$d | ForEach-Object { Write-Output ('RADIO ' + $_.InstanceId + ' ' + $_.FriendlyName) }")
rc, out = _ps(pick)
if rc != 0 or "NO-RADIO" in out:
log("✗ 没找到 USB 上的蓝牙电台(可能是别的方式接的,或驱动异常)—— 什么都没动。")
return False
if "RADIO " not in out:
log("✗ 没能列出蓝牙电台 —— 什么都没动。")
return False
insts = []
for line in out.splitlines():
parts = line.strip().split(None, 2)
if len(parts) >= 2 and parts[0] == "RADIO":
insts.append(parts[1])
# ★ 第 0 步:**先把被禁用的电台启用回来**。
# 真机上发生过:用户按提示"手动关开一次蓝牙开关"只关了没开回来(或者设备管理器里
# 被禁用),结果 Windows 设置里的蓝牙开关**整个消失**、所有蓝牙设备都不见了 ——
# 用户以为设备坏了。这一步就是那个状态的对症解药(而且是幂等的:正常时不做事)。
en0 = ("Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | "
"Where-Object { $_.InstanceId -like 'USB*' -and $_.Status -ne 'OK' } | ForEach-Object {"
" Write-Output ('发现非正常状态的电台:' + $_.Status + ' / ' + $_.FriendlyName);"
" try { Enable-PnpDevice -InstanceId $_.InstanceId -Confirm:$false -ErrorAction Stop;"
" Write-Output ' Enable-PnpDevice 成功' }"
" catch { Write-Output (' Enable-PnpDevice 失败: ' + $_.Exception.Message) } }")
_ps(en0)
for inst in insts:
# pnputil 的 enable 更兼容(真机上 Enable-PnpDevice 也会"不支持")
_ps(f'pnputil /enable-device "{inst}"')
time.sleep(2)
# ★★ 安全闸门:如果一开始就是"非正常状态"(被禁用/Error),那么**只要启用成功就够了**,
# 绝不再去"关开一次" —— 真机上这一步曾经把电台留在禁用状态,用户以为设备坏了。
# 确认变好就收手,不做任何多余动作。
rc_ok, chk0 = _ps("$d = @(Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | "
"Where-Object { $_.InstanceId -like 'USB*' -and $_.Status -eq 'OK' });"
"if ($d.Count -gt 0) { Write-Output 'RADIO-OK' } else { Write-Output 'RADIO-DOWN' }")
if "RADIO-DOWN" in out or "Error" in out or "Unknown" in out or "Disabled" in out:
if "RADIO-OK" in chk0:
log("✓ 电台已被启用并正常工作(原来是禁用/异常状态,所以只做了启用,没有再关开)")
return "enabled"
log("… 启用后状态仍不对,继续尝试重启设备")
# ★ 第 1 步:用 `pnputil /restart-device` 做"关开一次"。
# 它是"重启设备",**不存在"禁用后没启用回来"的窗口**,而且比
# Disable/Enable-PnpDevice 兼容(真机上 Disable-PnpDevice 直接报"不支持"
# HRESULT 0x8004100c,那条路根本走不通)。重启失败再退回禁用+启用。
for inst in insts:
log(f"用 pnputil 重启设备:{inst}")
rc3, o3 = _ps(f'pnputil /restart-device "{inst}"')
if rc3 == 0:
log("✓ 设备已重启(这条路径不会把电台留在禁用状态)")
return "restarted"
log(" pnputil 这条路没成功,改用『禁用 + 启用』")
dis = ("Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | "
"Where-Object { $_.InstanceId -like 'USB*' } | ForEach-Object {"
" Write-Output ('禁用 ' + $_.FriendlyName);"
" try { Disable-PnpDevice -InstanceId $_.InstanceId -Confirm:$false -ErrorAction Stop }"
" catch { Write-Output ('禁用失败(继续): ' + $_.Exception.Message) } }")
_ps(dis)
time.sleep(3)
# ★ 启用是"必须成功"的那一步:独立调用 + 重试
for attempt in range(1, 4):
en = ("Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | "
"Where-Object { $_.InstanceId -like 'USB*' } | ForEach-Object {"
" Write-Output ('启用 ' + $_.FriendlyName);"
" try { Enable-PnpDevice -InstanceId $_.InstanceId -Confirm:$false -ErrorAction Stop }"
" catch { Write-Output ('启用失败: ' + $_.Exception.Message) } }")
_ps(en)
time.sleep(3)
rc2, chk = _ps("$d = @(Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | "
"Where-Object { $_.InstanceId -like 'USB*' -and $_.Status -eq 'OK' });"
"if ($d.Count -gt 0) { Write-Output 'RADIO-OK' } else { Write-Output 'RADIO-DOWN' }")
if "RADIO-OK" in chk:
log(f"✓ 蓝牙电台已恢复(第 {attempt} 次确认)")
return True
log(f"… 电台还没回来(第 {attempt} 次),再试一次启用")
log("✗ 蓝牙电台没能自己回来。**别慌,设备还在**,手动两步就能救回:")
log(" ① 设备管理器(Win+X → 设备管理器)→ 蓝牙 → 右键那个适配器 → **启用设备**")
log(" ② 还不行就**完全关机**(开始菜单点「关机」,不是「重启」)再开机 —— ")
log(" 快速启动有时会让蓝牙设备开机后不枚举,完整关机一次必回来。")
return False
def main():
# 只重置蓝牙电台(不动自启/任务):给界面上那个「重置蓝牙电台」按钮用
if "--reset-bt" in sys.argv:
try:
open(LOG, "w", encoding="utf-8").close()
except Exception:
pass
log("=== 重置蓝牙电台 ===")
if not is_admin():
log("✗ 当前不是管理员,什么都没做。请从界面点按钮并在 UAC 里点「是」。")
log("DONE")
return 1
log("管理员权限 ✓(期间蓝牙设备会短暂断开,几秒后自动恢复)")
ok = reset_bluetooth()
if ok == "restarted":
log("✓ 蓝牙电台已重启,等待遥控器自动重连…")
elif ok == "enabled":
log("⚠ 这台机器**不支持**用软件重启蓝牙电台(Windows 报「不支持 / 需要重启系统才能完成上一次操作」)——")
log(" 我已经确保它是**启用**状态;要真正「关开一次」请二选一:")
log(" ① 把遥控器**电池取出 10 秒**再装回(推荐,最快)")
log(" ② **重启电脑**(或完全关机再开机)—— 系统里还有一次待完成的蓝牙配置操作,重启后才干净")
else:
log("✗ 重置失败 —— 手动两步:① 设备管理器 → 蓝牙 → 右键适配器 → 「启用设备」;② 完全关机再开机")
log("DONE")
return 0 if ok else 1
try:
open(LOG, "w", encoding="utf-8").close()
except Exception:
pass
log("=== 一次性优化开始 ===")
if not is_admin():
log("✗ 当前不是管理员,什么都没做。请从界面点「一键优化」并在 UAC 里点「是」。")
log("DONE")
return 1
log("管理员权限 ✓")
cleanup_frida()
if install_task():
remove_run_entry()
log("✓ 开机自启已改为「最高权限计划任务」:以后开机静默以管理员身份启动,")
log(" Frida 需要的权限现成,**不会再弹任何 UAC**。")
else:
log("✗ 计划任务没建成。可以只做第一步(清残留)也不影响使用;")
log(" 自启仍走 HKCU Run(那种方式每次开机 Frida 可能要你授权一次)。")
restart_client()
log("DONE")
return 0
if __name__ == "__main__":
sys.exit(main())