Skip to content

Commit 3805dfd

Browse files
committed
修复工具检测
1. 修复git bash等工具只检测固定路径问题。
1 parent d3ed8e3 commit 3805dfd

1 file changed

Lines changed: 267 additions & 28 deletions

File tree

Code-encoding-fix.py

Lines changed: 267 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from tkinter import filedialog, scrolledtext, ttk
1717
import json
1818
from typing import Callable
19+
from itertools import chain
20+
import time
1921

2022
try:
2123
import winreg # type: ignore
@@ -76,12 +78,15 @@ def __init__(self, root: tk.Tk) -> None:
7678
self._detect_cache_max = 64
7779
self._shell_marker_detail = {}
7880
self._tool_config_detail = {}
81+
self._registry_cache: dict[tuple[str, ...], list[Path]] = {}
82+
self._shortcut_cache: dict[tuple[str, ...], list[Path]] = {}
83+
self._detecting = False
7984

8085
self._build_layout()
8186
self._apply_window_position()
8287
# 先记录应用启动,再进行 Shell 路径检测,保证日志顺序符合直觉
8388
self._log("应用已启动,准备检测 Shell 路径", "info")
84-
self._detect_all_paths(log=True)
89+
self._detect_all_paths_in_thread(log=True)
8590
self._refresh_env_tool_labels()
8691
self._update_restore_button_state()
8792
self._refresh_start_button_state()
@@ -245,14 +250,12 @@ def _build_layout(self) -> None:
245250
row_idx = self._build_shell_row(
246251
parent=path_frame,
247252
key="vscode",
248-
label_text="Visual Studio Code settings",
253+
label_text="Visual Studio Code",
249254
var=self.vscode_path_var,
250255
open_cmd=lambda: self._open_path("vscode"),
251256
start_row=row_idx,
252257
readonly=True,
253258
)
254-
# Visual Studio Code 状态提示
255-
self._row_widgets["vscode"]["status_full"].config(textvariable=self.tool_info_var)
256259

257260
console_frame = ttk.LabelFrame(self.root, text="控制台配置", padding="8")
258261
console_frame.pack(fill="x", padx=12, pady=(2, 4))
@@ -263,7 +266,7 @@ def _build_layout(self) -> None:
263266
status_frame.pack(fill="x", padx=0, pady=(0, 4))
264267
self.admin_label = ttk.Label(status_frame, textvariable=self.status_var, foreground="#0063b1")
265268
self.admin_label.pack(side="left", anchor="w")
266-
ttk.Button(status_frame, text="重新检测", command=self._detect_all_paths).pack(side="right")
269+
ttk.Button(status_frame, text="重新检测", command=lambda: self._detect_all_paths_in_thread(log=True)).pack(side="right")
267270

268271
progress_frame = ttk.Frame(self.root, padding="12 2 12 0")
269272
progress_frame.pack(fill="x")
@@ -1479,8 +1482,32 @@ def _update_restore_button_state(self, buttons_enabled: bool | None = None) -> N
14791482
state = "normal" if enabled else "disabled"
14801483
self.restore_btn.config(state=state)
14811484

1485+
# --------- 检测调度 ---------
1486+
def _detect_all_paths_in_thread(self, log: bool = True) -> None:
1487+
"""后台线程执行检测,避免启动/重新检测阻塞 UI。"""
1488+
if self.is_running or getattr(self, "_detecting", False):
1489+
return
1490+
self._detecting = True
1491+
self.status_var.set("正在检测工具与编码状态...")
1492+
self._set_buttons_state(False)
1493+
threading.Thread(
1494+
target=self._run_detect_all_paths_safe, args=(log,), daemon=True
1495+
).start()
1496+
1497+
def _run_detect_all_paths_safe(self, log: bool) -> None:
1498+
try:
1499+
self._detect_all_paths(log=log)
1500+
finally:
1501+
self._ui_call(self._on_detect_done)
1502+
1503+
def _on_detect_done(self) -> None:
1504+
self._detecting = False
1505+
self._set_buttons_state(True)
1506+
self._refresh_start_button_state()
1507+
14821508
def _detect_all_paths(self, log: bool = True) -> None:
14831509
# 如果上一条日志是检测分隔线,先清理本次检测段落,避免重复追加
1510+
t0 = time.perf_counter()
14841511
if log:
14851512
self._trim_last_detection_block()
14861513
self._log_separator("检测开始")
@@ -1492,6 +1519,7 @@ def _detect_all_paths(self, log: bool = True) -> None:
14921519
# 在检测块中输出“哪些内容被手动更改”的差异提示
14931520
self._log_config_drift_report()
14941521
if log:
1522+
self._log(f"检测耗时 {time.perf_counter() - t0:.2f}s", "info")
14951523
self._log_separator("检测结束")
14961524
# 先刷新编码/环境,再基于结果刷新汇总与按钮状态
14971525
self._update_console_state_label()
@@ -1501,8 +1529,18 @@ def _detect_all_paths(self, log: bool = True) -> None:
15011529
self._refresh_reset_default_button_state()
15021530

