chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if os(macOS)
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
public final class XPCClient: Sendable {
|
||||
/// The maximum amount of time to wait for a request to a recently
|
||||
/// registered XPC service. Once a service has launched, XPC
|
||||
/// requests only have milliseconds of overhead, but in some instances,
|
||||
/// macOS can take 5 seconds (or considerably longer) to launch a
|
||||
/// service after it has been registered.
|
||||
public static let xpcRegistrationTimeout: Duration = .seconds(60)
|
||||
|
||||
private nonisolated(unsafe) let connection: xpc_connection_t
|
||||
private let q: DispatchQueue?
|
||||
private let service: String
|
||||
|
||||
public init(service: String, queue: DispatchQueue? = nil) {
|
||||
let connection = xpc_connection_create_mach_service(service, queue, 0)
|
||||
self.connection = connection
|
||||
self.q = queue
|
||||
self.service = service
|
||||
|
||||
xpc_connection_set_event_handler(connection) { _ in }
|
||||
xpc_connection_set_target_queue(connection, self.q)
|
||||
xpc_connection_activate(connection)
|
||||
}
|
||||
|
||||
public init(connection: xpc_connection_t, label: String, queue: DispatchQueue? = nil) {
|
||||
self.connection = connection
|
||||
self.q = queue
|
||||
self.service = label
|
||||
|
||||
xpc_connection_set_event_handler(connection) { _ in }
|
||||
xpc_connection_set_target_queue(connection, self.q)
|
||||
xpc_connection_activate(connection)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCClient {
|
||||
/// Close the underlying XPC connection.
|
||||
public func close() {
|
||||
xpc_connection_cancel(connection)
|
||||
}
|
||||
|
||||
/// Returns the pid of process to which we have a connection.
|
||||
/// Note: `xpc_connection_get_pid` returns 0 if no activity
|
||||
/// has taken place on the connection prior to it being called.
|
||||
public func remotePid() -> pid_t {
|
||||
xpc_connection_get_pid(self.connection)
|
||||
}
|
||||
|
||||
/// Install a handler that is called whenever the connection receives an XPC error event.
|
||||
///
|
||||
/// This replaces the existing (no-op) event handler. Call this before the first
|
||||
/// `send()` to avoid a disconnect-before-handler race.
|
||||
///
|
||||
/// ```swift
|
||||
/// let client = XPCClient(service: "com.example.myservice")
|
||||
/// client.setDisconnectHandler {
|
||||
/// print("service disconnected, cleaning up")
|
||||
/// }
|
||||
/// let response = try await client.send(request)
|
||||
/// ```
|
||||
public func setDisconnectHandler(_ handler: @Sendable @escaping () -> Void) {
|
||||
xpc_connection_set_event_handler(connection) { object in
|
||||
if xpc_get_type(object) == XPC_TYPE_ERROR { handler() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a persistent session backed by this client connection.
|
||||
///
|
||||
/// The session installs a disconnect handler at initialisation time, before
|
||||
/// any messages are sent, ensuring no server-exit event is missed.
|
||||
public func openSession() -> XPCClientSession {
|
||||
XPCClientSession(client: self)
|
||||
}
|
||||
|
||||
/// Send the provided message to the service.
|
||||
@discardableResult
|
||||
public func send(_ message: XPCMessage, responseTimeout: Duration? = nil) async throws -> XPCMessage {
|
||||
try await withThrowingTaskGroup(of: XPCMessage.self, returning: XPCMessage.self) { group in
|
||||
if let responseTimeout {
|
||||
group.addTask {
|
||||
try await Task.sleep(for: responseTimeout)
|
||||
let route = message.string(key: XPCMessage.routeKey) ?? "nil"
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "XPC timeout for request to \(self.service)/\(route)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
xpc_connection_send_message_with_reply(self.connection, message.underlying, nil) { reply in
|
||||
do {
|
||||
let message = try self.parseReply(reply)
|
||||
cont.resume(returning: message)
|
||||
} catch {
|
||||
cont.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let response = try await group.next()
|
||||
// once one task has finished, cancel the rest.
|
||||
group.cancelAll()
|
||||
// we don't really care about the second error here
|
||||
// as it's most likely a `CancellationError`.
|
||||
try? await group.waitForAll()
|
||||
|
||||
guard let response else {
|
||||
throw ContainerizationError(.invalidState, message: "failed to receive XPC response")
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
private func parseReply(_ reply: xpc_object_t) throws -> XPCMessage {
|
||||
switch xpc_get_type(reply) {
|
||||
case XPC_TYPE_ERROR:
|
||||
var code = ContainerizationError.Code.invalidState
|
||||
if reply.connectionError {
|
||||
code = .interrupted
|
||||
}
|
||||
throw ContainerizationError(
|
||||
code,
|
||||
message: "XPC connection error: \(reply.errorDescription ?? "unknown")"
|
||||
)
|
||||
case XPC_TYPE_DICTIONARY:
|
||||
let message = XPCMessage(object: reply)
|
||||
// check errors from our protocol
|
||||
try message.error()
|
||||
return message
|
||||
default:
|
||||
fatalError("unhandled xpc object type: \(xpc_get_type(reply))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user