Merge pull request #59 from RobotsAndPencils/xpc-connection-bug

Fold HelperInstaller into HelperClient, improve error handling and logging
This commit is contained in:
Brandon Evans 2021-01-19 20:26:23 -07:00 committed by GitHub
commit 23df4a8c3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 60 deletions

View file

@ -39,19 +39,18 @@
CA9FF8DB25959B4000E47BAF /* XPCDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8DA25959B4000E47BAF /* XPCDelegate.swift */; };
CA9FF8E025959BAA00E47BAF /* ConnectionVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8DF25959BAA00E47BAF /* ConnectionVerifier.swift */; };
CA9FF8E625959BB800E47BAF /* AuditTokenHack.m in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8E525959BB800E47BAF /* AuditTokenHack.m */; };
CA9FF8F525959CE000E47BAF /* HelperInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */; };
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF9352595B44700E47BAF /* HelperClient.swift */; };
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2C255A5262003FD669 /* AppleAPI */; };
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */; };
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */; };
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */; };
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */; };
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */ = {isa = PBXBuildFile; productRef = CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */; };
CAA8587C25A2B37900ACF8C0 /* IsTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */; };
CAA8589325A2B77E00ACF8C0 /* aria2c in Copy aria2c */ = {isa = PBXBuildFile; fileRef = CAA8588025A2B63A00ACF8C0 /* aria2c */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA8589A25A2B83000ACF8C0 /* Aria2CError.swift */; };
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */ = {isa = PBXBuildFile; fileRef = CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */; };
CAA858C425A2BE4E00ACF8C0 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA858C325A2BE4E00ACF8C0 /* Downloader.swift */; };
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */ = {isa = PBXBuildFile; productRef = CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */; };
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */ = {isa = PBXBuildFile; fileRef = CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */; };
CABFA9BB2592EEEA00380FEE /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9BA2592EEEA00380FEE /* DateFormatter+.swift */; };
CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A92592EEE900380FEE /* Environment.swift */; };
CABFA9BF2592EEEA00380FEE /* URLSession+DownloadTaskPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B32592EEEA00380FEE /* URLSession+DownloadTaskPublisher.swift */; };
@ -188,7 +187,6 @@
CA9FF8E425959BB800E47BAF /* AuditTokenHack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AuditTokenHack.h; sourceTree = "<group>"; };
CA9FF8E525959BB800E47BAF /* AuditTokenHack.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AuditTokenHack.m; sourceTree = "<group>"; };
CA9FF8EA25959BDD00E47BAF /* com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "com.robotsandpencils.XcodesApp.Helper-Bridging-Header.h"; sourceTree = "<group>"; };
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperInstaller.swift; sourceTree = "<group>"; };
CA9FF9052595A28400E47BAF /* com.robotsandpencils.XcodesApp.HelperTest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = com.robotsandpencils.XcodesApp.HelperTest.entitlements; sourceTree = "<group>"; };
CA9FF9252595A7EB00E47BAF /* Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Scripts; sourceTree = "<group>"; };
CA9FF9352595B44700E47BAF /* HelperClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperClient.swift; sourceTree = "<group>"; };
@ -200,8 +198,8 @@
CAA8588025A2B63A00ACF8C0 /* aria2c */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = aria2c; sourceTree = "<group>"; };
CAA8588A25A2B69300ACF8C0 /* aria2c.LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = aria2c.LICENSE; sourceTree = "<group>"; };
CAA8589A25A2B83000ACF8C0 /* Aria2CError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aria2CError.swift; sourceTree = "<group>"; };
CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = "aria2-release-1.35.0.tar.gz"; sourceTree = "<group>"; };
CAA858C325A2BE4E00ACF8C0 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = "<group>"; };
CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = "aria2-release-1.35.0.tar.gz"; sourceTree = "<group>"; };
CABFA9A02592EAF500380FEE /* R&PLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "R&PLogo.png"; sourceTree = "<group>"; };
CABFA9A12592EAFB00380FEE /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
CABFA9A32592ED5700380FEE /* Apple.paw */ = {isa = PBXFileReference; lastKnownFileType = file; name = Apple.paw; path = ../xcodes/Apple.paw; sourceTree = "<group>"; };
@ -409,7 +407,6 @@
CABFA9B82592EEEA00380FEE /* FileManager+.swift */,
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */,
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */,
CA9FF9352595B44700E47BAF /* HelperClient.swift */,
CAC281D9259F985100B8AB0B /* InstallationStep.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
@ -754,7 +751,6 @@
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
CA9FF8F525959CE000E47BAF /* HelperInstaller.swift in Sources */,
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */,

