Files
container/Sources/Services/ContainerSandboxService/Client/ExitMonitor.swift
T
J Logan 356c8d2f88 Reorganize client libraries. (#1020)
- Closes #461.
- Extract core types into ContainerResources target.
- Extract ContainerNetworkServiceClient from ContainerNetworkService.
- Relocate sandbox client from ContainerClient to
ContainerSandboxServiceClient.
- Relocate ContainerClient to ContainerAPIServiceClient.
- Common structure from services and clients under Source/Services.

Updated project hierarchy:

```
Sources/CAuditToken - audit token access wrapper
Sources/CLI - CLI executable
Sources/ContainerBuild - builder
Sources/ContainerCommands - CLI command implementations
Sources/ContainerLog - logging helpers
Sources/ContainerPersistence - persistent data and system property helpers
Sources/ContainerPlugin - plugin system
Sources/ContainerResource - resource (container, image, volume, network) types
Sources/ContainerVersion - version helpers
Sources/ContainerXPC - XPC helpers
Sources/CVersion - injected project version
Sources/DNSServer - container DNS resolver
Sources/Helpers - service executables
Sources/Services/*/Client - service clients
Sources/Services/*/Server - service implementations
Sources/SocketForwarder - port forwarding
Sources/TerminalProgress - progress bar
```

## Type of Change
- [ ] Bug fix
- [ ] New feature  
- [x] Breaking change
- [ ] Documentation update

## Motivation and Context
The ContainerClient library was a bit of a grab bag. This refactor
applies a more sensible project and library structure for resource data
types, services, and clients.

## Testing
- [x] Tested locally
- [x] Added/updated tests
- [ ] Added/updated docs
2026-01-06 08:27:14 -08:00

92 lines
3.6 KiB
Swift

//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
import Containerization
import ContainerizationError
import ContainerizationExtras
import Foundation
import Logging
/// Track when long running work exits, and notify the caller via a callback.
public actor ExitMonitor {
/// A callback that receives the client identifier and exit code.
public typealias ExitCallback = @Sendable (String, ExitStatus) async throws -> Void
/// A function that waits for work to complete, returning an exit code.
public typealias WaitHandler = @Sendable () async throws -> ExitStatus
/// Create a new monitor.
///
/// - Parameters:
/// - log: The destination for log messages.
public init(log: Logger? = nil) {
self.log = log
}
private var exitCallbacks: [String: ExitCallback] = [:]
private var runningTasks: [String: Task<Void, Never>] = [:]
private let log: Logger?
/// Remove tracked work from the monitor.
///
/// - Parameters:
/// - id: The client identifier for the tracked work.
public func stopTracking(id: String) async {
if let task = self.runningTasks[id] {
task.cancel()
}
exitCallbacks.removeValue(forKey: id)
runningTasks.removeValue(forKey: id)
}
/// Register long running work so that the monitor invokes
/// a callback when the work completes.
///
/// - Parameters:
/// - id: The client identifier for the work.
/// - onExit: The callback to invoke when the work completes.
public func registerProcess(id: String, onExit: @escaping ExitCallback) async throws {
guard self.exitCallbacks[id] == nil else {
throw ContainerizationError(.invalidState, message: "ExitMonitor already setup for process \(id)")
}
self.exitCallbacks[id] = onExit
}
/// Await the completion of previously registered item of work.
///
/// - Parameters:
/// - id: The client identifier for the work.
/// - waitingOn: A function that waits for the work to complete,
/// and then returns an exit code.
public func track(id: String, waitingOn: @escaping WaitHandler) async throws {
guard let onExit = self.exitCallbacks[id] else {
throw ContainerizationError(.invalidState, message: "ExitMonitor not setup for process \(id)")
}
guard self.runningTasks[id] == nil else {
throw ContainerizationError(.invalidState, message: "already have a running task tracking process \(id)")
}
self.runningTasks[id] = Task {
do {
let exitStatus = try await waitingOn()
try await onExit(id, exitStatus)
} catch {
self.log?.error("WaitHandler for \(id) threw error \(String(describing: error))")
try? await onExit(id, ExitStatus(exitCode: -1))
}
}
}
}