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。
实测受影响的有 com.todesktop.*(Cursor)、com.cmuxterm.app(cmux)等 todesktop/Electron 打包应用;而 com.apple.*、com.microsoft.VSCode、com.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 "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 索引才是无副作用的纯读取。