From 9d6394d4d97f64b11e7b355f7606a8fc5b92c53e Mon Sep 17 00:00:00 2001 From: xiadong Date: Mon, 3 Aug 2026 12:23:18 +0800 Subject: [PATCH] fix(windows): replace native scanner with fs scan to fix Segfault (#592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ztools_native.node's scanWindowsShortcuts Segfaults in the Electron main process on Windows systems where the desktop folder is redirected to a path containing non-ASCII characters (e.g. OneDrive/桌面). The crash bypasses try/catch and is suspected to be a conflict between uiohook-napi and the native module's Win32 API calls. Replace WindowsShortcutScanner.scan() with fs.readdirSync recursion over .lnk files. No native module is called, so the crash cannot occur. Known regressions vs. the native implementation: - .url shortcuts in Start Menu are not shown - desktop.ini localized names are not resolved (shows file basename) - .lnk target paths are not resolved (deduplication falls back to .lnk path) Fixes #592 --- .../core/commandScanner/windowsScanner.ts | 119 ++++++++++-------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/src/main/core/commandScanner/windowsScanner.ts b/src/main/core/commandScanner/windowsScanner.ts index a6c4faae..9d251c21 100644 --- a/src/main/core/commandScanner/windowsScanner.ts +++ b/src/main/core/commandScanner/windowsScanner.ts @@ -1,6 +1,8 @@ +import fs from 'fs' +import path from 'path' import { extractAcronym } from '../../utils/common' -import { getWindowsRootScanPaths, getWindowsScanPaths } from '../../utils/systemPaths' -import { WindowsShortcutScanner, type WindowsShortcutInfo } from '../native/index' +import { getWindowsScanPaths } from '../../utils/systemPaths' +import type { WindowsShortcutInfo } from '../native/index' import type { Command } from './types' // ========== 配置 ========== @@ -20,51 +22,33 @@ export const SKIP_FOLDERS = [ ] // 要跳过的快捷方式名称关键词(不区分大小写) -// 仅按名称过滤,不按目标类型/路径/扩展名过滤 -// 因为扫描范围仅限开始菜单和桌面,这些位置的快捷方式都是有意放置的 export const SKIP_NAME_PATTERN = /^uninstall|^卸载|卸载$|website|网站|帮助|help|readme|read me|文档|manual|license|documentation/i // ========== 辅助函数 ========== -/** - * Windows 扫描实现说明: - * - * 原 TS 实现里的这些步骤现在都已迁移到原生模块: - * - 解析 desktop.ini 中的 [LocalizedFileNames] 段。 - * desktop.ini 通常是 UTF-16LE 编码(带 BOM),部分为 UTF-8。 - * 条目值可能是纯文本或 MUI 引用(@dll,-id)。 - * - 批量解析 MUI 资源字符串(如 @%SystemRoot%\system32\shell32.dll,-22067)。 - * 通过 Win32 API 解析 Windows 系统快捷方式的本地化显示名称。 - * - 解析 .url 文件,提取 URL 和 IconFile 字段。 - * 跳过普通网页链接(http/https),保留其他应用协议(如 steam://)。 - * - 处理单个快捷方式 entry(.url / .lnk):解析、过滤、入列。 - * 递归与扁平扫描共用,仅处理文件 entry;目录的下钻 / 跳过由原生模块决定。 - * - 递归扫描目录中的快捷方式(Programs 子树 / 桌面)。 - * 处理子目录时跳过 SDK、示例、文档等开发相关文件夹。 - * - 扁平扫描 Start Menu 根。 - * 仅处理本层文件,不下钻 Programs 子目录,避免重复索引。 - * - * TS 层保留最终的名称过滤、图标协议封装、首字母缩写和去重,避免业务侧行为变化。 - */ - -// 检查是否应该跳过该快捷方式(仅按名称过滤) export function shouldSkipShortcut(name: string): boolean { return SKIP_NAME_PATTERN.test(name) } -// 生成图标 URL export function getIconUrl(appPath: string): string { - // 将绝对路径编码为 URL return `ztools-icon://${encodeURIComponent(appPath)}` } /** - * 将原生模块扫描结果转换为 Command。 - * - * desktop.ini 本地化名称、MUI 解析、.url 解析、.lnk 目标解析已迁移到原生模块实现; - * TS 层只保留名称过滤、图标协议封装、首字母缩写和去重字段整理。 + * 将 .lnk 文件路径转换为 WindowsShortcutInfo 结构。 + * path 设为 .lnk 文件路径(Launcher 可直接通过 Shell 打开), + * icon 同样指向 .lnk,图标协议层会通过 Windows Shell 提取目标图标。 */ +function lnkToEntry(lnkPath: string): WindowsShortcutInfo { + return { + name: path.basename(lnkPath, '.lnk'), + path: lnkPath, + icon: lnkPath, + sourceType: 'lnk' + } +} + function toCommand(entry: WindowsShortcutInfo): (Command & { _dedupeTarget?: string }) | null { if (!entry.name || !entry.path) { return null @@ -74,10 +58,6 @@ function toCommand(entry: WindowsShortcutInfo): (Command & { _dedupeTarget?: str return null } - // 始终使用原生模块返回的启动路径: - // - .lnk:使用快捷方式路径,Windows Shell API 能正确处理参数、工作目录等 - // - .url 或 .lnk 指向 .url:使用应用协议链接(已在原生模块跳过 http/https) - // 图标使用原生模块返回的 icon 源路径,再封装成 ztools-icon:// 协议 return { name: entry.name, path: entry.path, @@ -87,19 +67,12 @@ function toCommand(entry: WindowsShortcutInfo): (Command & { _dedupeTarget?: str } } -/** - * 去重:按名称+目标路径的组合去重(允许不同名但同目标的应用共存) - * 对于 .lnk 快捷方式,使用 _dedupeTarget(目标路径)而非 .lnk 路径去重 - * 这样同名同目标但位于不同目录(用户/系统开始菜单)的快捷方式只保留一个 - */ export function deduplicateCommands(apps: (Command & { _dedupeTarget?: string })[]): Command[] { const uniqueApps = new Map() apps.forEach((app) => { - // 优先使用 _dedupeTarget(快捷方式的目标路径)去重,降级为 path const dedupeTarget = app._dedupeTarget || app.path const dedupeKey = `${app.name.toLowerCase()}|${dedupeTarget.toLowerCase()}` if (!uniqueApps.has(dedupeKey)) { - // 清除内部去重字段,不泄漏到外部 const { _dedupeTarget, ...cleanApp } = app uniqueApps.set(dedupeKey, cleanApp) } @@ -107,29 +80,71 @@ export function deduplicateCommands(apps: (Command & { _dedupeTarget?: string }) return Array.from(uniqueApps.values()) } +/** + * 递归收集 dir 下所有 .lnk 文件路径。 + * 跳过 SKIP_FOLDERS 中列出的子目录名称(不区分大小写)。 + */ +function collectLnkFiles(dir: string, out: string[]): void { + let entries: string[] + try { + entries = fs.readdirSync(dir) + } catch { + return + } + for (const name of entries) { + const full = path.join(dir, name) + let stat: fs.Stats + try { + stat = fs.statSync(full) + } catch { + continue + } + if (stat.isDirectory()) { + if (!SKIP_FOLDERS.includes(name.toLowerCase())) { + collectLnkFiles(full, out) + } + } else if (name.toLowerCase().endsWith('.lnk')) { + out.push(full) + } + } +} + +/** + * Windows 应用扫描。 + * + * 使用 fs.readdirSync 递归收集开始菜单和桌面的 .lnk 文件, + * 不调用 ztools_native.node,规避 #592 中 Electron 主进程 Segfault 问题 + * (native 模块在含非 ASCII 字符桌面路径的环境下与 uiohook-napi 存在冲突)。 + * + * 相比原生实现的已知差异: + * - 不解析 .url 文件(开始菜单中的 URL 快捷方式不显示) + * - 不读取 desktop.ini 本地化名称(显示英文快捷方式文件名) + * - 不解析 .lnk 目标路径(dedupeTarget 为空,同名应用在去重上依赖文件名) + */ export async function scanApplications(): Promise { try { - // 获取 Windows 扫描路径(开始菜单 + 桌面) const scanPaths = getWindowsScanPaths() - // 获取 Start Menu 根路径 - const rootScanPaths = getWindowsRootScanPaths() + if (scanPaths.length === 0) return [] + + const lnkPaths: string[] = [] + for (const sp of scanPaths) { + collectLnkFiles(sp, lnkPaths) + } - // 原生模块负责递归扫描 Programs + 桌面,并扁平扫描 Start Menu 根 - // 同时在原生模块中处理 desktop.ini 本地化名称、MUI 资源、.url 和 .lnk - const nativeEntries = WindowsShortcutScanner.scan(scanPaths, rootScanPaths, SKIP_FOLDERS) - const apps = nativeEntries + const entries = lnkPaths.map(lnkToEntry) + const apps = entries .map((entry) => toCommand(entry)) .filter((entry): entry is Command & { _dedupeTarget?: string } => entry !== null) const deduplicatedApps = deduplicateCommands(apps) console.log( - `[Scanner] native 扫描完成: ${nativeEntries.length} 个条目 -> ${deduplicatedApps.length} 个应用` + `[Scanner] fs scan: ${lnkPaths.length} .lnk -> ${deduplicatedApps.length} apps` ) return deduplicatedApps } catch (error) { - console.error('[Scanner] native Windows 应用扫描失败:', error) + console.error('[Scanner] Windows app scan failed:', error) return [] } }