From 9cbffe63b71b69d053d896ef7ad3f0150bb81dea Mon Sep 17 00:00:00 2001 From: Josh Kaplan Date: Mon, 24 Jan 2022 22:47:54 +1300 Subject: [PATCH] Makes repeated route registration result in `fatalError` instead of an `Error` In doing so, slightly consolidates the internal implementation of route registration. --- Sources/SecureXPC/Server/XPCServer.swift | 91 ++++++++++--------- Sources/SecureXPC/XPCError.swift | 2 - .../Round-trip Integration Test.swift | 28 +++--- .../Server Termination Integration Test.swift | 2 +- 4 files changed, 63 insertions(+), 60 deletions(-) diff --git a/Sources/SecureXPC/Server/XPCServer.swift b/Sources/SecureXPC/Server/XPCServer.swift index c5ea255..cf4d9bb 100644 --- a/Sources/SecureXPC/Server/XPCServer.swift +++ b/Sources/SecureXPC/Server/XPCServer.swift @@ -83,15 +83,15 @@ import Foundation /// - ``makeAnonymous()`` /// - ``makeAnonymous(clientRequirements:)`` /// ### Registering Routes -/// - ``registerRoute(_:handler:)-82935`` -/// - ``registerRoute(_:handler:)-7yvyr`` -/// - ``registerRoute(_:handler:)-3ohmq`` -/// - ``registerRoute(_:handler:)-4jjs6`` +/// - ``registerRoute(_:handler:)-4ttqe`` +/// - ``registerRoute(_:handler:)-9a0x9`` +/// - ``registerRoute(_:handler:)-4fxv0`` +/// - ``registerRoute(_:handler:)-1jw9d`` /// ### Registering Async Routes -/// - ``registerRoute(_:handler:)-ck6u`` -/// - ``registerRoute(_:handler:)-5gscr`` -/// - ``registerRoute(_:handler:)-3pv2z`` -/// - ``registerRoute(_:handler:)-7l0xv`` +/// - ``registerRoute(_:handler:)-6htah`` +/// - ``registerRoute(_:handler:)-g7ww`` +/// - ``registerRoute(_:handler:)-rw2w`` +/// - ``registerRoute(_:handler:)-2vk6u`` /// ### Configuring a Server /// - ``targetQueue`` /// - ``errorHandler`` @@ -144,112 +144,117 @@ public class XPCServer { connections.compactMap{ $0.connection }.forEach{ xpc_connection_set_target_queue($0, newValue) } } } + // MARK: Route registration - private func checkUniqueRoute(route: XPCRoute) throws { - if self.routes.keys.contains(route) { - throw XPCError.routeAlreadyRegistered(route.pathComponents) + /// Internal function that actually registers the route and enforces that a route is only ever registered once. + /// + /// All of the public functions exist to satisfy type constraints. + private func registerRoute(_ route: XPCRoute, handler: XPCHandler) { + if let _ = self.routes.updateValue(handler, forKey: route) { + fatalError("Route \(route.pathComponents) is already registered") } } /// Registers a route that has no message and can't receive a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has no message and can't receive a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. public func registerRoute(_ route: XPCRouteWithoutMessageWithoutReply, - handler: @escaping () throws -> Void) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithoutMessageWithoutReplySync(handler: handler) + handler: @escaping () throws -> Void) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithoutMessageWithoutReplySync(handler: handler)) } /// Registers a route that has no message and can't receive a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has no message and can't receive a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. @available(macOS 10.15.0, *) public func registerRoute(_ route: XPCRouteWithoutMessageWithoutReply, - handler: @escaping () async throws -> Void) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithoutMessageWithoutReplyAsync(handler: handler) + handler: @escaping () async throws -> Void) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithoutMessageWithoutReplyAsync(handler: handler)) } /// Registers a route that has a message and can't receive a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has a message and can't receive a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. public func registerRoute(_ route: XPCRouteWithMessageWithoutReply, - handler: @escaping (M) throws -> Void) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithoutReplySync(handler: handler) + handler: @escaping (M) throws -> Void) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithMessageWithoutReplySync(handler: handler)) } /// Registers a route that has a message and can't receive a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has a message and can't receive a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. @available(macOS 10.15.0, *) public func registerRoute(_ route: XPCRouteWithMessageWithoutReply, - handler: @escaping (M) async throws -> Void) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithoutReplyAsync(handler: handler) + handler: @escaping (M) async throws -> Void) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithMessageWithoutReplyAsync(handler: handler)) } /// Registers a route that has no message and expects a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has no message and expects a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. public func registerRoute(_ route: XPCRouteWithoutMessageWithReply, - handler: @escaping () throws -> R) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithoutMessageWithReplySync(handler: handler) + handler: @escaping () throws -> R) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithoutMessageWithReplySync(handler: handler)) } /// Registers a route that has no message and expects a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has no message and expects a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. @available(macOS 10.15.0, *) public func registerRoute(_ route: XPCRouteWithoutMessageWithReply, - handler: @escaping () async throws -> R) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithoutMessageWithReplyAsync(handler: handler) + handler: @escaping () async throws -> R) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithoutMessageWithReplyAsync(handler: handler)) } /// Registers a route that has a message and expects a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has a message and expects a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. - /// - Throws: If this route has already been registered. public func registerRoute(_ route: XPCRouteWithMessageWithReply, - handler: @escaping (M) throws -> R) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithReplySync(handler: handler) + handler: @escaping (M) throws -> R) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithMessageWithReplySync(handler: handler)) } /// Registers a route that has a message and expects a reply. /// + /// > Important: Routes can only be registered with a handler once; it is a programming error to provide a route which has already been registered. + /// /// - Parameters: /// - route: A route that has a message and expects a reply. /// - handler: Will be called when the server receives an incoming request for this route if the request is accepted. /// - Throws: If this route has already been registered. @available(macOS 10.15.0, *) public func registerRoute(_ route: XPCRouteWithMessageWithReply, - handler: @escaping (M) async throws -> R) throws { - try checkUniqueRoute(route: route.route) - self.routes[route.route] = ConstrainedXPCHandlerWithMessageWithReplyAsync(handler: handler) + handler: @escaping (M) async throws -> R) { + self.registerRoute(route.route, handler: ConstrainedXPCHandlerWithMessageWithReplyAsync(handler: handler)) } internal func startClientConnection(_ connection: xpc_connection_t) { @@ -344,7 +349,7 @@ public class XPCServer { } } else { fatalError("Non-sync handler for route \(request.route.pathComponents) was found, but only sync routes " + - "should be registerable on this OS version. Handler: \(handler)") + "should be registrable on this OS version. Handler: \(handler)") } } diff --git a/Sources/SecureXPC/XPCError.swift b/Sources/SecureXPC/XPCError.swift index cd7efb2..89438f4 100644 --- a/Sources/SecureXPC/XPCError.swift +++ b/Sources/SecureXPC/XPCError.swift @@ -42,8 +42,6 @@ public enum XPCError: Error, Codable { /// /// The associated value describes this decoding error. case decodingError(String) - /// The route can't be registered because a route with this path already exists. - case routeAlreadyRegistered([String]) /// The route associated with the incoming XPC request is not registered with the ``XPCServer``. case routeNotRegistered([String]) /// While the route associated with the incoming XPC request is registered with the ``XPCServer``, the message and/or reply does not match the handler diff --git a/Tests/SecureXPCTests/Client & Server/Round-trip Integration Test.swift b/Tests/SecureXPCTests/Client & Server/Round-trip Integration Test.swift index 056d704..146f52f 100644 --- a/Tests/SecureXPCTests/Client & Server/Round-trip Integration Test.swift +++ b/Tests/SecureXPCTests/Client & Server/Round-trip Integration Test.swift @@ -25,7 +25,7 @@ class RoundTripIntegrationTest: XCTestCase { let replyBlockWasCalled = self.expectation(description: "The echo reply was received") let echoRoute = XPCRoute.named("echo").withMessageType(String.self).withReplyType(String.self) - try anonymousServer.registerRoute(echoRoute) { msg in + anonymousServer.registerRoute(echoRoute) { msg in remoteHandlerWasCalled.fulfill() return "echo: \(msg)" } @@ -44,14 +44,14 @@ class RoundTripIntegrationTest: XCTestCase { func testSendWithMessageWithReply_AsyncClient_SyncServer() async throws { let echoRoute = XPCRoute.named("echo").withMessageType(String.self).withReplyType(String.self) - try anonymousServer.registerRoute(echoRoute) { msg in "echo: \(msg)" } + anonymousServer.registerRoute(echoRoute) { msg in "echo: \(msg)" } let result = try await xpcClient.sendMessage("Hello, world!", route: echoRoute) XCTAssertEqual(result, "echo: Hello, world!") } func testSendWithMessageWithReply_AsyncClient_AsyncServer() async throws { let echoRoute = XPCRoute.named("echo").withMessageType(String.self).withReplyType(String.self) - try anonymousServer.registerRoute(echoRoute) { (msg: String) async -> String in + anonymousServer.registerRoute(echoRoute) { (msg: String) async -> String in "echo: \(msg)" } let result = try await xpcClient.sendMessage("Hello, world!", route: echoRoute) @@ -63,7 +63,7 @@ class RoundTripIntegrationTest: XCTestCase { let replyBlockWasCalled = self.expectation(description: "The pong reply was received") let pingRoute = XPCRoute.named("ping").withReplyType(String.self) - try anonymousServer.registerRoute(pingRoute) { + anonymousServer.registerRoute(pingRoute) { remoteHandlerWasCalled.fulfill() return "pong" } @@ -82,14 +82,14 @@ class RoundTripIntegrationTest: XCTestCase { func testSendWithoutMessageWithReply_AsyncClient_SyncServer() async throws { let pingRoute = XPCRoute.named("ping").withReplyType(String.self) - try anonymousServer.registerRoute(pingRoute) { "pong" } + anonymousServer.registerRoute(pingRoute) { "pong" } let result = try await xpcClient.send(route: pingRoute) XCTAssertEqual(result, "pong") } func testSendWithoutMessageWithReply_AsyncClient_AsyncServer() async throws { let pingRoute = XPCRoute.named("ping").withReplyType(String.self) - try anonymousServer.registerRoute(pingRoute) { () async -> String in + anonymousServer.registerRoute(pingRoute) { () async -> String in "pong" } let result = try await xpcClient.send(route: pingRoute) @@ -100,7 +100,7 @@ class RoundTripIntegrationTest: XCTestCase { let remoteHandlerWasCalled = self.expectation(description: "The remote handler was called") let msgNoReplyRoute = XPCRoute.named("msgNoReplyRoute").withMessageType(String.self) - try anonymousServer.registerRoute(msgNoReplyRoute) { msg in + anonymousServer.registerRoute(msgNoReplyRoute) { msg in XCTAssertEqual(msg, "Hello, world!") remoteHandlerWasCalled.fulfill() } @@ -115,7 +115,7 @@ class RoundTripIntegrationTest: XCTestCase { let responseBlockWasCalled = self.expectation(description: "The response was received") let msgNoReplyRoute = XPCRoute.named("msgNoReplyRoute").withMessageType(String.self) - try anonymousServer.registerRoute(msgNoReplyRoute) { msg in + anonymousServer.registerRoute(msgNoReplyRoute) { msg in XCTAssertEqual(msg, "Hello, world!") remoteHandlerWasCalled.fulfill() } @@ -133,7 +133,7 @@ class RoundTripIntegrationTest: XCTestCase { func testSendWithMessageWithoutReply_AsyncClient_SyncServer() async throws { let remoteHandlerWasCalled = self.expectation(description: "The remote handler was called") let msgNoReplyRoute = XPCRoute.named("msgNoReplyRoute").withMessageType(String.self) - try anonymousServer.registerRoute(msgNoReplyRoute) { msg in + anonymousServer.registerRoute(msgNoReplyRoute) { msg in XCTAssertEqual(msg, "Hello, world!") remoteHandlerWasCalled.fulfill() } @@ -145,7 +145,7 @@ class RoundTripIntegrationTest: XCTestCase { func testSendWithMessageWithoutReply_AsyncClient_AsyncServer() async throws { let remoteHandlerWasCalled = self.expectation(description: "The remote handler was called") let msgNoReplyRoute = XPCRoute.named("msgNoReplyRoute").withMessageType(String.self) - try anonymousServer.registerRoute(msgNoReplyRoute) { (msg: String) async -> Void in + anonymousServer.registerRoute(msgNoReplyRoute) { (msg: String) async -> Void in XCTAssertEqual(msg, "Hello, world!") remoteHandlerWasCalled.fulfill() } @@ -158,7 +158,7 @@ class RoundTripIntegrationTest: XCTestCase { let remoteHandlerWasCalled = self.expectation(description: "The remote handler was called") let noMsgNoReplyRoute = XPCRoute.named("noMsgNoReplyRoute") - try anonymousServer.registerRoute(noMsgNoReplyRoute) { + anonymousServer.registerRoute(noMsgNoReplyRoute) { remoteHandlerWasCalled.fulfill() } @@ -172,7 +172,7 @@ class RoundTripIntegrationTest: XCTestCase { let responseBlockWasCalled = self.expectation(description: "The response was received") let noMsgNoReplyRoute = XPCRoute.named("noMsgNoReplyRoute") - try anonymousServer.registerRoute(noMsgNoReplyRoute) { + anonymousServer.registerRoute(noMsgNoReplyRoute) { remoteHandlerWasCalled.fulfill() } @@ -189,7 +189,7 @@ class RoundTripIntegrationTest: XCTestCase { func testSendWithoutMessageWithoutReply_AsyncClient_SyncServer() async throws { let remoteHandlerWasCalled = self.expectation(description: "The remote handler was called") let noMsgNoReplyRoute = XPCRoute.named("noMsgNoReplyRoute") - try anonymousServer.registerRoute(noMsgNoReplyRoute) { + anonymousServer.registerRoute(noMsgNoReplyRoute) { remoteHandlerWasCalled.fulfill() } try await xpcClient.send(route: noMsgNoReplyRoute) @@ -200,7 +200,7 @@ class RoundTripIntegrationTest: XCTestCase { func testSendWithoutMessageWithoutReply_AsyncClient_AsyncServer() async throws { let remoteHandlerWasCalled = self.expectation(description: "The remote handler was called") let noMsgNoReplyRoute = XPCRoute.named("noMsgNoReplyRoute") - try anonymousServer.registerRoute(noMsgNoReplyRoute, handler: { () async -> Void in + anonymousServer.registerRoute(noMsgNoReplyRoute, handler: { () async -> Void in remoteHandlerWasCalled.fulfill() }) try await xpcClient.send(route: noMsgNoReplyRoute) diff --git a/Tests/SecureXPCTests/Client & Server/Server Termination Integration Test.swift b/Tests/SecureXPCTests/Client & Server/Server Termination Integration Test.swift index cb05edf..274ab8a 100644 --- a/Tests/SecureXPCTests/Client & Server/Server Termination Integration Test.swift +++ b/Tests/SecureXPCTests/Client & Server/Server Termination Integration Test.swift @@ -23,7 +23,7 @@ class ServerTerminationIntegrationTest: XCTestCase { // Server & client setup let echoRoute = XPCRoute.named("echo").withMessageType(String.self).withReplyType(String.self) let server = XPCServer.makeAnonymous(clientRequirements: dummyRequirements) - try server.registerRoute(echoRoute) { msg in + server.registerRoute(echoRoute) { msg in return "echo: \(msg)" } server.start()