在Mac app上执行AppleScript

Xcode,iOS,定位

Posted by Karim on November 10, 2019

前言

前段时间我开发了虚拟定位,一脚踩进了AppleScript的坑里无法自拔。

AppleScript

AppleScript它是什么?

AppleScript它是苹果提供在Mac OS上实现自动化的脚本语言,语法相对简单、易懂,类似英语语法。下面这段代码,你可以通过 Mac 自带的脚本编辑器运行:

运行后它会打开 iTunes :

tell application "iTunes"
	play
end tell

这三行可以简单的理解为:

告诉 iTunes
    播放
结束

tell application "xxx" 如果你是想让微信 App做点什么,就是tell application "微信",而play这个事件,是iTunes独有的,怎么能知道app特有的事件?你可以通过脚本编辑器的导航栏中按照以下步骤找到:

然后选择iTunes:

这里可以看到关于play事件的一些描述。
在编写Mac app之前,可以先通过脚本编辑器把我们想实现的功能调试好,然后再把脚本移到Mac app上,确保脚本是正确的,而不是因为App沙盒、权限等问题引起。

System Events

上面已经说到,可以通过tell application "xxx"这种方式去调用 App 允许的行为,如果你看完了上面的介绍,打算自己动手去做一些 App 自动化的事情,你会发现 App 提供的事件很少,甚至有可能没有,所以接下来讲的是 System Events , System Events 为我们提供了系统事件,允许我们模拟用户点击行为,可以说所有用户界面相关的操作,你都可以通过 System Events 去进行自动化操作。

下面这一段就是通过 System Events 启动 Safari 然后通过菜单栏的选项卡,新建标签页。

tell application "Safari"
	# 打开Safari
	activate
	tell application "System Events"
		tell process "Safari"
			# 点击Safari菜单栏-文件-新建标签页
			click menu item "新建标签页" of menu 1 of menu bar item "文件" of menu bar 1
		end tell
	end tell
end tell

Mac App 调用 AppleScript 的方式

通过 NSAppleScript 运行以下代码:

        let source = """
tell application "Safari"
    # 打开Safari
    activate
    tell application "System Events"
        tell process "Safari"
            # 点击Safari菜单栏-文件-新建标签页
            click menu item "新建标签页" of menu 1 of menu bar item "文件" of menu bar 1
        end tell
    end tell
end tell
"""
        let script = NSAppleScript(source: source)!
        var error : NSDictionary?
        script.executeAndReturnError(&error)
        print(error)

预期结果是正常的,但是会发现打印出了这样的错误:

2019-09-22 21:15:30.094229+0800 SwiftAppleScript[1453:42847] skipped scripting addition "/Library/ScriptingAdditions/SASyphonInjector.osax" because it is not SIP-protected.
Optional({
    NSAppleScriptErrorAppName = "System Events";
    NSAppleScriptErrorBriefMessage = "Not authorized to send Apple events to System Events.";
    NSAppleScriptErrorMessage = "Not authorized to send Apple events to System Events.";
    NSAppleScriptErrorNumber = "-1743";
    NSAppleScriptErrorRange = "NSRange: {168, 69}";
})

这个地方非常的坑,如果没有打印error,在没有打开App Sanbox的情况下,是有可能预期效果是正常的,Safari能正常打开然后新建标签页。

如果之前是做iOS开发的,对这个错误应该会很自然的有头绪,应该就是一些权限问题,直接在Info.plist上就能找到一个看起来是这个权限的字段,Privacy - AppleEvents Sending Usage Description,把这个字段加上后,重新运行,发现多出了这个弹窗:
如果能够正常运行,那恭喜你,说明你没有打开Capabilities->App Sanbox

为什么要打开App Sanbox?

Mac OSX在10.6系统之后,为了防止恶意的App通过系统漏洞攻击系统,获取控制权限,要求上架的所有的App,都必须要开启沙盒。不要以为App安全离我们很远,Mac App一不留神就有可能被利用,System Events 就可以做到这个事情。

讲回正题,当我们把 App Sanbox 打开后,新的报错又来了:

Optional({
    NSAppleScriptErrorAppName = Safari;
    NSAppleScriptErrorBriefMessage = "Application isn’t running.";
    NSAppleScriptErrorMessage = "Safari got an error: Application isn’t running.";
    NSAppleScriptErrorNumber = "-600";
    NSAppleScriptErrorRange = "NSRange: {45, 8}";
})

这次是提示 Safari 没有运行,我们的代码里已经加了让 Safari 唤醒的功能了,这说明这段代码没办法执行,既然没有解决,只能靠 google 了。

在查阅了一大波资料后,在 App.entitlements 加上这个 com.apple.security.temporary-exception.apple-events 权限就可以了。

到这里为止,一切正常的。
在虚拟定位加入这个权限的第一个版本,上架也非常顺利,但是这之后更新的一个版本,仅涉及一些UI细节的修复,收到苹果拒审的邮件,内容如下:

我知道你要使用 com.apple.security.temporary-exception.apple-events 干嘛,
但是我不能给你用,要么你就去掉这个功能,要么你要自己找别的方式去实现。

既然不给我用,为什么上个版本还能给我上架成功???

这个方法不行,只能另外再找方法了。

从苹果的文档可以找到怎么通过 AppleScript 调用其他 App 功能的资料,苹果在文档中给我们举了个例子,如果要用 Mail.app 的邮件编写功能,可以在 App.entitlements 加上一下的权限:

<key>com.apple.security.scripting-targets</key>
    <dict>
        <key>com.apple.mail</key>
        <array>
            <string>com.apple.mail.compose</string>
        </array>
    </dict>

这样就可以使用com.apple.mail.compose这个访问组下的权限了,这个访问组下的权限,可以通过以下的命令看得到:

sdef /Applications/Mail.app > ~/Desktop/mail.xml

但是 Xcode 本身并没有提供一个可以修改定位的访问组权限给到我们,所以并不能使用以上的方法去解决。

这时候不禁会想到,System Event 能做什么呢,为什么苹果会不允许我们使用这个权限?对此我又对这个权限有了一个更深入的了解。

思考

System Events其实是一个系统App,提供控制Mac OSX GUI和应用之间的交互,在这个路径下你可以找到/System/Library/CoreServices/System Events.app
通过System Events,我们可以做到以下这些东西:

  • 获取正在运行的App列表
  • (得到/点击)任意一个正在运行App中的UI元素
  • (打开/移动/删除)文件

等等…这不就是等于能远程监控和控制别人的电脑吗?
所以苹果不开放这个权限也是有原因,既然这样,也只能找别的方法看看了。
苹果不允许开发者动态调用AppleScript,但是可以调用 App 沙盒里的 Application Scripts 目录下的文件。 最终虚拟定位采用的方案也是这个,在Info.plist加上NSAppleEventsUsageDescription,并且通过弹窗引导用户把控制Xcode的脚本保存在Application Scripts目录下:

FileManager.default.urls(for: .applicationScriptsDirectory, in: .userDomainMask)

最终,总算顺利上架成功了。

总结

App的沙盒机制,给用户带来了安全保障,但是也给开发者带来了很多开发上的困难,同时也一定程度上会增加用户在使用上带来不好的体验。
如果你也想开发一款App并且上架AppStore,但是要依赖一个不是自己开发的App,大部分非Apple的App,都不会提供AppleScript的相关接口给到第三方使用,把applescript放在Application Scripts目录下使用是目前唯一能上架成功的方式。


请保持转载后文章内容的完整,以及文章出处。本人保留所有版权相关权利。

分享到: