macOS沙盒里,如何安全又优雅地运行沙盒外脚本?
参考自:https://github.com/lexrus/SwiftyMenu
1. 问题背景
在开发 macOS 应用时,苹果的沙盒(Sandbox)安全机制会带来一个挑战:为了保证用户安全,应用不能随意执行外部的脚本文件。本文档提供了一种可靠、灵活的解决方案。
2. 核心思路与流程
这个方案的核心是使用一个统一的辅助脚本 (runner.sh
) 作为所有脚本任务的“调度中心”。应用本身只和这个调度中心打交道,由它来分发和执行具体的任务。
我们依赖苹果官方推荐的沙盒内执行工具 NSUserUnixTask
来调用我们的调度中心。
总流程如下:
1
| App -> NSUserUnixTask -> runner.sh -> 最终要执行的脚本或命令
|
3. 核心调度脚本 runner.sh
详解
runner.sh
是本方案的基石。理解它的工作原理至关重要。
3.1. 脚本源码
该脚本的功能是接收一系列参数,将第一个参数作为要执行的命令,并将其余所有参数传递给该命令。
1 2 3 4 5 6 7 8 9
| #!/bin/sh
action="$1"
shift 1
"$action" "$@"
|
3.2. 工作原理
runner.sh
巧妙地利用 shift
命令分离了“执行者”和“执行参数”。
例如,当 Swift 代码通过 NSUserUnixTask
传递以下参数数组时:
["/bin/sh", "/path/to/temp_script.sh", "arg1"]
-
action="$1"
:action
变量被赋值为 "/bin/sh"
。
-
shift 1
:参数列表变为 ["/path/to/temp_script.sh", "arg1"]
。
-
"$action" "$@"
:最终执行的命令被解析为 /bin/sh "/path/to/temp_script.sh" "arg1"
。
这样,runner.sh
就实现了作为一个通用调度代理的功能。
4. Swift 实现步骤
现在,让我们看看如何在 Swift 代码中实现这个流程。
4.1. 第一步:定义 runner.sh
为确保安全和一致性,我们将 runner.sh
的内容直接定义为 Swift 字符串。当然,您也可以将其作为文件打包在 App 中。
1 2 3 4 5 6 7 8 9 10 11
| public enum PersistentHelperScript: String { case runner = """ #!/bin/sh # 第一个参数是要执行的命令或脚本 # 后续所有参数都传递给该命令 action="$1" shift 1 "$action" "$@" """ }
|
4.2. 第二步:安装 runner.sh
在 App 首次启动时,需将上述脚本内容写入磁盘上的固定位置,并赋予执行权限。这个位置通常在 ~/Library/Application Scripts/
目录下,沙盒应用对此目录有权访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import Foundation
func installRunnerScript() { guard let runnerURL = getRunnerURL() else { return } let scriptContent = PersistentHelperScript.runner.rawValue let fileManager = FileManager.default let dirPath = runnerURL.deletingLastPathComponent() if !fileManager.fileExists(atPath: dirPath.path) { try? fileManager.createDirectory(at: dirPath, withIntermediateDirectories: true) }
if !fileManager.fileExists(atPath: runnerURL.path) { guard let data = scriptContent.data(using: .utf8) else { return } fileManager.createFile(atPath: runnerURL.path, contents: data, attributes: [ .posixPermissions: 0o755 ]) } }
|
4.3. 第三步:创建统一的执行函数
应用中所有的脚本任务,都通过下面这个 execute
函数来统一调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func execute(arguments: [String], completion: ((Error?) -> Void)? = nil) { guard let runnerURL = getRunnerURL() else { return }
guard FileManager.default.isExecutableFile(atPath: runnerURL.path) else { completion?() return }
do { let task = try NSUserUnixTask(url: runnerURL) task.execute(withArguments: arguments) { error in DispatchQueue.main.async { completion?(error) } } } catch { completion?(error) } }
|
4.4. 第四步:不同场景的调用方法
通过构造不同的参数列表,我们可以指挥 runner.sh
完成各种任务。
(注:以下示例依赖一个辅助函数 createTempFile
来创建临时脚本,请参考您提供的原文实现该函数。)
场景 A:执行一个基本的 Shell 脚本
目标:运行一个临时的 Shell 脚本文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let scriptContent = "echo '任务完成.'" guard let tempScriptPath = createTempFile(content: scriptContent) else { return }
let arguments = [ "/bin/sh", tempScriptPath ]
execute(arguments: arguments) { error in try? FileManager.default.removeItem(atPath: tempScriptPath) if error == nil { print("Shell 脚本执行成功。") } }
|
场景 B:执行 AppleScript
目标:运行一段临时的 AppleScript。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| let scriptContent = "display dialog \"来自 AppleScript 的消息。\"" guard let tempAppleScriptPath = createTempFile(content: scriptContent, ext: "applescript") else { return }
let arguments = [ "/usr/bin/osascript", tempAppleScriptPath ]
execute(arguments: arguments) { error in try? FileManager.default.removeItem(atPath: tempAppleScriptPath) if error == nil { print("AppleScript 执行成功。") } }
|
场景 C:执行复杂的终端命令
目标:在 iTerm2 中运行一条命令。对于这种复杂操作,我们可以在运行时动态创建一个临时的辅助脚本来处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| let terminalExpertScriptContent = """ #!/usr/bin/env bash 这里定义一些复杂的脚本,都是没问题的 """
guard let tempExpertPath = createTempFile(content: terminalExpertScriptContent) else { return }
let arguments = [ "/bin/bash", tempExpertPath, "com.googlecode.iterm2", "/Users/Shared", "echo '复杂任务执行成功!'" ]
execute(arguments: arguments) { error in try? FileManager.default.removeItem(atPath: tempExpertPath) if error == nil { print("终端命令执行成功。") } }
|
5. 结论与优势总结
这个方案通过 App -> NSUserUnixTask -> runner.sh -> 用户脚本
的链条,在完全遵循 macOS 沙盒规则的前提下,实现了强大而灵活的脚本执行功能。
其优点非常明确:
- 最小化安装:只在用户系统上安装一个必要的
runner.sh
文件。
- 高度灵活:既能直接调用系统命令,也能通过创建临时脚本来处理任意复杂的逻辑。
- 安全可靠:执行流程清晰,入口统一,临时文件用完即删,非常符合沙盒的设计理念。