应用中有些shell
命令可能需要sudo
,也就是需要root
权限来完成的,比如:
$ sudo spctl --master-disable # 开启软件安装的未知来源
碰到这类情况,直接使用是肯定不行的,那么我们该如何处理?
方案一
AuthorizationExecuteWithPrivileges()
这个函数是Security.framework中的一员,使用非常方便。而且还有一个封装得非常好的库 STPrivilegedTask,用法和NSTask
几乎一样。
但是,虽然这个方法简单易用,但根据官方文档所示,AuthorizationExecuteWithPrivileges 函数的适用限制在macOS 10.1–10.7,也就是说早在OS X Lion 的时候就开始被Deprecated
了,现在居然还能用也挺令人迷惑的。
经测试,在新版的系统使用该函数在某些情况下会提权失败,为了兼容以后的系统,不推荐使用这种方法,在本文也不做过多介绍。
优点:
- 上手简单,使用方便
缺点:
Deprecated
,不稳定、兼容性差
方案二
注册 LaunchdDaemon
注册LaunchdDaemon
的常用方法是通过launchd
工具加载一个与Daemon
程序相关的标准的plist
文件,由于launchd
需要高权限运行,所以启动的子程序自然也是高权限运行。这个过程一般放在PKG 的安装脚本中完成,但当前越来越多的软件摒弃了PKG的打包方式,而是直接选择了打包成App来提升用户体验,此时安装辅助工具的工作也就要放到 App 运行过程中了。ServiceManagement
的 API 可以完成这样的操作。
通过ServiceManagement
注册LaunchdDaemon
是苹果推荐的一种提权方式,官方也提供了一个SMJobBless的 Demo,需要用苹果开发者账号编译。
具体思路是使用Security.framework
和ServiceManagement.framework
两个库,把需要root权限的操作封装成一个 Target,作为项目的子程序,把该子程序注册LaunchdDaemon
。
成为LaunchdDaemon
后:
- 子进程会被放在 /Library/PrivilegedHelperTools
- 相应的plist配置文件会被放在 /Library/LaunchDaemons ,
Launchd
加载该子进程会需要读取该配置文件
过程较为复杂,可以先学习一下 Demo,然后尝试给自己的应用(这里以项目 MyApp 举例)添加:
-
关闭 App Sandbox
-
拷贝 Demo 中的 SMJobBlessUtil.py 到项目根目录
-
创建一个新 Target,选择 Command Line Tool,命名为 MyAppHelper
-
创建 MyAppHelper-Info.plist 文件并配置必要参数
-
创建 MyAppHelper-Launchd.plist 文件并配置必要参数
-
在 MyAppHelper Target 中配置 Other Linker Flags:
-sectcreate TEXT info_plist MyAppHelper/MyAppHelper-Info.plist
-sectcreate TEXT launchd_plist MyAppHelper/MyAppHelper-Launchd.plist -
在 MyAppHelper Target 中配置 Product Module Name 和 Product Name 为 com.yourcompany.MyApp.Helper
-
选择 MyApp Target 配置 Copy Files,路径 Contents/Library/LaunchServices
-
生成签名参数(参考 Demo 中的 ReadMe.txt)
-
写入授权代码(参考 Demo 中的 SMJobBlessAppController.m)
-
运行测试,如果授权成功即可开始修改具体需要授权的部分代码行为
优点:
- 目前主流的提权方案
- 将高权限任务封装到独立的子程序按需调用,不会让整个程序处于高权限的状态,相对安全
- 子程序可实现开机启动、长驻后台、高权限的需求
缺点:
- 使用起来较为繁琐
- 弹出认证对话框的提示内容是”需要安装帮助程序”,这样的提示不够友好
LaunchdDaemon
及其配置文件是需要安装到 /Library 下的,可能存在卸载残留的问题
方案三
AppleScript
do shell script "..." with administrator privileges
省略号部分填入shell
脚本即可。
AppleScript
脚本在代码中有两种执行方式:
直接用传统的Process
执行 /usr/bin/osascript -e "do shell ..."
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", "do shell script \"...\" with administrator privileges"]
task.launch()
通过NSAppleScript执行
let script = "do shell script \"...\" with administrator privileges"
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: script) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
print(output.stringValue)
}
使用传统的Process
方法要注意:
- 在所有shell执行完成后才会把
stdout
返回,因此当启动的是Daemon
进程,就算使用Process
的NSFileHandleDataAvailable Notification
,也无法把stdout
分次读取出来,不能做到与用户进行良好的交互。 - 这种方法的认证窗口提示信息是osascript wants to make changes.,对于小白用户来说会不会有一种这样的感觉,我明明安装的是 XXX.app,怎么来了一个
osascript
让我输入密码?- 解决方法:添加with prompt "xxx"参数自定义提示信息
NSAppleScript
方法使用时认证窗口的提示信息是“APP_NAME wants to make changes.”,这样的提示较为友好,但也要注意:
NSAppleScript
执行Daemon
进程的话会直到Daemon
退出才退出,即会一直占用线程- 解决方法:在子线程使用
NSAppleScript
,以免阻塞UI
线程
- 解决方法:在子线程使用
优点:
- 比上述“注册 LaunchdDaemon”的方法实现起来简单很多
- 不用担心卸载残留的问题,因为全部内容都存放于 xxx.app
本文为原创文章,版权归字节时代所有,欢迎分享本文,转载请保留出处!