15031531
def _detect_ps5(self, log: bool = True) -> None:
1504-
sys_root = os.environ.get("SystemRoot", r"C:\Windows")
1505-
candidate = Path(sys_root) / "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe"
1532+
candidate = None
1533+
which_ps = shutil.which("powershell")
1534+
if which_ps:
1535+
candidate = Path(which_ps).resolve()
1536+
if candidate is None:
1537+
sys_root = os.environ.get("SystemRoot", r"C:\Windows")
1538+
candidate = Path(sys_root) / "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe"
1539+
if candidate is None or not candidate.exists():
1540+
for target in self._shortcut_targets(["**/Windows PowerShell*.lnk"]):
1541+
if target.exists():
1542+
candidate = target
1543+
break
15061544
if candidate.exists():
15071545
self._ps5_exe = candidate
15081546
self._ps5_available = True
@@ -1527,14 +1565,36 @@ def _detect_ps5(self, log: bool = True) -> None:
15271565
self._log("未检测到 Windows PowerShell 5.1", "warning")
15281566

15291567
def _detect_ps7(self, log: bool = True) -> None:
1530-
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
1531-
candidates = sorted(Path(pf).glob("PowerShell/*/pwsh.exe"), reverse=True)
15321568
path = None
1533-
for c in candidates:
1534-
if c.exists():
1535-
path = c
1536-
break
1537-
if path:
1569+
which_pwsh = shutil.which("pwsh")
1570+
if which_pwsh:
1571+
path = Path(which_pwsh).resolve()
1572+
else:
1573+
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
1574+
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
1575+
pf64 = os.environ.get("ProgramW6432", pf)
1576+
search_roots = [pf, pf86, pf64]
1577+
candidates: list[Path] = []
1578+
for root in search_roots:
1579+
if not root:
1580+
continue
1581+
candidates.extend(sorted(Path(root).glob("PowerShell/*/pwsh.exe"), reverse=True))
1582+
for c in candidates:
1583+
if c.exists():
1584+
path = c
1585+
break
1586+
if path is None:
1587+
for loc in self._registry_install_locations(["powershell 7"]):
1588+
candidate = Path(loc) / "pwsh.exe"
1589+
if candidate.exists():
1590+
path = candidate
1591+
break
1592+
if path is None:
1593+
for target in self._shortcut_targets(["**/PowerShell 7*.lnk"]):
1594+
if target.exists():
1595+
path = target
1596+
break
1597+
if path and path.exists():
15381598
self._ps7_exe = path
15391599
self._ps7_available = True
15401600
profile_exists = self._ps7_profile_path.exists()
@@ -1768,38 +1828,196 @@ def _trim_last_detection_block(self) -> None:
17681828
self.log_text.insert("end", new_content + "\n")
17691829
self.log_text.configure(state="disabled")
17701830

