mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
手表端 - 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源) - 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、 剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等 - 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘 - 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮 滚轮点按=中键、上下滑=滚动 - 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、 点画面播放暂停、上一/下一、播完自动下一首 电脑端 - 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身 - 新增 media_list / media_pull(播放器数据源) 安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过 中转站 - known_devices 从 ID 集合改成「设备ID -> 配对码」映射 - 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉 - 老格式 state 文件平滑升级
766 lines
31 KiB
Python
766 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''ykxmz 诊断工具 —— 在出问题的机器上跑一遍,把所有可疑点问清楚。
|
||
|
||
== 为什么做这个 ==
|
||
|
||
用户反馈「Win7 上语音播报不能用」。但远程猜不出来 —— 可能是:
|
||
|
||
* 没装任何 TTS 语音(精简版系统常见)
|
||
* 没有音频设备 / 音频服务没启动(SAPI 会**静默失败**,退出码还是 0)
|
||
* cscript / powershell 被删了
|
||
* 权限问题(写不了临时文件)
|
||
* 或者是别的完全没想到的原因
|
||
|
||
**猜来猜去不如让它自己说。** 这个程序把每一项都试一遍,
|
||
输出一段文本,用户复制回来,我照着改。
|
||
|
||
== 设计原则 ==
|
||
|
||
1. **绝不改动机器上的任何东西** —— 只读 + 在 %TEMP% 里写临时文件(跑完删掉)
|
||
2. **每一项都要有明确结论**(可用/不可用 + 为什么),不是只打印一堆原始数据
|
||
3. **每一项都单独 try** —— 某一项崩了不能影响后面的
|
||
4. **输出要能复制** —— 同时打印到屏幕 + 写到桌面上的 .txt
|
||
5. **32 位 + Python 3.8** —— 和主程序一样,保证 Win7 能跑
|
||
|
||
== 探测什么 ==
|
||
|
||
一、系统环境 版本/位数/代码页/管理员/临时目录可写
|
||
二、语音能力 ★ 重点:5 种方案逐个试 + 语音列表 + 音频设备 + 音频服务
|
||
三、外部程序 cscript / wscript / powershell / cmd 在不在、什么版本
|
||
四、.NET 环境 System.Speech 依赖它
|
||
五、屏幕和输入 分辨率 / DPI / 能否截屏 / 能否注入鼠标
|
||
六、网络 能不能连上中转站
|
||
七、主程序状态 装在哪、数据目录、版本、有没有在跑
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import ctypes
|
||
import datetime
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
IS_WINDOWS = os.name == "nt"
|
||
NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||
|
||
OUT: list = []
|
||
REPORT_PATH = None
|
||
|
||
|
||
# ---------------------------------------------------------------- 输出
|
||
def _console_encode(text: str) -> bytes:
|
||
r'''把要打印的文字编码成**控制台能正确显示**的字节。
|
||
|
||
== 为什么不用 UTF-8 ==
|
||
|
||
第一版我强行 `chcp 65001` + `sys.stdout.reconfigure(encoding="utf-8")`,
|
||
结果在 Win7 上**满屏问号**。
|
||
|
||
原因有两层:
|
||
1. Win7 的 cmd 即使切到 65001,**默认点阵字体也不含中文字形**
|
||
-> 显示成方块或问号
|
||
2. 用户看到的 `???` 是字体层的问题,不是编码层的
|
||
|
||
**最稳的做法**:按**系统 ANSI 代码页**(中文 Windows = GBK/936)输出。
|
||
这正是 cmd 默认期望的编码,配合默认字体一定能显示。
|
||
'''
|
||
for enc in ("mbcs", "gbk", "utf-8"):
|
||
try:
|
||
return text.encode(enc, errors="replace")
|
||
except Exception: # noqa: BLE001
|
||
continue
|
||
return text.encode("ascii", errors="replace")
|
||
|
||
|
||
def say(line: str = "") -> None:
|
||
"""同时打印到屏幕(本机代码页)和报告(UTF-8)。"""
|
||
try:
|
||
sys.stdout.buffer.write(_console_encode(line) + b"\r\n")
|
||
sys.stdout.buffer.flush()
|
||
except Exception: # noqa: BLE001
|
||
try:
|
||
print(line, flush=True)
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
OUT.append(line)
|
||
|
||
|
||
def head(title: str) -> None:
|
||
say()
|
||
say("=" * 64)
|
||
say(" " + title)
|
||
say("=" * 64)
|
||
|
||
|
||
def item(name: str, ok, detail: str = "") -> None:
|
||
"""一行结论。ok 可以是 True/False/None(None = 只是信息)。"""
|
||
if ok is True:
|
||
mark = "[ 可用 ]"
|
||
elif ok is False:
|
||
mark = "[不可用]"
|
||
else:
|
||
mark = "[ 信息 ]"
|
||
say(" %s %-34s %s" % (mark, name, detail))
|
||
|
||
|
||
def safe(fn, *a, **kw):
|
||
"""跑一个探测,出错不让它影响后面的。"""
|
||
try:
|
||
return fn(*a, **kw)
|
||
except Exception as exc: # noqa: BLE001
|
||
return "【探测出错】%s: %s" % (type(exc).__name__, exc)
|
||
|
||
|
||
def run(cmd, timeout=30, encoding=None):
|
||
"""跑命令,返回 (退出码, 输出)。异常都吃掉,转成文字。"""
|
||
try:
|
||
p = subprocess.run(cmd, capture_output=True, timeout=timeout,
|
||
creationflags=NO_WINDOW,
|
||
encoding=encoding or "mbcs", errors="replace")
|
||
out = ((p.stdout or "") + (p.stderr or "")).strip()
|
||
return p.returncode, out
|
||
except FileNotFoundError:
|
||
return -1, "(找不到这个程序)"
|
||
except subprocess.TimeoutExpired:
|
||
return -2, "(超时 %d 秒)" % timeout
|
||
except Exception as exc: # noqa: BLE001
|
||
return -3, "%s: %s" % (type(exc).__name__, exc)
|
||
|
||
|
||
# ================================================================ 一、系统
|
||
def probe_system() -> None:
|
||
head("一、系统环境")
|
||
|
||
try:
|
||
v = sys.getwindowsversion()
|
||
item("Windows 版本", True, "%d.%d (build %d)" % (v.major, v.minor, v.build))
|
||
# Win7 = 6.1
|
||
if (v.major, v.minor) == (6, 1):
|
||
say(" ^ 这是 Windows 7")
|
||
elif (v.major, v.minor) == (6, 0):
|
||
say(" ^ 这是 Windows Vista")
|
||
except Exception: # noqa: BLE001
|
||
item("Windows 版本", None, "取不到")
|
||
|
||
item("Python 位数", True, "%d 位" % (64 if sys.maxsize > 2 ** 32 else 32))
|
||
item("Python 版本", True, sys.version.split()[0])
|
||
item("进程是否 64 位", True,
|
||
"是" if ctypes.sizeof(ctypes.c_void_p) == 8 else "否(32 位进程)")
|
||
|
||
# 管理员?
|
||
try:
|
||
admin = bool(ctypes.windll.shell32.IsUserAnAdmin())
|
||
except Exception: # noqa: BLE001
|
||
admin = None
|
||
item("管理员权限", admin, "有" if admin else "没有(普通用户)")
|
||
|
||
# 代码页 —— 中文乱码问题跟它有关
|
||
try:
|
||
cp = ctypes.windll.kernel32.GetACP()
|
||
item("系统 ANSI 代码页", True, "%d(%s)" % (cp, "中文 GBK" if cp == 936 else "其它"))
|
||
except Exception: # noqa: BLE001
|
||
item("系统 ANSI 代码页", None, "取不到")
|
||
|
||
# 临时目录能不能写
|
||
try:
|
||
t = Path(tempfile.gettempdir())
|
||
p = t / "_ykxmz_diag_test.txt"
|
||
p.write_text("ok", encoding="utf-8")
|
||
p.unlink()
|
||
item("临时目录可写", True, str(t))
|
||
except Exception as exc: # noqa: BLE001
|
||
item("临时目录可写", False, "%s: %s" % (type(exc).__name__, exc))
|
||
|
||
item("程序自身路径", None, sys.executable)
|
||
item("当前工作目录", None, os.getcwd())
|
||
|
||
|
||
# ================================================================ 二、语音
|
||
VBS_TEMPLATE = (
|
||
'Dim v\r\n'
|
||
'Set v = CreateObject("SAPI.SpVoice")\r\n'
|
||
'v.Speak "{text}"\r\n'
|
||
)
|
||
|
||
|
||
def probe_tts() -> None:
|
||
head("二、语音播报能力(重点)")
|
||
|
||
TEXT = "语音测试一二三"
|
||
|
||
# ---- 2.1 外部程序在不在 ----
|
||
say()
|
||
say(" --- 2.1 依赖的外部程序 ---")
|
||
sysroot = os.environ.get("SystemRoot", r"C:\Windows")
|
||
for name in ("cscript", "wscript", "powershell", "cmd"):
|
||
found = shutil.which(name)
|
||
# 再按绝对路径找一遍(PATH 里没有时)
|
||
abs_guess = {
|
||
"cscript": sysroot + r"\System32\cscript.exe",
|
||
"wscript": sysroot + r"\System32\wscript.exe",
|
||
"powershell": sysroot + r"\System32\WindowsPowerShell\v1.0\powershell.exe",
|
||
"cmd": sysroot + r"\System32\cmd.exe",
|
||
}.get(name, "")
|
||
abs_ok = bool(abs_guess) and Path(abs_guess).is_file()
|
||
if found:
|
||
item(name, True, "PATH 里有: %s" % found)
|
||
elif abs_ok:
|
||
item(name, True, "PATH 里没有,但绝对路径有: %s" % abs_guess)
|
||
else:
|
||
item(name, False, "PATH 和绝对路径都找不到")
|
||
|
||
# powershell 版本
|
||
ps = shutil.which("powershell") or (
|
||
sysroot + r"\System32\WindowsPowerShell\v1.0\powershell.exe")
|
||
if Path(ps).is_file():
|
||
code, out = run([ps, "-NoProfile", "-Command",
|
||
"$PSVersionTable.PSVersion.Major"], timeout=20)
|
||
item("PowerShell 主版本", code == 0, out.strip() or "查不到")
|
||
code, out = run([ps, "-NoProfile", "-Command",
|
||
"[System.Environment]::Version.ToString()"], timeout=20)
|
||
item(".NET CLR 版本", code == 0, out.strip() or "查不到")
|
||
|
||
# ---- 2.2 音频设备和音频服务 ----
|
||
say()
|
||
say(" --- 2.2 音频环境(SAPI 没声音的头号嫌疑)---")
|
||
code, out = run(["sc", "query", "Audiosrv"], timeout=20)
|
||
running = "RUNNING" in (out or "").upper()
|
||
item("音频服务 Audiosrv", running,
|
||
"正在运行" if running else "没在运行(没声音多半是它)")
|
||
|
||
# ⚠ 第一版这里用了 `Get-CimInstance` —— 那是 **PowerShell 3.0+** 的 cmdlet。
|
||
# Win7 自带 **PowerShell 2.0**,直接报
|
||
# "The term 'Get-CimInstance' is not recognized"。
|
||
# 改用 **`Get-WmiObject`**(PS 2.0 就有,3.0+ 也还在)。
|
||
say(" (以下用 Get-WmiObject / wmic,兼容 PowerShell 2.0)")
|
||
if Path(ps).is_file():
|
||
code, out = run([
|
||
ps, "-NoProfile", "-Command",
|
||
"Get-WmiObject Win32_SoundDevice | "
|
||
"ForEach-Object { $_.Name + ' | ' + $_.Status }"], timeout=30)
|
||
lines = [x.strip() for x in (out or "").splitlines() if x.strip()]
|
||
bad = [x for x in lines
|
||
if "not recognized" in x or "CategoryInfo" in x
|
||
or "At line" in x or "FullyQualified" in x]
|
||
if bad:
|
||
item("音频输出设备(WMI)", False, bad[0][:90])
|
||
elif lines:
|
||
item("音频输出设备(WMI)", True, "共 %d 个" % len(lines))
|
||
for x in lines[:5]:
|
||
say(" - " + x)
|
||
else:
|
||
item("音频输出设备(WMI)", False,
|
||
"**一个都没有** —— 虚拟机常见(没加声卡),SAPI 会静默失败")
|
||
|
||
# 再用 wmic 独立验一遍(完全不依赖 PowerShell)
|
||
code, out = run(["wmic", "sounddev", "get", "Name,Status"],
|
||
timeout=30, encoding="mbcs")
|
||
if code == 0:
|
||
rows = [x.strip() for x in (out or "").splitlines()
|
||
if x.strip() and x.strip().lower() not in ("name status",
|
||
"name", "status")]
|
||
item("音频输出设备(wmic)", bool(rows),
|
||
("共 %d 个: %s" % (len(rows), " / ".join(rows[:3]))) if rows
|
||
else "**一个都没有**")
|
||
|
||
# SAPI 自己怎么看音频输出
|
||
if Path(ps).is_file():
|
||
code, out = run([
|
||
ps, "-NoProfile", "-Command",
|
||
"$v = New-Object -ComObject SAPI.SpVoice;"
|
||
"try { $v.AudioOutput.GetDescription() } "
|
||
"catch { 'ERR: ' + $_.Exception.Message }"], timeout=30)
|
||
txt = (out or "").strip()
|
||
item("SAPI 默认音频输出", code == 0 and not txt.startswith("ERR"),
|
||
txt[:100] or "拿不到")
|
||
|
||
# ---- 2.3 SAPI 语音列表 ----
|
||
say()
|
||
say(" --- 2.3 装了哪些语音 ---")
|
||
voices_ps = []
|
||
if Path(ps).is_file():
|
||
code, out = run([
|
||
ps, "-NoProfile", "-Command",
|
||
"$v = New-Object -ComObject SAPI.SpVoice;"
|
||
"$v.GetVoices() | ForEach-Object { $_.GetDescription() }"],
|
||
timeout=25)
|
||
voices_ps = [x.strip() for x in (out or "").splitlines() if x.strip()]
|
||
if code == 0 and voices_ps:
|
||
item("SAPI5 语音(COM 方式)", True, "共 %d 个" % len(voices_ps))
|
||
for v in voices_ps[:10]:
|
||
say(" - " + v)
|
||
else:
|
||
item("SAPI5 语音(COM 方式)", False,
|
||
"列不出来 (退出码 %s) %s" % (code, out[:80]))
|
||
|
||
# System.Speech 那条路
|
||
if Path(ps).is_file():
|
||
code, out = run([
|
||
ps, "-NoProfile", "-Command",
|
||
"Add-Type -AssemblyName System.Speech;"
|
||
"(New-Object System.Speech.Synthesis.SpeechSynthesizer)"
|
||
".GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }"],
|
||
timeout=30)
|
||
names = [x.strip() for x in (out or "").splitlines() if x.strip()]
|
||
item("System.Speech 语音", code == 0 and bool(names),
|
||
"共 %d 个: %s" % (len(names), "、".join(names[:5]))
|
||
if names else "列不出来 (退出码 %s) %s" % (code, (out or "")[:80]))
|
||
|
||
# ---- 2.4 五种方案逐个试 ----
|
||
say()
|
||
say(" --- 2.4 五种发声方案逐个试 ---")
|
||
say(" (注意:能出声才算数。这里只能看退出码 + 耗时)")
|
||
|
||
vbs = Path(tempfile.gettempdir()) / "_ykxmz_diag_say.vbs"
|
||
try:
|
||
vbs.write_bytes(
|
||
(VBS_TEMPLATE.replace("{text}", TEXT)).encode("mbcs",
|
||
errors="replace"))
|
||
vbs_ok = True
|
||
item("写 VBS 文件", True, "%s (%d 字节)" % (vbs, vbs.stat().st_size))
|
||
except Exception as exc: # noqa: BLE001
|
||
vbs_ok = False
|
||
item("写 VBS 文件", False, "%s: %s" % (type(exc).__name__, exc))
|
||
|
||
attempts = []
|
||
if vbs_ok:
|
||
attempts.append(("1. cscript + VBScript",
|
||
["cscript", "//nologo", str(vbs)]))
|
||
attempts.append(("2. wscript + VBScript",
|
||
["wscript", "//nologo", str(vbs)]))
|
||
if Path(ps).is_file():
|
||
attempts.append(("3. powershell + SAPI COM", [
|
||
ps, "-NoProfile", "-WindowStyle", "Hidden", "-Command",
|
||
"$v = New-Object -ComObject SAPI.SpVoice; $v.Speak('%s')" % TEXT]))
|
||
attempts.append(("4. powershell + System.Speech", [
|
||
ps, "-NoProfile", "-WindowStyle", "Hidden", "-Command",
|
||
"Add-Type -AssemblyName System.Speech;"
|
||
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;"
|
||
"$s.Speak('%s')" % TEXT]))
|
||
|
||
for label, cmd in attempts:
|
||
exe = cmd[0]
|
||
# 用绝对路径兜底
|
||
if not shutil.which(exe):
|
||
cand = {
|
||
"cscript": sysroot + r"\System32\cscript.exe",
|
||
"wscript": sysroot + r"\System32\wscript.exe",
|
||
}.get(exe)
|
||
if cand and Path(cand).is_file():
|
||
cmd = [cand] + list(cmd[1:])
|
||
exe = cand
|
||
t0 = time.perf_counter()
|
||
code, out = run(cmd, timeout=60)
|
||
secs = time.perf_counter() - t0
|
||
ok = code == 0
|
||
item(label, ok, "退出码 %s,耗时 %.1fs%s"
|
||
% (code, secs, (" " + out[:70]) if out else ""))
|
||
|
||
# 清理
|
||
try:
|
||
if vbs.exists():
|
||
vbs.unlink()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
say()
|
||
say(" ⚠ 如果上面全部「可用」但实际没声音,问题在音频侧:")
|
||
say(" * 音频服务没运行")
|
||
say(" * 没有音频输出设备(耳机孔也没插)")
|
||
say(" * 或者装了语音但系统音量/静音设置有问题")
|
||
|
||
|
||
# ================================================================ 三、屏幕输入
|
||
def probe_ui() -> None:
|
||
head("三、屏幕和输入")
|
||
|
||
if not IS_WINDOWS:
|
||
say(" 非 Windows,跳过")
|
||
return
|
||
u = ctypes.windll.user32
|
||
|
||
try:
|
||
w = u.GetSystemMetrics(0)
|
||
h = u.GetSystemMetrics(1)
|
||
item("主屏分辨率", True, "%dx%d" % (w, h))
|
||
except Exception as exc: # noqa: BLE001
|
||
item("主屏分辨率", False, str(exc))
|
||
|
||
try:
|
||
vw = u.GetSystemMetrics(78) # SM_CXVIRTUALSCREEN
|
||
vh = u.GetSystemMetrics(79)
|
||
item("虚拟屏尺寸", True, "%dx%d(多显示器会更大)" % (vw, vh))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
# DPI
|
||
try:
|
||
dc = u.GetDC(0)
|
||
dpi = ctypes.windll.gdi32.GetDeviceCaps(dc, 88) # LOGPIXELSX
|
||
u.ReleaseDC(0, dc)
|
||
item("DPI", True, "%d(100%% 是 96)" % dpi)
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
# 截屏能力
|
||
try:
|
||
gdi = ctypes.windll.gdi32
|
||
dc = u.GetDC(0)
|
||
mem = gdi.CreateCompatibleDC(dc)
|
||
bmp = gdi.CreateCompatibleBitmap(dc, 100, 100)
|
||
gdi.SelectObject(mem, bmp)
|
||
ok = bool(gdi.BitBlt(mem, 0, 0, 100, 100, dc, 0, 0, 0x00CC0020))
|
||
gdi.DeleteObject(bmp)
|
||
gdi.DeleteDC(mem)
|
||
u.ReleaseDC(0, dc)
|
||
item("能否截屏(BitBlt)", ok, "成功" if ok else "失败")
|
||
except Exception as exc: # noqa: BLE001
|
||
item("能否截屏(BitBlt)", False, "%s: %s" % (type(exc).__name__, exc))
|
||
|
||
# 输入桌面是不是锁屏
|
||
try:
|
||
h = u.OpenInputDesktop(0, False, 0x0001)
|
||
name = ""
|
||
if h:
|
||
buf = ctypes.create_unicode_buffer(256)
|
||
need = ctypes.c_int(0)
|
||
u.GetUserObjectInformationW(h, 2, buf, 512, ctypes.byref(need))
|
||
name = buf.value
|
||
u.CloseDesktop(h)
|
||
item("当前输入桌面", name != "Winlogon",
|
||
"%s%s" % (name or "(拿不到)",
|
||
"(锁屏中)" if name == "Winlogon" else ""))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
# 能不能注入鼠标(只测 API 在不在,不动鼠标)
|
||
try:
|
||
ctypes.windll.user32.SendInput
|
||
item("SendInput 可用", True, "函数在")
|
||
except Exception as exc: # noqa: BLE001
|
||
item("SendInput 可用", False, str(exc))
|
||
|
||
|
||
# ================================================================ 四、网络
|
||
def probe_net() -> None:
|
||
head("四、网络")
|
||
|
||
import socket
|
||
for host, port in (("yk.xxmxxs.top", 443), ("1.1.1.1", 53)):
|
||
t0 = time.perf_counter()
|
||
try:
|
||
s = socket.create_connection((host, port), timeout=10)
|
||
s.close()
|
||
item("连 %s:%d" % (host, port), True,
|
||
"通,%.2fs" % (time.perf_counter() - t0))
|
||
except Exception as exc: # noqa: BLE001
|
||
item("连 %s:%d" % (host, port), False,
|
||
"%s(%.2fs)" % (type(exc).__name__,
|
||
time.perf_counter() - t0))
|
||
|
||
# 本机 IP
|
||
try:
|
||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
s.connect(("1.1.1.1", 80))
|
||
item("本机局域网 IP", True, s.getsockname()[0])
|
||
s.close()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
|
||
# ============================================================ 五、环境与工具链
|
||
def probe_env() -> None:
|
||
r'''采集**环境与工具链**信息 —— 这是决定"我能怎么写代码"的依据。
|
||
|
||
== 为什么要这一段 ==
|
||
|
||
用户要的是:**搞清楚这台 Win7 上有哪些工具/组件可用**,
|
||
好在写兼容代码时不踩空。所以这一段不谈某个具体软件,
|
||
只回答"这台机器**能提供什么**"。
|
||
|
||
关注点:
|
||
* 系统到底哪个版本、什么 SP、什么架构
|
||
* .NET 装到几(System.Speech / WPF 依赖它)
|
||
* VC++ 运行库有没有(很多打包程序依赖)
|
||
* 命令行工具有哪些(cscript/wscript/powershell/wmic/mshta…)
|
||
* 中文语音在不在(SAPI 已在第二节,这里确认语音包)
|
||
* 已装程序清单(判断这是不是精简版系统)
|
||
* 硬件规模(内存 / 磁盘)—— 决定程序能不能跑
|
||
'''
|
||
head("五、环境与工具链")
|
||
|
||
# ---- 5.1 系统细节 ----
|
||
say()
|
||
say(" --- 5.1 系统细节 ---")
|
||
try:
|
||
v = sys.getwindowsversion()
|
||
item("内核版本", True, "%d.%d build %d" % (v.major, v.minor, v.build))
|
||
names = {(6, 1): "Windows 7", (6, 2): "Windows 8",
|
||
(6, 3): "Windows 8.1", (10, 0): "Windows 10/11"}
|
||
item("对应产品名", None, names.get((v.major, v.minor), "未知"))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
item("架构", None, os.environ.get("PROCESSOR_ARCHITECTURE", "?"))
|
||
item("处理器标识", None, os.environ.get("PROCESSOR_IDENTIFIER", "?")[:60])
|
||
item("CPU 核心数", None, str(os.cpu_count()))
|
||
item("系统盘/Windows", None, os.environ.get("SystemRoot", "?"))
|
||
item("Program Files", None, os.environ.get("ProgramFiles", "?"))
|
||
|
||
# 内存 / 磁盘
|
||
try:
|
||
class MEMORYSTATUSEX(ctypes.Structure):
|
||
_fields_ = [("dwLength", ctypes.c_ulong),
|
||
("dwMemoryLoad", ctypes.c_ulong),
|
||
("ullTotalPhys", ctypes.c_ulonglong),
|
||
("ullAvailPhys", ctypes.c_ulonglong),
|
||
("ullTotalPageFile", ctypes.c_ulonglong),
|
||
("ullAvailPageFile", ctypes.c_ulonglong),
|
||
("ullTotalVirtual", ctypes.c_ulonglong),
|
||
("ullAvailVirtual", ctypes.c_ulonglong),
|
||
("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
|
||
m = MEMORYSTATUSEX()
|
||
m.dwLength = ctypes.sizeof(m)
|
||
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m)):
|
||
item("物理内存", True, "%.2f GB(可用 %.2f GB,占用 %d%%)"
|
||
% (m.ullTotalPhys / 2 ** 30, m.ullAvailPhys / 2 ** 30,
|
||
m.dwMemoryLoad))
|
||
except Exception as exc: # noqa: BLE001
|
||
item("物理内存", False, str(exc))
|
||
|
||
for drive in ("C:\\",):
|
||
try:
|
||
free = ctypes.c_ulonglong(0)
|
||
total = ctypes.c_ulonglong(0)
|
||
ctypes.windll.kernel32.GetDiskFreeSpaceExW(
|
||
ctypes.c_wchar_p(drive), None, ctypes.byref(total),
|
||
ctypes.byref(free))
|
||
item("磁盘 %s" % drive, True,
|
||
"总 %.1f GB,剩 %.1f GB" % (total.value / 2 ** 30,
|
||
free.value / 2 ** 30))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
# ---- 5.2 .NET ----
|
||
say()
|
||
say(" --- 5.2 .NET 运行库(System.Speech 依赖)---")
|
||
ps = shutil.which("powershell") or (
|
||
os.environ.get("SystemRoot", r"C:\Windows")
|
||
+ r"\System32\WindowsPowerShell\v1.0\powershell.exe")
|
||
if Path(ps).is_file():
|
||
code, out = run([ps, "-NoProfile", "-Command",
|
||
"[System.Environment]::Version.ToString()"], timeout=25)
|
||
item("CLR 版本", code == 0, (out or "").strip())
|
||
|
||
# 注册表里的 .NET 版本
|
||
for hive in (r"HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full",
|
||
r"HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5",
|
||
r"HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.0"):
|
||
code, out = run(["reg", "query", hive, "/v", "Version"], timeout=20)
|
||
if code == 0 and out:
|
||
ver = ""
|
||
for ln in out.splitlines():
|
||
if "Version" in ln and "REG_SZ" in ln:
|
||
ver = ln.split("REG_SZ")[-1].strip()
|
||
item(hive.split("\\")[-2] + " / " + hive.split("\\")[-1],
|
||
True, ver or "在")
|
||
|
||
# ---- 5.3 命令行工具 ----
|
||
say()
|
||
say(" --- 5.3 命令行工具 ---")
|
||
sysroot = os.environ.get("SystemRoot", r"C:\Windows")
|
||
tools = [
|
||
("cscript", r"\System32\cscript.exe"),
|
||
("wscript", r"\System32\wscript.exe"),
|
||
("cmd", r"\System32\cmd.exe"),
|
||
("mshta", r"\System32\mshta.exe"), # 可跑 VBS/JS
|
||
("rundll32", r"\System32\rundll32.exe"),
|
||
("reg", r"\System32\reg.exe"),
|
||
("wmic", r"\System32\wbem\wmic.exe"), # 查硬件
|
||
("tasklist", r"\System32\tasklist.exe"),
|
||
("netsh", r"\System32\netsh.exe"),
|
||
("sc", r"\System32\sc.exe"),
|
||
("taskkill", r"\System32\taskkill.exe"),
|
||
("certutil", r"\System32\certutil.exe"),
|
||
]
|
||
for name, rel in tools:
|
||
found = shutil.which(name)
|
||
abs_p = sysroot + rel
|
||
if found:
|
||
item(name, True, found)
|
||
elif Path(abs_p).is_file():
|
||
item(name, True, "PATH 外,绝对路径在: %s" % abs_p)
|
||
else:
|
||
item(name, False, "找不到")
|
||
|
||
# ---- 5.4 Windows 组件 ----
|
||
say()
|
||
say(" --- 5.4 Windows 组件 ---")
|
||
# 语音(确认中文语音包)
|
||
if Path(ps).is_file():
|
||
code, out = run([
|
||
ps, "-NoProfile", "-Command",
|
||
"$v=New-Object -ComObject SAPI.SpVoice;"
|
||
"$v.GetVoices()|ForEach-Object{$_.GetDescription()}"], timeout=25)
|
||
vs = [x.strip() for x in (out or "").splitlines() if x.strip()]
|
||
cn = [x for x in vs if "Chinese" in x or "Huihui" in x or "中文" in x]
|
||
item("中文语音包", bool(cn),
|
||
"、".join(cn) if cn else "没有中文语音(只有: %s)"
|
||
% ("、".join(vs[:3]) if vs else "一个语音都没有"))
|
||
|
||
# 媒体功能
|
||
code, out = run(["reg", "query",
|
||
r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion",
|
||
"/v", "ProductName"], timeout=20)
|
||
name = ""
|
||
if code == 0:
|
||
for ln in (out or "").splitlines():
|
||
if "ProductName" in ln and "REG_SZ" in ln:
|
||
name = ln.split("REG_SZ")[-1].strip()
|
||
item("系统产品名(注册表)", None, name or "查不到")
|
||
|
||
# ---- 5.5 已装程序(判断是不是精简版)----
|
||
say()
|
||
say(" --- 5.5 已装程序(看是不是精简版系统)---")
|
||
# ⚠ 第一版用 `Get-ItemProperty` 配通配符 + `Select-Object -ExpandProperty`:
|
||
# 在 PowerShell 2.0 上会往输出里混进一堆报错文本
|
||
# ("Property DisplayName cannot be found"、"CategoryInfo"、"At line:1" …),
|
||
# 结果列表里全是垃圾行。**报错和结果混在同一个 stdout 里**,
|
||
# 光看行数根本分不出来 —— 这次报告里就出现了。
|
||
#
|
||
# 改法:
|
||
# 1. `Get-WmiObject Win32_Product`(PS 2.0 兼容,但只有 MSI 装的程序)
|
||
# 2. `reg query` 兜底 —— **完全不经过 PowerShell**,不受版本影响
|
||
# 3. 过滤掉所有已知的报错关键字
|
||
apps = set()
|
||
if Path(ps).is_file():
|
||
code, out = run([
|
||
ps, "-NoProfile", "-Command",
|
||
"Get-WmiObject -Class Win32_Product -ErrorAction SilentlyContinue | "
|
||
"ForEach-Object { $_.Name }"], timeout=90)
|
||
for x in (out or "").splitlines():
|
||
apps.add(x.strip())
|
||
|
||
for hive in (r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
|
||
r"HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows"
|
||
r"\CurrentVersion\Uninstall"):
|
||
code, out = run(["reg", "query", hive, "/s", "/v", "DisplayName"],
|
||
timeout=60, encoding="mbcs")
|
||
for ln in (out or "").splitlines():
|
||
if "DisplayName" in ln and "REG_SZ" in ln:
|
||
apps.add(ln.split("REG_SZ")[-1].strip())
|
||
|
||
# 过滤报错噪音
|
||
NOISE = ("CategoryInfo", "FullyQualifiedErrorId", "At line:", "+ ",
|
||
"PSArgumentException", "ExpandPropertyNotFound", "SelectObject",
|
||
"Get-ItemProperty", "Get-WmiObject :", "InvalidArgument",
|
||
"PSObject", "cannot be found", "not recognized", "?????")
|
||
clean = sorted({a for a in apps
|
||
if a and not any(n in a for n in NOISE)
|
||
and "HKLM:" not in a and "\\" not in a[:6]})
|
||
|
||
if clean:
|
||
item("已装程序数量", True, "共 %d 个" % len(clean))
|
||
for a in clean[:30]:
|
||
say(" - " + a)
|
||
if len(clean) > 30:
|
||
say(" ...(还有 %d 个)" % (len(clean) - 30))
|
||
else:
|
||
item("已装程序数量", False, "一个都列不出来")
|
||
|
||
# ---- 5.6 环境变量(关键几个)----
|
||
say()
|
||
say(" --- 5.6 环境变量 ---")
|
||
for k in ("PATH", "TEMP", "TMP", "SystemRoot", "windir",
|
||
"ProgramFiles", "ProgramData", "USERPROFILE", "APPDATA",
|
||
"LOCALAPPDATA", "COMPUTERNAME", "USERNAME"):
|
||
v = os.environ.get(k)
|
||
if k == "PATH":
|
||
item(k, None, "%d 个路径" % len((v or "").split(";")))
|
||
for p in (v or "").split(";")[:8]:
|
||
if p.strip():
|
||
say(" " + p.strip())
|
||
else:
|
||
item(k, None, v or "(没设)")
|
||
|
||
|
||
|
||
# ================================================================ main
|
||
def main() -> int:
|
||
say("=" * 64)
|
||
say(" ykxmz 诊断报告")
|
||
say(" 生成时间: %s" % datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||
say(" 机器名: %s" % os.environ.get("COMPUTERNAME", "?"))
|
||
say(" 用户名: %s" % os.environ.get("USERNAME", "?"))
|
||
say("=" * 64)
|
||
say()
|
||
say(" ★ 把这一整段复制发回去就行。程序不改动系统任何东西。")
|
||
|
||
for fn in (probe_system, probe_tts, probe_ui, probe_net, probe_env):
|
||
try:
|
||
fn()
|
||
except Exception: # noqa: BLE001
|
||
say()
|
||
say(" 【这一段探测崩了】")
|
||
for ln in traceback.format_exc().splitlines()[-6:]:
|
||
say(" " + ln)
|
||
|
||
say()
|
||
say("=" * 64)
|
||
say(" 报告结束")
|
||
say("=" * 64)
|
||
|
||
# 同时写到桌面和 exe 旁边
|
||
text = "\r\n".join(OUT)
|
||
written = []
|
||
targets = []
|
||
try:
|
||
targets.append(Path(os.path.expanduser("~")) / "Desktop" / "ykxmz诊断.txt")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
try:
|
||
targets.append(Path(sys.executable).parent / "ykxmz诊断.txt")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
targets.append(Path(tempfile.gettempdir()) / "ykxmz诊断.txt")
|
||
|
||
for t in targets:
|
||
# ⚠ 写两份:UTF-8-BOM(记事本能认)+ GBK(老编辑器/控制台重定向能认)。
|
||
# Win7 自带的记事本从 Vista 起就支持 UTF-8,但为了万无一失都写。
|
||
for enc, suffix in (("utf-8-sig", ""), ("gbk", "_gbk")):
|
||
try:
|
||
t.parent.mkdir(parents=True, exist_ok=True)
|
||
p = t if not suffix else t.with_name(t.stem + suffix + t.suffix)
|
||
p.write_text(text, encoding=enc, errors="replace")
|
||
written.append(str(p))
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
print()
|
||
print(" 报告已保存到:")
|
||
for w in written:
|
||
print(" " + w)
|
||
print()
|
||
print(" 按回车键关闭…")
|
||
try:
|
||
input()
|
||
except Exception: # noqa: BLE001
|
||
time.sleep(30)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
sys.exit(main())
|
||
except Exception: # noqa: BLE001
|
||
print(traceback.format_exc())
|
||
try:
|
||
input(" 出错了,按回车关闭…")
|
||
except Exception: # noqa: BLE001
|
||
pass
|