Убедитесь, что вспомогательный инструмент установлен

Я пишу приложение для macOS на Swift, которому нужен привилегированный вспомогательный инструмент — хотелось бы, чтобы повышение прав не было необходимо, но похоже, что это так.

Я нашел это отличный пример приложения, специально предназначенного для этого сценария. Мне удалось перенести его код в свое собственное приложение, но я застрял на том этапе, когда мне нужно проверить, установлен ли вспомогательный инструмент, и если это не так, используйте SMJobBless() и друзей для его установки.

При запуске примера приложения, если вспомогательный инструмент не установлен, приложение остается на следующем экране:

введите здесь описание изображения

Чтобы было ясно, прочитав код, я подумал, что в какой-то момент предполагалось обновить метку до «Помощник установлен: нет», но, похоже, этого не происходит.

Если я нажму «Установить помощника», это результат.

введите здесь описание изображения

С этого момента, если я не удалю вспомогательный инструмент вручную, при повторном запуске приложения будет отображаться этот экран с надписью «Помощник установлен: Да».

Такое поведение может быть приемлемым в данном примере ситуации, когда пользователю приходится вручную нажимать кнопку «Установить помощник». Однако в моем приложении я хотел бы, чтобы оно автоматически запрашивало установку вспомогательного инструмента, если он еще не установлен. Если он уже установлен, я не хочу тратить время пользователя на повторный запрос пароля.

Я думал, что это будет достаточно просто: если вспомогательный инструмент недоступен, где-то в процессе подключения к нему произойдет ошибка, которая побудит меня запросить установку инструмента. Если ошибок не возникает, предполагается, что инструмент уже установлен.

Вот взломанный код, который я написал для подключения к вспомогательному инструменту через 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(); если он что-то возвращает, «Установить помощник» становится серым, а метка меняется на «Помощник установлен: Да».

Я подумал о том, чтобы немного расширить эту идею, написав в своем инструменте простую функцию, которая мгновенно возвращается, и использую тайм-аут в основном приложении — если я не получу результат до истечения времени кода, вспомогательный инструмент, скорее всего, не установлены. Я нахожу это хакерским решением — что, если, например, вспомогательный инструмент (который запускается по требованию) запускается слишком долго, скажем, из-за того, что компьютер старый, а пользователь запускает что-то, интенсивно использующее ЦП?

Я вижу другие альтернативы, такие как просмотр файловой системы в ожидаемых местах (/Library/PrivilegedHelperTools и /Library/LaunchDaemons), но опять же это решение кажется мне неудовлетворительным.

Мой вопрос: Есть ли способ однозначно определить, прослушивает ли на другом конце привилегированный вспомогательный инструмент XPC?

Моя среда: macOS Mojave 10.14.2, Xcode 10.1, Swift 4.2.


person swineone    schedule 19.01.2019    source источник


Ответы (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, которые я отправляю сразу после установки нового помощника или непосредственно перед тем, как планирую удалить или заменить установленный помощник.

person James Bucanek    schedule 21.01.2019
comment
Примерно так я и думал сделать. Меня просто беспокоит необходимость тайм-аута, чтобы убедиться, что инструмент на самом деле не установлен, а не просто слишком долго работает - обычно мое приложение запускается при входе в систему, поэтому система может быть довольно занята. Как вы справляетесь с этим в своих приложениях? - person swineone; 25.01.2019
comment
Отредактировал ответ, включив в него фрагмент кода (частично написанный от руки), чтобы проиллюстрировать, как тестировать компонент. Поскольку все сообщения XPC являются асинхронными, вам придется установить какой-то тайм-аут, но это несложно. Обратите внимание, что -verifyServiceVersion будет блокироваться до тех пор, пока помощник не ответит или не истечет время ожидания, но его безопасно запускать в фоновом потоке. - person James Bucanek; 25.01.2019

Я бы проверил файловую систему, существует ли двоичный файл (в /Library/PrivilegedHelperTools и существует ли plist в /Library/LaunchDaemons). Затем вы можете связаться со службой XPC и вызвать функцию проверки связи, которая отвечает, если служба запущена и работает.

только мои 2 балла,

Роберт

person Robert    schedule 19.01.2019
comment
Преимущество этого подхода заключается в том, что он позволяет быстро определить, что не установлено, что важно для моего варианта использования. Мне все еще нужно проверить, какая версия установлена ​​(для чего требуется тайм-аут), но это все же полезный подход. . - person Mark Bessey; 13.02.2020

На самом деле можно избежать тайм-аута ожидания ответа инструмента. Фактически, приведенный вами пример erikberglund/SwiftPrivilegedHelper печатает слово «Нет» рядом с текстовым полем «Установлен помощник», если инструмент не включен (см. /AppDelegate.swift#L91" rel="nofollow noreferrer">здесь), хотя и асинхронно, но практически мгновенно.

Тем не менее, я был в уникальном положении, имея именно ту проблему, которую вы описали в моей собственной реализации привилегированного помощника, но с примером SwiftPrivilegedHelper, полностью работающим для меня. Если инструмент не установлен, в первом случае обработчик ошибок remoteObjectProxyWithErrorHandler никогда не вызывается, а во втором — вызывается.

Поскольку у меня был пример вашей проблемы, а также рабочий пример, я считаю, что мне удалось определить основную причину:

По крайней мере, в моем случае я не полностью удалил вспомогательный инструмент. В какой-то момент я удалил его plist из /Library/LaunchDaemons, а сам инструмент из /Library/PrivilegedHelperTools, но, похоже, я не запускал sudo launchctl unload x. Он все еще был в списке, когда я запустил sudo launchctl list. В этих обстоятельствах вызов инструмента, очевидно, не увенчается успехом (поскольку инструмент не установлен), но и не завершится сбоем (поскольку launchctl считает, что он установлен).

Как только я все прояснил (запустив sudo launchctl remove x), о чудо, мой remoteObjectProxyWithErrorHandler начал вызывать свой обработчик ошибок, когда инструмент не установлен.

person jeff-h    schedule 19.08.2020