You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Rust开发Windows启动器:可靠枚举已安装应用及获取可执行路径的方案咨询

Rust开发Windows启动器:可靠枚举已安装应用及获取可执行路径的方案咨询

我来帮你梳理这个Windows启动器开发中的核心问题,结合Rust生态和Windows平台特性,逐一解答你的疑问,并给出可落地的优化方案:

一、当前双源方案的合理性

你的开始菜单快捷方式+注册表的方向完全正确,这也是大多数Windows启动器的标准做法,但可以优化优先级和细节处理:

  • 开始菜单快捷方式(.lnk)确实是最可靠的源,因为它直接指向可执行文件,且是用户实际能看到的程序入口;
  • 注册表作为补充是必要的,但你当前只扫了HKLM下的Uninstall项,遗漏了32位程序在64位系统下的Wow6432Node路径,还有HKCU下的Uninstall项(用户级安装的程序)。

二、更可靠的可执行路径获取方式

1. 优化快捷方式(.lnk)解析

你用lnk crate的思路没问题,但可以补充两个细节:

  • 确保只保留目标为.exe的快捷方式,过滤掉URL、脚本等其他类型的快捷方式;
  • 切换到UTF-16编码解析,Windows原生快捷方式用UTF-16存储路径,比WINDOWS_1252兼容更多非英文字符的场景。

2. 深挖注册表的可用字段

注册表的InstallLocation只是安装目录,不是直接的exe路径,你可以尝试从这些字段中提取:

  • DisplayIcon:格式通常是C:\path\to\app.exe,0,分割逗号取前半部分即可得到exe路径;
  • UninstallString/QuietUninstallString:通常包含exe路径(比如"C:\path\to\uninstall.exe" /param),可以提取引号内的内容作为exe路径;
  • 以上字段都没有时,再从InstallLocation目录下智能扫描exe。

三、处理注册表没有直接exe路径的情况

当只有InstallLocation时,不要直接存目录,而是在该目录下智能扫描可执行文件

  • 优先查找与程序名称同名的exe(比如程序名是"VS Code",找Code.exe);
  • 扫描常见子目录:binappx64x86等;
  • 限制扫描深度(最多2层),避免不必要的性能开销;
  • 如果找到多个exe,优先选择文件大小最大的那个(通常是主程序)。

四、推荐的Rust Crates和Windows API

1. 核心Crates

  • windows:微软官方维护的Windows API Rust绑定,替代winreg可以直接调用COM API(比如IShellLinkSHGetKnownFolderPath),功能更全面,也更贴合系统原生行为;
  • lnk:如果不想用复杂的COM API,这个轻量crate足够应付大部分.lnk解析场景,注意升级到最新版本;
  • rayon:如果需要加速目录/注册表扫描,可以用它做并行迭代,提升大系统下的扫描速度;
  • 你正在用的walkdirserdeserde_json保持不变即可。

2. 关键Windows API

  • IShellLinkW:解析快捷方式的权威API,支持路径自动修复(比如目标文件移动后重新定位);
  • SHGetKnownFolderPath:获取系统已知文件夹路径(比如开始菜单),比硬编码环境变量更可靠,避免环境变量被篡改的情况;
  • RegOpenKeyExW:配合KEY_WOW64_32KEY/KEY_WOW64_64KEY标志,可以同时读取32位和64位程序的注册表项。

五、代码优化建议

我基于你的原始代码做了核心逻辑优化,解决你提到的所有问题:

use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};

use walkdir::{DirEntry, WalkDir};
use winreg::enums::*;
use winreg::RegKey;
use serde::Serialize;

#[derive(Debug, Serialize, Clone)]
struct App {
    name: String,
    path: String,
}

// 过滤.lnk文件
fn is_lnk_file(entry: &DirEntry) -> bool {
    entry.file_type().is_file() 
    && entry.path().extension().and_then(|s| s.to_str()) == Some("lnk")
}

// 从安装目录扫描可执行文件
fn scan_exe_from_dir(install_dir: &str, app_name: &str) -> Option<String> {
    let dir_path = Path::new(install_dir);
    if !dir_path.is_dir() {
        return None;
    }

    let app_name_lower = app_name.to_lowercase();
    let mut candidates = WalkDir::new(dir_path)
        .max_depth(2)
        .into_iter()
        .filter_map(Result::ok)
        .filter(|e| {
            e.file_type().is_file() 
            && e.path().extension().and_then(|s| s.to_str()) == Some("exe")
        })
        .map(|e| e.path().to_path_buf())
        .collect::<Vec<_>>();

    // 优先匹配名称近似的exe
    if let Some(match_exe) = candidates.iter().find(|exe| {
        exe.file_name()
            .and_then(|s| s.to_str())
            .map_or(false, |name| name.to_lowercase().contains(&app_name_lower))
    }) {
        return Some(match_exe.to_string_lossy().to_string());
    }

    // 没有匹配的话,取第一个找到的exe
    candidates.sort_by_key(|p| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0));
    candidates.last().map(|p| p.to_string_lossy().to_string())
}