View file

@ -237,7 +237,7 @@ public struct Defaults {
private let helperClient = HelperClient()
public struct Helper {
var install: () -> Void = HelperInstaller.install
var install: () -> Void = helperClient.install
var checkIfLatestHelperIsInstalled: () -> AnyPublisher<Bool, Never> = helperClient.checkIfLatestHelperIsInstalled
var getVersion: () -> AnyPublisher<String, Error> = helperClient.getVersion
var switchXcodePath: (_ absolutePath: String) -> AnyPublisher<Void, Error> = helperClient.switchXcodePath

View file

@ -1,5 +1,7 @@
import Combine
import Foundation
import os.log
import ServiceManagement
final class HelperClient {
private var connection: NSXPCConnection?
@ -34,26 +36,35 @@ final class HelperClient {
}
func checkIfLatestHelperIsInstalled() -> AnyPublisher<Bool, Never> {
Logger.helperClient.info(#function)
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName)
guard
let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any],
let bundledHelperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String
else {
return Just(false).eraseToAnyPublisher()
return Just(false)
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
return getVersion()
.map { installedHelperVersion in installedHelperVersion == bundledHelperVersion }
.catch { _ in Just(false) }
// Failure is Never, so don't bother logging completion
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0), privacy: .public)") })
.eraseToAnyPublisher()
}
func getVersion() -> AnyPublisher<String, Error> {
Logger.helperClient.info(#function)
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
@ -71,15 +82,27 @@ final class HelperClient {
.map { _ in Void() }
)
.map { $0.0 }
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0), privacy: .public)") },
receiveCompletion: { completion in
switch completion {
case .finished:
Logger.helperClient.info("\(#function): finished")
case let .failure(error):
Logger.helperClient.error("\(#function): \(String(describing: error))")
}
})
.eraseToAnyPublisher()
}
func switchXcodePath(_ absolutePath: String) -> AnyPublisher<Void, Error> {
Logger.helperClient.info("\(#function): \(absolutePath, privacy: .private(mask: .hash))")
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
@ -101,15 +124,27 @@ final class HelperClient {
.map { _ in Void() }
)
.map { $0.0 }
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0))") },
receiveCompletion: { completion in
switch completion {
case .finished:
Logger.helperClient.info("\(#function): finished")
case let .failure(error):
Logger.helperClient.error("\(#function): \(String(describing: error))")
}
})
.eraseToAnyPublisher()
}
func devToolsSecurityEnable() -> AnyPublisher<Void, Error> {
Logger.helperClient.info(#function)
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
@ -131,15 +166,27 @@ final class HelperClient {
.map { _ in Void() }
)
.map { $0.0 }
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0))") },
receiveCompletion: { completion in
switch completion {
case .finished:
Logger.helperClient.info("\(#function): finished")
case let .failure(error):
Logger.helperClient.error("\(#function): \(String(describing: error))")
}
})
.eraseToAnyPublisher()
}
func addStaffToDevelopersGroup() -> AnyPublisher<Void, Error> {
Logger.helperClient.info(#function)
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
@ -161,15 +208,27 @@ final class HelperClient {
.map { _ in Void() }
)
.map { $0.0 }
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0))") },
receiveCompletion: { completion in
switch completion {
case .finished:
Logger.helperClient.info("\(#function): finished")
case let .failure(error):
Logger.helperClient.error("\(#function): \(String(describing: error))")
}
})
.eraseToAnyPublisher()
}
func acceptXcodeLicense(absoluteXcodePath: String) -> AnyPublisher<Void, Error> {
Logger.helperClient.info("\(#function): \(absoluteXcodePath, privacy: .private(mask: .hash))")
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
@ -191,15 +250,27 @@ final class HelperClient {
.map { _ in Void() }
)
.map { $0.0 }
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0))") },
receiveCompletion: { completion in
switch completion {
case .finished:
Logger.helperClient.info("\(#function): finished")
case let .failure(error):
Logger.helperClient.error("\(#function): \(String(describing: error))")
}
})
.eraseToAnyPublisher()
}
func runFirstLaunch(absoluteXcodePath: String) -> AnyPublisher<Void, Error> {
Logger.helperClient.info("\(#function): \(absoluteXcodePath, privacy: .private(mask: .hash))")
let connectionErrorSubject = PassthroughSubject<String, Error>()
guard
let helper = self.helper(errorSubject: connectionErrorSubject)
else {
return Fail(error: NSError())
return Fail(error: HelperClientError.failedToCreateRemoteObjectProxy)
.handleEvents(receiveCompletion: { Logger.helperClient.error("\(#function): \(String(describing: $0))") })
.eraseToAnyPublisher()
}
@ -221,6 +292,72 @@ final class HelperClient {
.map { _ in Void() }
)
.map { $0.0 }
.handleEvents(receiveOutput: { Logger.helperClient.info("\(#function): \(String(describing: $0))") },
receiveCompletion: { completion in
switch completion {
case .finished:
Logger.helperClient.info("\(#function): finished")
case let .failure(error):
Logger.helperClient.error("\(#function): \(String(describing: error))")
}
})
.eraseToAnyPublisher()
}
// MARK: - Install
// From https://github.com/securing/SimpleXPCApp/
func install() {
Logger.helperClient.info(#function)
var authItem = kSMRightBlessPrivilegedHelper.withCString { name in
AuthorizationItem(name: name, valueLength: 0, value:UnsafeMutableRawPointer(bitPattern: 0), flags: 0)
}
var authRights = withUnsafeMutablePointer(to: &authItem) { authItem in
AuthorizationRights(count: 1, items: authItem)
}
do {
let authRef = try authorizationRef(&authRights, nil, [.interactionAllowed, .extendRights, .preAuthorize])
var cfError: Unmanaged<CFError>?
SMJobBless(kSMDomainSystemLaunchd, machServiceName as CFString, authRef, &cfError)
if let error = cfError?.takeRetainedValue() { throw error }
self.connection?.invalidate()
self.connection = nil
Logger.helperClient.info("\(#function): Finished installation")
} catch {
Logger.helperClient.error("\(#function): \(error.localizedDescription)")
}
}
private func executeAuthorizationFunction(_ authorizationFunction: () -> (OSStatus) ) throws {
let osStatus = authorizationFunction()
guard osStatus == errAuthorizationSuccess else {
throw HelperClientError.message(String(describing: SecCopyErrorMessageString(osStatus, nil)))
}
}
func authorizationRef(_ rights: UnsafePointer<AuthorizationRights>?,
_ environment: UnsafePointer<AuthorizationEnvironment>?,
_ flags: AuthorizationFlags) throws -> AuthorizationRef? {
var authRef: AuthorizationRef?
try executeAuthorizationFunction { AuthorizationCreate(rights, environment, flags, &authRef) }
return authRef
}
}
enum HelperClientError: LocalizedError {
case failedToCreateRemoteObjectProxy
case message(String)
var errorDescription: String? {
switch self {
case .failedToCreateRemoteObjectProxy:
return "Unable to communicate with privileged helper."
case let .message(message):
return message
}
}
}

