返回博客

macOS 检测 App 是否安装,osascript path to application id 会偷偷把它启动

我在做一个桌面工具的「扫描本机编辑器/终端」功能时,遭遇了一个让人摸不着头脑的 bug:每次触发扫描逻辑,几秒后屏幕上就莫名弹出几个根本没打开过的应用窗口。延迟时长恰好等于批量 osascript 的执行时间,第一反应是「事件冒泡」或「按钮被重复触发」,绕了好大一圈才找到真正的元凶。

现象

扫描逻辑用以下命令逐一解析 bundle id 对应的安装路径:

osascript -e 'path to application id "com.todesktop.230313mzl4w4u92"'   # Cursor
osascript -e 'path to application id "com.cmuxterm.app"'                  # cmux

每次执行扫描后,Cursor、cmux 等应用自己弹了出来——不是偶发,而是 100% 复现,延迟约等于批量 osascript 调用的耗时。

根因

path to application id "X"(以及 path to application "Name")底层走的是 macOS LaunchServices。LaunchServices 在解析部分应用时,不只返回路径,而是真正触发了 launch

坑:path to application id 对 Electron/todesktop app 会直接启动它

实测受影响的有 com.todesktop.*(Cursor)、com.cmuxterm.app(cmux)等 todesktop/Electron 打包应用;而 com.apple.*com.microsoft.VSCodecom.mitchellh.ghostty 当时并未被启动。

行为因 app 而异,不一致且不可依赖——绝不能用它做「纯检测」。

验证方法:在执行前后分别跑 pgrep -l <appname>,对比进程列表,一眼看出是否凭空多了新进程。

正解

mdfind 查询 Spotlight 索引。它只读索引,不会触发任何 launch,零副作用:

mdfind "kMDItemCFBundleIdentifier == 'com.todesktop.230313mzl4w4u92'"
# 已安装 → stdout 输出 .app 路径(非空)
# 未安装 → exit 0 但 stdout 为空

在 Tauri 命令里批量检测的 Rust 实现:

#[tauri::command]
pub fn detect_apps(bundle_ids: Vec<String>) -> Vec<String> {
    use std::process::Command;
    bundle_ids.into_iter().filter(|bid| {
        Command::new("mdfind")
            .arg(format!("kMDItemCFBundleIdentifier == '{bid}'"))
            .output()
            .map(|o| o.status.success() && !o.stdout.is_empty())
            .unwrap_or(false)
    }).collect()
}

bundle id 只含 [A-Za-z0-9.-],格式字符串里无引号注入风险。

正解:用 mdfind 查 Spotlight 索引,只读、零副作用

mdfind "kMDItemCFBundleIdentifier == '<bid>'" 命中则输出路径,未装则 stdout 为空。完全不触发 LaunchServices launch 逻辑。

边界情况

  • 本来就要打开 app:用 open -b <bid> <path>,这才是正确的 launch 语义,没有问题。
  • Spotlight 被用户关闭mdfind 会退化为「未装」结果。/Applications 默认被索引,实践中可靠;若需 100% 不依赖 Spotlight,可改为遍历 /Applications~/Applications/System/Applications 下的 *.app,用 defaults read <app>/Contents/Info CFBundleIdentifier 读 bundle id 比对——同样不启动 app。

一句话外卖

在 macOS 上「检测 app 是否安装」时,osascript path to application id 是带副作用的 launch,mdfind 查 Spotlight 索引才是无副作用的纯读取。