// 从注册表字段解析exe路径
fn parse_exe_from_reg(subkey: &RegKey, app_name: &str) -> Option<String> {
    // 尝试DisplayIcon字段
    if let Ok(display_icon) = subkey.get_value::<String, _>("DisplayIcon") {
        let exe_path = display_icon.split(',').next().unwrap_or(&display_icon);
        if Path::new(exe_path).exists() {
            return Some(exe_path.to_string());
        }
    }

    // 尝试UninstallString字段
    if let Ok(uninstall_str) = subkey.get_value::<String, _>("UninstallString") {
        let exe_path = uninstall_str.split('"').nth(1).unwrap_or(&uninstall_str);
        if exe_path.ends_with(".exe") && Path::new(exe_path).exists() {
            return Some(exe_path.to_string());
        }
    }

    // 最后尝试扫描安装目录
    subkey.get_value::<String, _>("InstallLocation")
        .ok()
        .and_then(|loc| scan_exe_from_dir(&loc, app_name))
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut apps: HashMap<String, App> = HashMap::new();

    // 1️⃣ 扫描开始菜单快捷方式(优先源)
    let appdata_path = format!(
        "{}\\Microsoft\\Windows\\Start Menu\\Programs",
        std::env::var("APPDATA")?
    );
    let start_paths = vec![
        r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs",
        &appdata_path,
    ];

    for dir in start_paths {
        for entry in WalkDir::new(dir)
            .into_iter()
            .filter_map(Result::ok)
            .filter(is_lnk_file)
        {
            match lnk::ShellLink::open(entry.path(), lnk::encoding::UTF_16) {
                Ok(link) => {
                    if let Some(target) = link.target() {
                        let target_path = target.to_string_lossy().to_string();
                        if target_path.ends_with(".exe") && Path::new(&target_path).exists() {
                            let app_name = entry.path()
                                .file_stem()
                                .unwrap_or_default()
                                .to_string_lossy()
                                .to_string();
                            
                            apps.entry(app_name.clone()).or_insert(App {
                                name: app_name,
                                path: target_path,
                            });
                        }
                    }
                }
                Err(e) => eprintln!("Failed to parse lnk {}: {}", entry.path().display(), e),
            }
        }
    }

    // 2️⃣ 扫描注册表(补充源,覆盖32/64位、用户/机器级)
    let reg_entries = vec![
        (HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
        (HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
        (HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
    ];

    for (hive, path) in reg_entries {
        let root_key = RegKey::predef(hive);
        match root_key.open_subkey(path) {
            Ok(uninstall_key) => {
                for key_name in uninstall_key.enum_keys().filter_map(Result::ok) {
                    match uninstall_key.open_subkey(&key_name) {
                        Ok(subkey) => {
                            if let Ok(app_name) = subkey.get_value::<String, _>("DisplayName") {
                                if !apps.contains_key(&app_name) {
                                    if let Some(exe_path) = parse_exe_from_reg(&subkey, &app_name) {
                                        apps.insert(app_name.clone(), App {
                                            name: app_name,
                                            path: exe_path,
                                        });
                                    }
                                }
                            }
                        }
                        Err(e) => eprintln!("Skipping subkey {}: {}", key_name, e),
                    }
                }
            }
            Err(e) => eprintln!("Skipping registry path {}: {}", path, e),
        }
    }

    // 3️⃣ 保存结果到JSON
    let result: Vec<App> = apps.into_values().collect();
    let json_output = serde_json::to_string_pretty(&result)?;

    let output_path = "src/data.json";
    let mut file = File::create(output_path)?;
    file.write_all(json_output.as_bytes())?;

    println!("Successfully saved {} applications to {}", result.len(), output_path);
    Ok(())
}

六、额外注意事项

  1. 错误处理:把原始代码中的unwrap()全部替换为?或错误匹配,避免程序因权限不足、路径不存在等情况崩溃;
  2. 重复名称处理:如果想保留同名的不同程序(比如同一软件的32/64位版本),可以把HashMap<String, App>改成Vec<App>,或者用(name, path)作为复合key;
  3. 权限问题:扫描系统目录或注册表时,部分项可能没有读取权限,要捕获错误并跳过,不要中断整个扫描流程;
  4. 性能优化:如果需要更快的扫描速度,可以用rayon把目录遍历和注册表枚举改成并行处理,但启动器场景下单线程通常足够。

火山引擎 最新活动