View file

@ -1,44 +0,0 @@
// From https://github.com/securing/SimpleXPCApp/
import Foundation
import os.log
import ServiceManagement
enum HelperAuthorizationError: Error {
case message(String)
}
class HelperInstaller {
private static func executeAuthorizationFunction(_ authorizationFunction: () -> (OSStatus) ) throws {
let osStatus = authorizationFunction()
guard osStatus == errAuthorizationSuccess else {
throw HelperAuthorizationError.message(String(describing: SecCopyErrorMessageString(osStatus, nil)))
}
}
static func authorizationRef(_ rights: UnsafePointer<AuthorizationRights>?,
_ environment: UnsafePointer<AuthorizationEnvironment>?,
_ flags: AuthorizationFlags) throws -> AuthorizationRef? {
var authRef: AuthorizationRef?
try executeAuthorizationFunction { AuthorizationCreate(rights, environment, flags, &authRef) }
return authRef
}
static func install() {
var authItem = kSMRightBlessPrivilegedHelper.withCString { name in
AuthorizationItem(name: name, valueLength: 0, value:UnsafeMutableRawPointer(bitPattern: 0), flags: 0)
}
var authRights = withUnsafeMutablePointer(to: &authItem) { authItem in
AuthorizationRights(count: 1, items: authItem)
}
do {
let authRef = try authorizationRef(&authRights, nil, [.interactionAllowed, .extendRights, .preAuthorize])
var cfError: Unmanaged<CFError>?
SMJobBless(kSMDomainSystemLaunchd, machServiceName as CFString, authRef, &cfError)
if let error = cfError?.takeRetainedValue() { throw error }
} catch {
Logger.helperInstaller.error("\(error.localizedDescription)")
}
}
}

View file

@ -5,6 +5,6 @@ extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!
static let appState = Logger(subsystem: subsystem, category: "appState")
static let helperInstaller = Logger(subsystem: subsystem, category: "helperInstaller")
static let helperClient = Logger(subsystem: subsystem, category: "helperClient")
static let subprocess = Logger(subsystem: subsystem, category: "subprocess")
}