4

我正在用 Swift 编写一个 macOS 应用程序,它需要一个特权帮助工具——希望提升不是必需的,但看起来它是.

我发现这个优秀的示例应用程序特别适合这种情况。我已经设法将它的代码移植到我自己的应用程序中,但我被困在需要检查是否安装了帮助工具的地方,如果没有,请使用SMJobBless()和朋友安装它。

运行示例应用程序时,如果未安装帮助工具,应用程序将停留在以下屏幕:

在此处输入图像描述

需要明确的是,通过阅读代码,我认为它应该在某个时候将标签更新为“Helper Installed:No”,但这似乎没有发生。

如果我单击“安装助手”,这就是结果。

在此处输入图像描述

从现在开始,除非我手动删除帮助工具,否则重新运行应用程序将显示此屏幕并显示“已安装帮助:是”。

在此示例情况下,此行为可能没问题,用户必须手动单击“安装助手”按钮。但是,在我的应用程序中,如果尚未安装,我希望它自动请求安装帮助工具。如果它已经安装,我不想浪费用户的时间再次请求他们的密码。

我认为这很简单:如果辅助工具不可用,在连接到它的过程中的某个地方,会发生错误,这是我请求安装该工具的触发器。如果没有发生错误,则假定该工具已安装。

这是我为通过 XPC 连接到帮助工具而编写的代码:

var helperConnection: NSXPCConnection?
var xpcErrorHandler: ((Error) -> Void)?
var helper: MyServiceProtocol?

// ...

helperConnection = NSXPCConnection(machServiceName: MyServiceName, options: .privileged)
helperConnection?.remoteObjectInterface = NSXPCInterface(with: MyServiceProtocol.self)
helperConnection?.resume()

helperConnection?.interruptionHandler = {
    // Handle interruption
    NSLog("interruptionHandler()")
}

helperConnection?.invalidationHandler = {
    // Handle invalidation
    NSLog("invalidationHandler()")
}

xpcErrorHandler = { error in
   NSLog("xpcErrorHandler: \(error.localizedDescription)")
}

guard
    let errorHandler = xpcErrorHandler,
    let helperService = helperConnection?.remoteObjectProxyWithErrorHandler(errorHandler) as? MyServiceProtocol
    else {
        return
}

helper = helperService

如果未安装帮助工具,则运行此代码不会产生错误或NSLog()输出。如果之后我通过 XPC(使用helper?.someFunction(...))调用函数,则什么也不会发生——我还不如与/dev/null.

现在,我只能摸不着头脑,寻找一种技术来检测是否安装了该工具。示例应用程序解决问题的方法是添加一个getVersion()方法;如果它返回某些内容,“Install Helper”将显示为灰色,并且标签更改为“Helper Installed: Yes”。

我想通过在我的工具中编写一个立即返回的简单函数来扩展这个想法,并在主应用程序中使用超时——如果我在代码超时之前没有得到结果,那么帮助工具可能不会安装。我发现这是一个 hacky 解决方案 - 例如,如果辅助工具(按需启动)启动时间过长,比如说因为计算机很旧并且用户正在运行 CPU 密集型的东西,该怎么办?

我看到了其他替代方法,例如在预期的位置 (/Library/PrivilegedHelperTools/Library/LaunchDaemons) 中查看文件系统,但是这个解决方案再次让我感到不满意。

我的问题:有没有办法明确检测特权 XPC 辅助工具是否在另一端监听?

我的环境:macOS Mojave 10.14.2、Xcode 10.1、Swift 4.2。

4

3 回答 3

3

如果二进制文件存在(在 /Library/PrivilegedHelperTools 中以及 plist 是否存在于 /Library/LaunchDaemons 中),我会检查文件系统。然后,您可以联系 XPC 服务并调用一种 ping 函数,该函数会回答该服务是否已启动并正在运行。

只是我的 2 cts,

罗伯特

于 2019-01-19T14:02:09.917 回答
3

由于您创建了帮助工具,只需添加一个 XPC 消息处理程序即可报告您的工具的状态。当您启动时,连接并发送该消息。如果其中任何一个失败,则您的工具未正确安装(或没有响应)。

在我的代码中,我的所有 XPC 服务(包括我的特权助手)都采用了用于测试和操作安装的基本协议:

@protocol DDComponentInstalling /*<NSObject>*/