1831+
# --------- 通用候选路径收集工具 ---------
1832+
def _registry_install_locations(self, keywords: list[str]) -> list[Path]:
1833+
"""从卸载注册表读取 InstallLocation,关键词大小写不敏感。"""
1834+
key_tuple = tuple(sorted(k.lower() for k in keywords))
1835+
if key_tuple in self._registry_cache:
1836+
return list(self._registry_cache[key_tuple])
1837+
if winreg is None:
1838+
return []
1839+
locations: list[Path] = []
1840+
hives = [
1841+
(winreg.HKEY_LOCAL_MACHINE, "HKLM"),
1842+
(winreg.HKEY_CURRENT_USER, "HKCU"),
1843+
]
1844+
views = [0]
1845+
if hasattr(winreg, "KEY_WOW64_64KEY"):
1846+
views = [winreg.KEY_WOW64_64KEY, winreg.KEY_WOW64_32KEY]
1847+
for hive, _ in hives:
1848+
for view in views:
1849+
try:
1850+
key = winreg.OpenKey(
1851+
hive,
1852+
r"Software\Microsoft\Windows\CurrentVersion\Uninstall",
1853+
0,
1854+
winreg.KEY_READ | view,
1855+
)
1856+
except OSError:
1857+
continue
1858+
try:
1859+
i = 0
1860+
while True:
1861+
try:
1862+
subkey_name = winreg.EnumKey(key, i)
1863+
except OSError:
1864+
break
1865+
i += 1
1866+
try:
1867+
subkey = winreg.OpenKey(key, subkey_name)
1868+
display_name, _ = winreg.QueryValueEx(subkey, "DisplayName")
1869+
except OSError:
1870+
continue
1871+
name_lower = str(display_name).lower()
1872+
if not any(k.lower() in name_lower for k in keywords):
1873+
continue
1874+
try:
1875+
loc, _ = winreg.QueryValueEx(subkey, "InstallLocation")
1876+
except OSError:
1877+
loc = ""
1878+
if loc:
1879+
p = Path(loc).expanduser()
1880+
if p.exists():
1881+
locations.append(p)
1882+
finally:
1883+
try:
1884+
winreg.CloseKey(key)
1885+
except Exception:
1886+
pass
1887+
seen = set()
1888+
uniq: list[Path] = []
1889+
for loc in locations:
1890+
if loc not in seen:
1891+
seen.add(loc)
1892+
uniq.append(loc)
1893+
self._registry_cache[key_tuple] = uniq
1894+
return uniq
1895+
1896+
def _shortcut_targets(self, patterns: list[str]) -> list[Path]:
1897+
"""解析开始菜单快捷方式目标路径(最佳努力,依赖 PowerShell COM)。"""
1898+
pat_tuple = tuple(sorted(patterns))
1899+
if pat_tuple in self._shortcut_cache:
1900+
return list(self._shortcut_cache[pat_tuple])
1901+
start_roots = [
1902+
Path(os.environ.get("ProgramData", r"C:\ProgramData"))
1903+
/ "Microsoft"
1904+
/ "Windows"
1905+
/ "Start Menu"
1906+
/ "Programs",
1907+
Path(os.environ.get("APPDATA", Path.home()))
1908+
/ "Microsoft"
1909+
/ "Windows"
1910+
/ "Start Menu"
1911+
/ "Programs",
1912+
]
1913+
pwsh = shutil.which("powershell") or shutil.which("pwsh")
1914+
if not pwsh:
1915+
return []
1916+
1917+
existing_roots = [str(r) for r in start_roots if r.exists()]
1918+
if not existing_roots:
1919+
self._shortcut_cache[pat_tuple] = []
1920+
return []
1921+
1922+
# 使用单次 PowerShell 批量解析,减少进程开销
1923+
pattern_clause = " -or ".join([f"($_.Name -like '{p}')" for p in patterns])
1924+
roots_literal = ",".join([f"'{r}'" for r in existing_roots])
1925+
ps_script = ";".join(
1926+
[
1927+
"$ErrorActionPreference='SilentlyContinue'",
1928+
f"$roots=@({roots_literal})",
1929+
"$ws=New-Object -ComObject WScript.Shell",
1930+
"$res=@()",
1931+
"foreach($r in $roots){",
1932+
" if(Test-Path $r){",
1933+
" Get-ChildItem -LiteralPath $r -Filter *.lnk -Recurse | Where-Object {"
1934+
f" {pattern_clause} }} | ForEach-Object {{"
1935+
" $s=$ws.CreateShortcut($_.FullName);"
1936+
" if($s -and $s.TargetPath){ $res += $s.TargetPath }"
1937+
" }",
1938+
" }",
1939+
"}",
1940+
"$res | Sort-Object -Unique",
1941+
]
1942+
)
1943+
targets: list[Path] = []
1944+
try:
1945+
proc = subprocess.run(
1946+
[pwsh, "-NoProfile", "-Command", ps_script],
1947+
capture_output=True,
1948+
text=True,
1949+
timeout=3,
1950+
)
1951+
if proc.stdout:
1952+
for line in proc.stdout.splitlines():
1953+
p = Path(line.strip()).expanduser()
1954+
if p.exists():
1955+
targets.append(p.resolve())
1956+
except Exception:
1957+
pass
1958+
1959+
seen = set()
1960+
uniq: list[Path] = []
1961+
for t in targets:
1962+
if t not in seen:
1963+
seen.add(t)
1964+
uniq.append(t)
1965+
self._shortcut_cache[pat_tuple] = uniq
1966+
return uniq
1967+
17711968
def _detect_git_paths(self, log: bool = True) -> None:
1772-
candidates = []
1969+
primary_paths: list[Path] = []
17731970
env_paths = [
17741971
os.environ.get("ProgramFiles"),
17751972
os.environ.get("ProgramFiles(x86)"),
1973+
os.environ.get("ProgramW6432"),
17761974
os.environ.get("USERPROFILE"),
17771975
]
17781976
for base in env_paths:
17791977
if not base:
17801978
continue
1781-
p = Path(base) / "Git"
1782-
candidates.append(p)
1783-
local = Path(base) / "AppData" / "Local" / "Programs" / "Git"
1784-
candidates.append(local)
1785-
candidates.append(Path("D:/Programs/Git"))
1979+
base_path = Path(base)
1980+
primary_paths.append(base_path / "Git")
1981+
primary_paths.append(base_path / "AppData" / "Local" / "Programs" / "Git")
17861982

