*如果應用程序偶爾需要管理員權限執(zhí)行命令旱眯,可使用AppleScript接口
*但是如果應用程序需要頻繁使用管理員權限執(zhí)行命令趴拧,這時候就需要用到SMJobBless獲取長時間可用的權限了
*由于此方法不能使用App Sandbox奶卓,可能會影響上架App Store
1.添加Target,作為后臺程序(Helper)
File -> New -> Target... -> macOS -> Command Line Tool
命名格式一般為 com.yourcompany.mainapp.helper
設置Bundle Identifier和Targer名一致,并且建議重命名一下Helper目錄(例子里把"com.ljq.SMJobBlessApp.CommandHelper"目錄命名為"CommandHelper"了)
2.創(chuàng)建plist文件
選中Helper的目錄玻孟,F(xiàn)ile -> New -> File...(或Commmand + N),創(chuàng)建Property List文件鳍征,命名為"CommandHelper-Info"
選中CommandHelper-Info.plist右鍵 -> Open As -> Soure Code黍翎,復制粘貼以下內(nèi)容并修改:
CFBundleIdentifier:Helper程序的Bundle Identifier
CFBundleName:Helper程序的名稱
SMAuthorizedClients里的"com.ljq.SMJobBlessApp"為主Target的Bundle Identifier
SMAuthorizedClients里的"GN79NMG846"為開發(fā)者賬號的Team Identifier
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.ljq.SMJobBlessApp.CommandHelper</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>CommandHelper</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>SMAuthorizedClients</key>
<array>
<string>anchor apple generic and identifier "com.ljq.SMJobBlessApp" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "GN79NMG846")</string>
</array>
</dict>
</plist>
如圖所示:
再次選中Helper的目錄,創(chuàng)建Property List文件艳丛,此次命名為"CommandHelper-Launchd"匣掸,并且和上面一樣復制粘貼以下內(nèi)容并修改:
Label:Helper程序的Bundle Identifier
MachServices:用于XPC通信服務
(其他為可選參數(shù))
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ljq.SMJobBlessApp.CommandHelper</string>
<key>RunAtLoad</key>
<true/>
<key>MachServices</key>
<dict>
<key>com.ljq.SMJobBlessApp.CommandHelper</key>
<true/>
</dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
</dict>
</plist>
如圖所示:
3.配置主Target
主Target -> General -> Frameworks... 趟紊,添加SystemConfiguration.framework與Security.framework
主Target -> Signing & Capabilities ,移除App Sandbox(假如有)
主Target -> Build Phases -> Dependencies碰酝,添加Helper
主Target -> Build Phases -> Copy Bundle Resources添加"CommandHelper-Info.plist"和"CommandHelper-Launchd.plist"
主Target -> Build Phases 左上角+號 New Copy Files Phase
Destination 改為 Wrapper霎匈,Subpath 改為 "Contents/Library/LaunchServices" ,并在下面添加Helper程序
選中主Target的Info.plist文件送爸,修改"com.ljq.SMJobBlessApp.CommandHelper"和"GN79NMG846"并添加以下內(nèi)容:
<key>SMPrivilegedExecutables</key>
<dict>
<key>com.ljq.SMJobBlessApp.CommandHelper</key>
<string>anchor apple generic and identifier "com.ljq.SMJobBlessApp.CommandHelper" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "GN79NMG846")</string>
</dict>
如圖所示:
注:如果出問題铛嘱,可以嘗試移除.entitlements文件試試,非必須的
假如要移除entitlements文件袭厂,還需要在主Target -> Build Settings -> Signing -> Code Signing Entitlements 刪除其內(nèi)容
4.配置Helper Target
Helper Target -> Build Settings -> Linking -> Other Linker Flags墨吓,修改以下內(nèi)容復制進去(點擊+直接粘貼長文本,回車后會自動換行)纹磺,下面兩個"xxx.plist"路徑要修改為項目實際的路徑帖烘,其他不用改
-sectcreate __TEXT __info_plist "$(SRCROOT)/CommandHelper/CommandHelper-Info.plist" -sectcreate __TEXT __launchd_plist "$(SRCROOT)/CommandHelper/CommandHelper-Launchd.plist"
如圖所示:
Helper Target -> Build Settings -> Packaging -> Info.plist File,修改以下路徑復制進去
$(SRCROOT)/CommandHelper/CommandHelper-Info.plist
如圖所示:
5.1.編寫安裝代碼(以下內(nèi)容建議直接查看demo源碼)
修改ViewController.m橄杨,運行時如果彈窗內(nèi)容為"The Helper Tool is available!"秘症,表示Helper程序已成功安裝,如果為其他則表示安裝失敗式矫,建議檢查以上流程
#import "ViewController.h"
#import <ServiceManagement/ServiceManagement.h>
#import <Security/Authorization.h>
@interface ViewController (){
AuthorizationRef _authRef;
}
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self initHelper];
});
}
- (void)initHelper
{
BOOL isAvailable = YES;
NSError *error = nil;
NSString *installedPath = [NSString stringWithFormat:@"/Library/PrivilegedHelperTools/%@", @"com.ljq.SMJobBlessApp.CommandHelper"];
if ([[NSFileManager defaultManager] fileExistsAtPath:installedPath] == NO) {
OSStatus status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &self->_authRef);
if (status == errAuthorizationSuccess) {
isAvailable = [self blessHelperWithLabel:@"com.ljq.SMJobBlessApp.CommandHelper" error:&error];
} else {
self->_authRef = NULL;
isAvailable = NO;
assert(NO);/* AuthorizationCreate really shouldn't fail. */
}
}
NSAlert *alert = [NSAlert new];
alert.informativeText =
isAvailable ?
@"The Helper Tool is available!" :
[NSString stringWithFormat:@"Something went wrong! code:%ld %@",error.code,error.localizedDescription];
[alert beginSheetModalForWindow:self.view.window completionHandler:nil];
}
- (BOOL)blessHelperWithLabel:(NSString *)label error:(NSError **)errorPtr
{
BOOL result = NO;
NSError * error = nil;
AuthorizationItem authItem = { kSMRightBlessPrivilegedHelper, 0, NULL, 0 };
AuthorizationRights authRights = { 1, &authItem };
AuthorizationFlags flags = kAuthorizationFlagDefaults |
kAuthorizationFlagInteractionAllowed |
kAuthorizationFlagPreAuthorize |
kAuthorizationFlagExtendRights;
/* Obtain the right to install our privileged helper tool (kSMRightBlessPrivilegedHelper). */
OSStatus status = AuthorizationCopyRights(self->_authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL);
if (status != errAuthorizationSuccess) {
NSString *errMsg = (__bridge_transfer NSString *)SecCopyErrorMessageString(status, NULL);
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:@{NSLocalizedDescriptionKey:errMsg}];
} else {
CFErrorRef cfError;
/* This does all the work of verifying the helper tool against the application
* and vice-versa. Once verification has passed, the embedded launchd.plist
* is extracted and placed in /Library/LaunchDaemons and then loaded. The
* executable is placed in /Library/PrivilegedHelperTools.
*/
result = (BOOL)SMJobBless(kSMDomainSystemLaunchd, (__bridge CFStringRef)label, self->_authRef, &cfError);
if (!result) {
error = CFBridgingRelease(cfError);
}
}
if (!result && (errorPtr != NULL) ) {
assert(error != nil);
*errorPtr = error;
}
return result;
}
@end
5.2.編寫XPC通信代碼
光是安裝Helper程序并不算得上是一個完整的代碼乡摹,此時需要使用到XPC接口來調(diào)用Helper程序
選中Helper的目錄添加頭文件(File -> New -> File... -> macOS -> Header File),命名為CommandHelperProtocol.h衷佃,并添加以下內(nèi)容
#import <Foundation/Foundation.h>
@protocol CommandHelperProtocol
- (void)executeCommand:(NSString *)command reply:(void(^)(int result))reply;
@end
再在此目錄添加一個類(File -> New -> File... -> macOS -> Cocoa Class)趟卸,命名為CommandHelper,并添加以下內(nèi)容
CommandHelper.h
#import <Foundation/Foundation.h>
#import "CommandHelperProtocol.h"
@interface CommandHelper : NSObject
- (void)run;
@end
CommandHelper.m
#import "CommandHelper.h"
@interface CommandHelper () <NSXPCListenerDelegate>
@property (nonatomic, strong) NSXPCListener *listener;
@end
@implementation CommandHelper
- (instancetype)init
{
self = [super init];
if (self) {
self.listener = [[NSXPCListener alloc] initWithMachServiceName:@"com.ljq.SMJobBlessApp.CommandHelper"];
self.listener.delegate = self;
}
return self;
}
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(CommandHelperProtocol)];
newConnection.exportedObject = self;
[newConnection resume];
return YES;
}
- (void)executeCommand:(NSString *)command reply:(nonnull void (^)(int))reply
{
reply(system(command.UTF8String));
}
- (void)run
{
[self.listener resume];
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
@end
最后在Helper目錄下的main.m中添加以下內(nèi)容
#import <Foundation/Foundation.h>
#import "CommandHelper.h"
int main(int argc, const char * argv[]) {
[[CommandHelper new] run];
return 0;
}
測試是否可用(更新了Helper代碼后氏义,需要在/Library/PrivilegedHelperTools/目錄下刪除此項目的Helper程序)
往ViewController.m添加以下代碼锄列,測試設置Wi-Fi的DNS命令
...
#import "CommandHelperProtocol.h"
...
@implementation ViewController
....
- (void)initHelper
{
...
if (isAvailable) {
// Reset DNS: "networksetup -setdnsservers Wi-Fi Empty"
[self executeCommand:@"networksetup -setdnsservers Wi-Fi 8.8.8.8" reply:^(int result) {
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert *alert = [NSAlert new];
alert.informativeText =
result == errSecSuccess ?
@"Execute succeeded!" :
[NSString stringWithFormat:@"Execute failed: %d",result];
[alert beginSheetModalForWindow:self.view.window completionHandler:nil];
});
}];
} else {
NSAlert *alert = [NSAlert new];
alert.informativeText = [NSString stringWithFormat:@"Something went wrong! code:%ld %@",error.code,error.localizedDescription];
[alert beginSheetModalForWindow:self.view.window completionHandler:nil];
}
}
- (void)executeCommand:(NSString *)command reply:(void(^)(int result))reply
{
NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.ljq.SMJobBlessApp.CommandHelper"
options:NSXPCConnectionPrivileged];
xpcConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(CommandHelperProtocol)];
xpcConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(CommandHelperProtocol)];
xpcConnection.exportedObject = self;
[[xpcConnection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
// 無法連接XPC服務、Helper進程已退出或已崩潰
NSLog(@"Get remote object proxy error: %@",error);
reply((int)error.code);
}] executeCommand:command reply:reply];
[xpcConnection resume];
}
...
@end
假如程序順利執(zhí)行惯悠,就會把Wi-Fi下的DNS設置為8.8.8.8
6.卸載
新建一個'Uninstall.sh'腳本到主Target的目錄下邻邮,添加以下內(nèi)容并修改"com.ljq.SMJobBlessApp.CommandHelper"
#!/bin/bash
sudo launchctl unload /Library/LaunchDaemons/com.ljq.SMJobBlessApp.CommandHelper.plist
sudo rm /Library/LaunchDaemons/com.ljq.SMJobBlessApp.CommandHelper.plist
sudo rm /Library/PrivilegedHelperTools/com.ljq.SMJobBlessApp.CommandHelper
執(zhí)行'Uninstall.sh'腳本
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"Uninstall" ofType:@"sh"];
NSString *shellScript = [NSString stringWithFormat:@"out=`sh \\\"%@\\\"`",scriptPath];
NSString *script = [NSString stringWithFormat:@"do shell script \"%@\" with administrator privileges", shellScript];
NSDictionary *errorInfo = nil;
NSAppleScript *appleScript = [[NSAppleScript new] initWithSource:script];
NSAppleEventDescriptor *result = [appleScript executeAndReturnError:&errorInfo];
if (errorInfo == nil || errorInfo.count == 0) {
NSLog(@"Uninstall succeeded!!!");
} else {
NSLog(@"Uninstall failed! script result: %@ , error: %@", [result stringValue], errorInfo.description);
}