@required
- (void)queryBuildNumberWithReply:(void(^_Nonnull)(UInt32))reply;

@optional
- (void)didInstallComponent;
- (void)willUninstallComponent;

返回一个整数,queryBuildNumberWithReply:描述组件的版本号:

- (void)queryBuildNumberWithReply:(void(^)(UInt32))reply
{
    reply(FULL_BUILD_VERSION);
}

如果消息成功,我会将返回的值与应用程序中的内部版本号常量进行比较。如果它们不匹配,则该服务是较旧/较新的版本,需要更换。我的产品每次公开发布时,这个常数都会增加。

我使用的代码如下所示:

- (BOOL)verifyServiceVersion
{
    DDConnection* connection = self.serviceConnection;
    id<DDComponentInstalling> proxy = connection.serviceProxy;  // get the proxy (will connect, as needed)
    if (proxy==nil)
        // an XPC connection could not be established or the proxy object could not be obtained
        return NO;  // assume service is not installed

    // Ask for the version number and wait for a response
    NSConditionLock* barrierLock = [[NSConditionLock alloc] initWithCondition:NO];
    __block UInt32 serviceVersion = UNKNOWN_BUILD_VERSION;
    [proxy queryBuildNumberWithReply:^(UInt32 version) {
        // Executes when service returns the build version
        [barrierLock lock];
        serviceVersion = version;
        [barrierLock unlockWithCondition:YES];  // signal to foreground thead that query is finished
        }];
    // wait for the message to reply
    [barrierLock lockWhenCondition:YES beforeDate:[NSDate dateWithTimeIntervalSinceNow:30.0];
    BOOL answer = (serviceVersion==FULL_BUILD_VERSION); // YES means helper is installed, alive, and correct version
    [barrierLock unlock];

    return answer;
}

请注意,这DDConnection是一个围绕 XPC 连接的实用程序包装器,并且该barrierLock技巧实际上被封装在一个共享方法中——所以我最终不会一遍又一遍地编写它——但为了演示的目的,这里将其展开。

我还需要处理安装前/安装后/升级后的问题,因此我的所有组件都实现了一个可选的didInstallComponent方法willUninstallComponent,我在安装新的帮助程序后立即发送,或者在我计划卸载或替换已安装的帮助程序之前发送。

于 2019-01-21T17:38:12.190 回答
1

背景

实际上可以避免等待工具响应的超时实际上,如果未启用该工具(请参阅此处erikberglund/SwiftPrivilegedHelper),您引用的示例确实会在“Helper Installed”文本字段旁边打印“No”一词,这是异步的,但实际上是即时的。

但是,我处于独特的位置,恰好遇到了您在我自己的特权助手实现中描述的问题,但该SwiftPrivilegedHelper示例完全适合我。当未安装该工具时,在前者remoteObjectProxyWithErrorHandler中永远不会调用错误处理程序,而在后者中却是。

因为我有一个关于您的问题的示例以及一个工作示例,所以我相信我已经设法确定了根本原因:

根本原因

至少在我的情况下,我没有完全卸载 helper tool

在某些时候,我已经删除了它plist/Library/LaunchDaemons工具本身,/Library/PrivilegedHelperTools但我想我没有运行sudo launchctl unload /Library/LaunchDaemons/com.example.foo.plist

我跑的时候它仍然列出来sudo launchctl list,​​或者sudo launchctl print system/com.example.foo.plist

在这些情况下,对该工具的调用显然不会成功(因为该工具未安装)但不会失败(因为 launchctl 认为它安装)。

解决方案

可以通过以下两种方式之一进行正确卸载:

  1. sudo launchctl remove com.example.foo(注:不是 system/com.example.foo
  2. sudo launchctl unload /Library/LaunchDaemons/com.example.foo.plist(这要求 plist 仍然存在于磁盘上)。

如果正确卸载,sudo launchctl print system/ca.example.foo应打印:

Bad request.
Could not find service "com.example.foo" in domain for system

奎因“爱斯基摩人!” 建议使用以下方法remove

  1. 删除属性列表。
  2. 删除辅助工具。
  3. 删除作业。

当我把事情清理干净后,你瞧,remoteObjectProxyWithErrorHandler当工具没有安装时,我开始调用它的错误处理程序。

于 2020-08-19T08:03:31.227 回答