17871983
bash_in_path = shutil.which("bash")
17881984
if bash_in_path:
1789-
candidates.append(Path(bash_in_path).resolve().parents[1])
1790-
1791-
unique_paths = []
1792-
for path in candidates:
1793-
if path and path not in unique_paths:
1794-
unique_paths.append(path)
1985+
primary_paths.append(Path(bash_in_path).resolve().parents[1])
1986+
git_in_path = shutil.which("git")
1987+
if git_in_path:
1988+
git_root = Path(git_in_path).resolve().parent.parent
1989+
primary_paths.append(git_root)
1990+
1991+
program_data = os.environ.get("ProgramData", r"C:\ProgramData")
1992+
secondary_paths = [
1993+
Path(program_data) / "chocolatey" / "lib" / "git" / "tools",
1994+
Path.home() / "scoop" / "apps" / "git" / "current",
1995+
Path("D:/Programs/Git"),
1996+
Path.home() / "AppData" / "Local" / "Programs" / "Git",
1997+
]
17951998

1796-
found = None
1797-
for path in unique_paths:
1999+
found: Path | None = None
2000+
for path in primary_paths:
17982001
bash = path / "bin" / "bash.exe"
17992002
if bash.exists():
18002003
found = bash
18012004
break
18022005

2006+
if not found:
2007+
# 仅在快速路径未命中时再做重扫描(注册表/快捷方式),避免启动慢
2008+
for loc in self._registry_install_locations(["git for windows", "git version", "git"]):
2009+
secondary_paths.append(loc)
2010+
for target in self._shortcut_targets(["Git Bash*.lnk", "Git*.lnk"]):
2011+
if target.name.lower().startswith("git-bash") or target.name.lower() == "bash.exe":
2012+
secondary_paths.append(target.parent.parent)
2013+
else:
2014+
secondary_paths.append(target.parent)
2015+
for path in secondary_paths:
2016+
bash = path / "bin" / "bash.exe"
2017+
if bash.exists():
2018+
found = bash
2019+
break
2020+
18032021
if found:
18042022
self._git_exe = found
18052023
self._git_bashrc_path = Path.home() / ".bashrc"
@@ -1828,6 +2046,27 @@ def _detect_vscode(self, log: bool = True) -> None:
18282046
settings_path = Path(appdata) / "Code" / "User" / "settings.json" if appdata else None
18292047
exe_path = shutil.which("code") or shutil.which("code.cmd")
18302048
exe_resolved = Path(exe_path).resolve() if exe_path else None
2049+
if not exe_resolved:
2050+
local_app = os.environ.get("LOCALAPPDATA")
2051+
candidates = []
2052+
if local_app:
2053+
candidates.append(Path(local_app) / "Programs" / "Microsoft VS Code" / "Code.exe")
2054+
program_files = [
2055+
os.environ.get("ProgramFiles"),
2056+
os.environ.get("ProgramFiles(x86)"),
2057+
os.environ.get("ProgramW6432"),
2058+
]
2059+
for root in program_files:
2060+
if root:
2061+
candidates.append(Path(root) / "Microsoft VS Code" / "Code.exe")
2062+
for loc in self._registry_install_locations(["visual studio code", "microsoft visual studio code"]):
2063+
candidates.append(Path(loc) / "Code.exe")
2064+
for target in self._shortcut_targets(["**/Visual Studio Code*.lnk"]):
2065+
candidates.append(target)
2066+
for c in candidates:
2067+
if c and c.exists():
2068+
exe_resolved = c.resolve()
2069+
break
18312070
self._vscode_available = bool(exe_resolved)
18322071
display_exe = None
18332072
if exe_resolved:

0 commit comments

Comments
 (0)