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
|
||||
@@ -0,0 +1,53 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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 Synchronization
|
||||
|
||||
/// Represents a long-lived connection to an XPC service on the client side.
|
||||
///
|
||||
/// Obtain one via `XPCClient.openSession()`. The disconnect handler is
|
||||
/// installed at initialisation time, before the first `send()`, so there is
|
||||
/// no window in which a server crash goes undetected.
|
||||
public final class XPCClientSession: Sendable {
|
||||
private let client: XPCClient
|
||||
private let handlers: Mutex<[@Sendable () async -> Void]> = Mutex([])
|
||||
|
||||
init(client: XPCClient) {
|
||||
self.client = client
|
||||
client.setDisconnectHandler { [weak self] in
|
||||
guard let self else { return }
|
||||
let snapshot = self.handlers.withLock { $0 }
|
||||
Task { for handler in snapshot { await handler() } }
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a handler to be called when the server disconnects.
|
||||
public func onDisconnect(_ handler: @Sendable @escaping () async -> Void) {
|
||||
handlers.withLock { $0.append(handler) }
|
||||
}
|
||||
|
||||
/// Send a message over the persistent connection.
|
||||
@discardableResult
|
||||
public func send(_ message: XPCMessage, responseTimeout: Duration? = nil) async throws -> XPCMessage {
|
||||
try await client.send(message, responseTimeout: responseTimeout)
|
||||
}
|
||||
|
||||
/// Cancel the underlying connection.
|
||||
public func close() { client.close() }
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,291 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
/// A message that can be pass across application boundaries via XPC.
|
||||
public struct XPCMessage: Sendable {
|
||||
/// Defined message key storing the route value.
|
||||
public static let routeKey = "com.apple.container.xpc.route"
|
||||
/// Defined message key storing the error value.
|
||||
public static let errorKey = "com.apple.container.xpc.error"
|
||||
|
||||
// Access to `object` is protected by a lock
|
||||
private nonisolated(unsafe) let object: xpc_object_t
|
||||
private let lock = NSLock()
|
||||
private let isErr: Bool
|
||||
|
||||
/// The underlying xpc object that the message wraps.
|
||||
public var underlying: xpc_object_t {
|
||||
lock.withLock {
|
||||
object
|
||||
}
|
||||
}
|
||||
public var isErrorType: Bool { isErr }
|
||||
|
||||
public init(object: xpc_object_t) {
|
||||
self.object = object
|
||||
self.isErr = xpc_get_type(self.object) == XPC_TYPE_ERROR
|
||||
}
|
||||
|
||||
public init(route: String) {
|
||||
self.object = xpc_dictionary_create_empty()
|
||||
self.isErr = false
|
||||
xpc_dictionary_set_string(self.object, Self.routeKey, route)
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCMessage {
|
||||
public static func == (lhs: XPCMessage, rhs: xpc_object_t) -> Bool {
|
||||
xpc_equal(lhs.underlying, rhs)
|
||||
}
|
||||
|
||||
public func reply() -> XPCMessage {
|
||||
lock.withLock {
|
||||
XPCMessage(object: xpc_dictionary_create_reply(object)!)
|
||||
}
|
||||
}
|
||||
|
||||
public func errorKeyDescription() -> String? {
|
||||
guard self.isErr,
|
||||
let xpcErr = lock.withLock({
|
||||
xpc_dictionary_get_string(
|
||||
self.object,
|
||||
XPC_ERROR_KEY_DESCRIPTION
|
||||
)
|
||||
})
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return String(cString: xpcErr)
|
||||
}
|
||||
|
||||
public func error() throws {
|
||||
let data = data(key: Self.errorKey)
|
||||
if let data {
|
||||
let item = try? JSONDecoder().decode(ContainerXPCError.self, from: data)
|
||||
precondition(item != nil, "expected to receive a ContainerXPCXPCError")
|
||||
|
||||
throw ContainerizationError(item!.code, message: item!.message)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(error: ContainerizationError) {
|
||||
var message = error.message
|
||||
if let cause = error.cause {
|
||||
message += " (cause: \"\(cause)\")"
|
||||
}
|
||||
let serializableError = ContainerXPCError(code: error.code.description, message: message)
|
||||
let data = try? JSONEncoder().encode(serializableError)
|
||||
precondition(data != nil)
|
||||
|
||||
set(key: Self.errorKey, value: data!)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContainerXPCError: Codable {
|
||||
let code: String
|
||||
let message: String
|
||||
}
|
||||
|
||||
extension XPCMessage {
|
||||
public func data(key: String) -> Data? {
|
||||
var length: Int = 0
|
||||
let bytes = lock.withLock {
|
||||
xpc_dictionary_get_data(self.object, key, &length)
|
||||
}
|
||||
|
||||
guard let bytes else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Data(bytes: bytes, count: length)
|
||||
}
|
||||
|
||||
/// dataNoCopy is similar to data, except the data is not copied
|
||||
/// to a new buffer. What this means in practice is the second the
|
||||
/// underlying xpc_object_t gets released by ARC the data will be
|
||||
/// released as well. This variant should be used when you know the
|
||||
/// data will be used before the object has no more references.
|
||||
public func dataNoCopy(key: String) -> Data? {
|
||||
var length: Int = 0
|
||||
let bytes = lock.withLock {
|
||||
xpc_dictionary_get_data(self.object, key, &length)
|
||||
}
|
||||
|
||||
guard let bytes else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Data(
|
||||
bytesNoCopy: UnsafeMutableRawPointer(mutating: bytes),
|
||||
count: length,
|
||||
deallocator: .none
|
||||
)
|
||||
}
|
||||
|
||||
public func set(key: String, value: Data) {
|
||||
value.withUnsafeBytes { ptr in
|
||||
if let addr = ptr.baseAddress {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_data(self.object, key, addr, value.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func string(key: String) -> String? {
|
||||
let _id = lock.withLock {
|
||||
xpc_dictionary_get_string(self.object, key)
|
||||
}
|
||||
if let _id {
|
||||
return String(cString: _id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func set(key: String, value: String) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_string(self.object, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
public func bool(key: String) -> Bool {
|
||||
lock.withLock {
|
||||
xpc_dictionary_get_bool(self.object, key)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, value: Bool) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_bool(self.object, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
public func uint64(key: String) -> UInt64 {
|
||||
lock.withLock {
|
||||
xpc_dictionary_get_uint64(self.object, key)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, value: UInt64) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_uint64(self.object, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
public func int64(key: String) -> Int64 {
|
||||
lock.withLock {
|
||||
xpc_dictionary_get_int64(self.object, key)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, value: Int64) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_int64(self.object, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
public func date(key: String) -> Date {
|
||||
lock.withLock {
|
||||
let nsSinceEpoch = xpc_dictionary_get_date(self.object, key)
|
||||
return Date(timeIntervalSince1970: TimeInterval(nsSinceEpoch) / 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, value: Date) {
|
||||
lock.withLock {
|
||||
let nsSinceEpoch = Int64(value.timeIntervalSince1970 * 1_000_000_000)
|
||||
xpc_dictionary_set_date(self.object, key, nsSinceEpoch)
|
||||
}
|
||||
}
|
||||
|
||||
public func fileHandle(key: String) -> FileHandle? {
|
||||
let fd = lock.withLock {
|
||||
xpc_dictionary_get_value(self.object, key)
|
||||
}
|
||||
if let fd {
|
||||
let fd2 = xpc_fd_dup(fd)
|
||||
return FileHandle(fileDescriptor: fd2, closeOnDealloc: false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func set(key: String, value: FileHandle) {
|
||||
let fd = xpc_fd_create(value.fileDescriptor)
|
||||
close(value.fileDescriptor)
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_value(self.object, key, fd)
|
||||
}
|
||||
}
|
||||
|
||||
public func fileHandles(key: String) -> [FileHandle]? {
|
||||
let fds = lock.withLock {
|
||||
xpc_dictionary_get_value(self.object, key)
|
||||
}
|
||||
if let fds {
|
||||
let fd1 = xpc_array_dup_fd(fds, 0)
|
||||
let fd2 = xpc_array_dup_fd(fds, 1)
|
||||
if fd1 == -1 || fd2 == -1 {
|
||||
return nil
|
||||
}
|
||||
return [
|
||||
FileHandle(fileDescriptor: fd1, closeOnDealloc: false),
|
||||
FileHandle(fileDescriptor: fd2, closeOnDealloc: false),
|
||||
]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func set(key: String, value: [FileHandle]) throws {
|
||||
let fdArray = xpc_array_create(nil, 0)
|
||||
for fh in value {
|
||||
guard let xpcFd = xpc_fd_create(fh.fileDescriptor) else {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
message: "failed to create xpc fd for \(fh.fileDescriptor)"
|
||||
)
|
||||
}
|
||||
xpc_array_append_value(fdArray, xpcFd)
|
||||
close(fh.fileDescriptor)
|
||||
}
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_value(self.object, key, fdArray)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, xpcDictionary: xpc_object_t) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_value(self.object, key, xpcDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
public func endpoint(key: String) -> xpc_endpoint_t? {
|
||||
lock.withLock {
|
||||
xpc_dictionary_get_value(self.object, key)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(key: String, value: xpc_endpoint_t) {
|
||||
lock.withLock {
|
||||
xpc_dictionary_set_value(self.object, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,288 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 CAuditToken
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
import Logging
|
||||
import os
|
||||
import Synchronization
|
||||
|
||||
public struct XPCServer: Sendable {
|
||||
public typealias RouteHandler = @Sendable (XPCMessage, XPCServerSession) async throws -> XPCMessage
|
||||
|
||||
/// Wraps a session-unaware handler for use in a route table.
|
||||
public static func route(
|
||||
_ fn: @Sendable @escaping (XPCMessage) async throws -> XPCMessage
|
||||
) -> RouteHandler {
|
||||
{ message, _ in try await fn(message) }
|
||||
}
|
||||
|
||||
private let routes: [String: RouteHandler]
|
||||
// Access to `connection` is protected by a lock.
|
||||
private nonisolated(unsafe) let connection: xpc_connection_t
|
||||
private let lock = NSLock()
|
||||
|
||||
let log: Logging.Logger
|
||||
|
||||
public init(identifier: String, routes: [String: RouteHandler], log: Logging.Logger) {
|
||||
let connection = xpc_connection_create_mach_service(
|
||||
identifier,
|
||||
nil,
|
||||
UInt64(XPC_CONNECTION_MACH_SERVICE_LISTENER)
|
||||
)
|
||||
|
||||
self.routes = routes
|
||||
self.connection = connection
|
||||
self.log = log
|
||||
}
|
||||
|
||||
public init(connection: xpc_connection_t, routes: [String: RouteHandler], log: Logging.Logger) {
|
||||
self.routes = routes
|
||||
self.connection = connection
|
||||
self.log = log
|
||||
}
|
||||
|
||||
public func listen() async throws {
|
||||
let connections = AsyncStream<xpc_connection_t> { cont in
|
||||
lock.withLock {
|
||||
xpc_connection_set_event_handler(self.connection) { object in
|
||||
switch xpc_get_type(object) {
|
||||
case XPC_TYPE_CONNECTION:
|
||||
// `object` isn't used concurrently.
|
||||
nonisolated(unsafe) let object = object
|
||||
cont.yield(object)
|
||||
case XPC_TYPE_ERROR:
|
||||
if object.connectionError {
|
||||
cont.finish()
|
||||
}
|
||||
default:
|
||||
fatalError("unhandled xpc object type: \(xpc_get_type(object))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
lock.withLock {
|
||||
xpc_connection_cancel(self.connection)
|
||||
}
|
||||
}
|
||||
|
||||
lock.withLock {
|
||||
xpc_connection_activate(self.connection)
|
||||
}
|
||||
|
||||
try await withThrowingDiscardingTaskGroup { group in
|
||||
for await conn in connections {
|
||||
// `conn` isn't used concurrently.
|
||||
nonisolated(unsafe) let conn = conn
|
||||
let added = group.addTaskUnlessCancelled { @Sendable in
|
||||
try await self.handleClientConnection(connection: conn)
|
||||
xpc_connection_cancel(conn)
|
||||
}
|
||||
|
||||
if !added {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
group.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
func handleClientConnection(connection: xpc_connection_t) async throws {
|
||||
let replySent = Mutex(false)
|
||||
let session = XPCServerSession()
|
||||
|
||||
let objects = AsyncStream<xpc_object_t> { cont in
|
||||
xpc_connection_set_event_handler(connection) { object in
|
||||
switch xpc_get_type(object) {
|
||||
case XPC_TYPE_DICTIONARY:
|
||||
// `object` isn't used concurrently.
|
||||
nonisolated(unsafe) let object = object
|
||||
cont.yield(object)
|
||||
case XPC_TYPE_ERROR:
|
||||
if object.connectionError {
|
||||
cont.finish()
|
||||
}
|
||||
if !(replySent.withLock({ $0 }) && object.connectionClosed) {
|
||||
// When a xpc connection is closed, the framework sends
|
||||
// a final XPC_ERROR_CONNECTION_INVALID message.
|
||||
// We can ignore this if we know we have already handled
|
||||
// the request.
|
||||
self.log.error(
|
||||
"xpc client handler connection error",
|
||||
metadata: [
|
||||
"error": "\(object.errorDescription ?? "no description")"
|
||||
])
|
||||
}
|
||||
default:
|
||||
fatalError("unhandled xpc object type: \(xpc_get_type(object))")
|
||||
}
|
||||
}
|
||||
}
|
||||
defer {
|
||||
xpc_connection_cancel(connection)
|
||||
}
|
||||
|
||||
xpc_connection_activate(connection)
|
||||
try await withThrowingDiscardingTaskGroup { group in
|
||||
// `connection` isn't used concurrently.
|
||||
nonisolated(unsafe) let connection = connection
|
||||
for await object in objects {
|
||||
// `object` isn't used concurrently.
|
||||
nonisolated(unsafe) let object = object
|
||||
let added = group.addTaskUnlessCancelled { @Sendable in
|
||||
try await self.handleMessage(connection: connection, object: object, session: session)
|
||||
replySent.withLock { $0 = true }
|
||||
}
|
||||
if !added {
|
||||
break
|
||||
}
|
||||
}
|
||||
group.cancelAll()
|
||||
}
|
||||
await session.fireDisconnect()
|
||||
}
|
||||
|
||||
func handleMessage(connection: xpc_connection_t, object: xpc_object_t, session: XPCServerSession) async throws {
|
||||
// All requests are dictionary-valued.
|
||||
guard xpc_get_type(object) == XPC_TYPE_DICTIONARY else {
|
||||
log.error("invalid request - not a dictionary")
|
||||
Self.replyWithError(
|
||||
connection: connection,
|
||||
object: object,
|
||||
err: ContainerizationError(.invalidArgument, message: "invalid request")
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that the client has our EUID
|
||||
var token = audit_token_t()
|
||||
xpc_dictionary_get_audit_token(object, &token)
|
||||
let serverEuid = geteuid()
|
||||
let clientEuid = audit_token_to_euid(token)
|
||||
guard clientEuid == serverEuid else {
|
||||
log.error(
|
||||
"unauthorized request - uid mismatch",
|
||||
metadata: [
|
||||
"server_euid": "\(serverEuid)",
|
||||
"client_euid": "\(clientEuid)",
|
||||
])
|
||||
Self.replyWithError(
|
||||
connection: connection,
|
||||
object: object,
|
||||
err: ContainerizationError(.invalidState, message: "unauthorized request")
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let route = object.route else {
|
||||
log.error("invalid request - empty route")
|
||||
Self.replyWithError(
|
||||
connection: connection,
|
||||
object: object,
|
||||
err: ContainerizationError(.invalidArgument, message: "invalid request")
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if let handler = routes[route] {
|
||||
do {
|
||||
let message = XPCMessage(object: object)
|
||||
let response = try await handler(message, session)
|
||||
xpc_connection_send_message(connection, response.underlying)
|
||||
} catch let error as ContainerizationError {
|
||||
log.error(
|
||||
"route handler threw an error",
|
||||
metadata: [
|
||||
"route": "\(route)",
|
||||
"error": "\(error)",
|
||||
])
|
||||
Self.replyWithError(
|
||||
connection: connection,
|
||||
object: object,
|
||||
err: error
|
||||
)
|
||||
} catch {
|
||||
log.error(
|
||||
"route handler threw an error",
|
||||
metadata: [
|
||||
"route": "\(route)",
|
||||
"error": "\(error)",
|
||||
])
|
||||
let message = XPCMessage(object: object)
|
||||
let reply = message.reply()
|
||||
|
||||
// Check if this is a VolumeError by looking at the error description
|
||||
let errorMessage = error.localizedDescription
|
||||
let errorTypeString = String(describing: type(of: error))
|
||||
if errorTypeString.contains("VolumeError") || errorMessage.contains("Volume") {
|
||||
let err = ContainerizationError(.invalidArgument, message: errorMessage)
|
||||
reply.set(error: err)
|
||||
} else {
|
||||
let err = ContainerizationError(.unknown, message: String(describing: error))
|
||||
reply.set(error: err)
|
||||
}
|
||||
xpc_connection_send_message(connection, reply.underlying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func replyWithError(connection: xpc_connection_t, object: xpc_object_t, err: ContainerizationError) {
|
||||
let message = XPCMessage(object: object)
|
||||
let reply = message.reply()
|
||||
reply.set(error: err)
|
||||
xpc_connection_send_message(connection, reply.underlying)
|
||||
}
|
||||
}
|
||||
|
||||
extension xpc_object_t {
|
||||
var route: String? {
|
||||
let croute = xpc_dictionary_get_string(self, XPCMessage.routeKey)
|
||||
guard let croute else {
|
||||
return nil
|
||||
}
|
||||
return String(cString: croute)
|
||||
}
|
||||
|
||||
var connectionError: Bool {
|
||||
precondition(isError, "not an error")
|
||||
return xpc_equal(self, XPC_ERROR_CONNECTION_INVALID) || xpc_equal(self, XPC_ERROR_CONNECTION_INTERRUPTED)
|
||||
}
|
||||
|
||||
var connectionClosed: Bool {
|
||||
precondition(isError, "not an error")
|
||||
return xpc_equal(self, XPC_ERROR_CONNECTION_INVALID)
|
||||
}
|
||||
|
||||
var isError: Bool {
|
||||
xpc_get_type(self) == XPC_TYPE_ERROR
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
precondition(isError, "not an error")
|
||||
let cstring = xpc_dictionary_get_string(self, XPC_ERROR_KEY_DESCRIPTION)
|
||||
guard let cstring else {
|
||||
return nil
|
||||
}
|
||||
return String(cString: cstring)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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)
|
||||
|
||||
/// Represents a single client connection on the server side.
|
||||
///
|
||||
/// The server creates one `XPCServerSession` per accepted client connection.
|
||||
/// Handlers that need to associate state with a connection (e.g. resource
|
||||
/// tracking for automatic cleanup) receive the session as a parameter and
|
||||
/// may register disconnect handlers via `onDisconnect(_:)`.
|
||||
public actor XPCServerSession {
|
||||
private var disconnectHandlers: [@Sendable () async -> Void] = []
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Register a handler to be called when the client connection closes.
|
||||
public func onDisconnect(_ handler: @Sendable @escaping () async -> Void) {
|
||||
disconnectHandlers.append(handler)
|
||||
}
|
||||
|
||||
func fireDisconnect() async {
|
||||
for handler in disconnectHandlers { await handler() }
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCServerSession: Hashable {
|
||||
public nonisolated static func == (lhs: XPCServerSession, rhs: XPCServerSession) -> Bool {
|
||||
lhs === rhs
|
||||
}
|
||||
|
||||
public nonisolated func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user