Files
container/Sources/ContainerCommands/System/SystemStatus.swift
T
Renlord Yang 175e493a6f Fix container system status false negative under a Seatbelt sandbox. (#2316)
- `launchctl list` does not work as expected in the sandbox.
- Probe with `launchctl print <domain>/<label>` instead,
  which launchd does allow a sandboxed process, and key
  off exit codes: 0 means registered, 113 (ENOSERVICE)
  means not registered, and anything else is an error rather
  than being mistranslated into a negative.
- The probe is advisory in `system status`, so an indeterminate
  answer falls through to the XPC health check, whose success 
  is authoritative.

Signed-off-by: Renlord Yang <renlord@apple.com>
2026-10-05 15:52:37 -07:00

231 lines
8.3 KiB
Swift

//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerAPIClient
import ContainerPlugin
import ContainerResource
import ContainerVersion
import ContainerizationError
import Foundation
import Logging
extension Application {
public struct SystemStatus: AsyncLoggableCommand {
public static let configuration = CommandConfiguration(
commandName: "status",
abstract: "Show the status of `container` services and system-wide information"
)
@Option(name: .shortAndLong, help: "Launchd prefix for services")
var prefix: String = "com.apple.container."
@Option(name: .long, help: "Format of the output")
var format: ListFormat = .table
@OptionGroup
public var logOptions: Flags.Logging
public init() {}
public func run() async throws {
let domain = try ServiceManager.getDomainString()
let isRegistered = (try? ServiceManager.isRegistered(fullServiceLabel: "\(domain)/\(prefix)apiserver")) ?? true
if !isRegistered {
try Output.render(payload: StatusPayload(status: "unregistered"), format: format) {
"apiserver is not running and not registered with launchd"
}
Application.exit(withError: ExitCode(1))
}
// Now ping our friendly daemon. Fail after 10 seconds with no response.
do {
let health = try await ClientHealthCheck.ping(timeout: .seconds(10))
let status = await Self.gather(health: health)
try Output.render(payload: status, format: format) {
Self.statusTable(status)
}
} catch {
try Output.render(payload: StatusPayload(status: "not running"), format: format) {
"apiserver is not running"
}
Application.exit(withError: ExitCode(1))
}
}
/// Collects system-wide status from the CLI and the running daemon.
/// Resource counts are populated best-effort and omitted when their
/// source is unavailable.
static func gather(health: SystemHealth) async -> StatusPayload {
let client = ClientInfo(
version: ReleaseVersion.version(),
build: ReleaseVersion.buildType(),
commit: ReleaseVersion.gitCommit() ?? "unspecified",
appName: "container"
)
let host = HostInfo(
architecture: Arch.hostArchitecture().rawValue,
operatingSystem: ProcessInfo.processInfo.operatingSystemVersionString,
cpus: ProcessInfo.processInfo.processorCount
)
let server = ServerInfo(
version: health.apiServerVersion,
build: health.apiServerBuild,
commit: health.apiServerCommit,
appName: health.apiServerAppName
)
let paths = PathInfo(
appRoot: health.appRoot.path(percentEncoded: false),
installRoot: health.installRoot.path(percentEncoded: false),
logRoot: health.logRoot?.string
)
var resources: ResourceCounts? = nil
let containerClient = ContainerClient()
if let all = try? await containerClient.list(filters: ContainerListFilters.all.withoutMachines()) {
let running = all.filter { $0.status == .running }.count
resources = ResourceCounts(
containersTotal: all.count,
containersRunning: running
)
}
if let images = try? await ClientImage.list() {
resources = Self.withImageCount(resources, imageCount: images.count)
}
return StatusPayload(
status: "running",
client: client,
server: server,
host: host,
paths: paths,
resources: resources
)
}
/// Records the image count on the resource counts when both are
/// available. Returns the counts unchanged (including `nil`) when there
/// are no resource counts yet, i.e. the daemon's container list was
/// unavailable.
static func withImageCount(_ resources: ResourceCounts?, imageCount: Int?) -> ResourceCounts? {
guard var resources, let imageCount else { return resources }
resources.images = imageCount
return resources
}
static func statusTable(_ status: StatusPayload) -> String {
var rows: [[String]] = [["FIELD", "VALUE"]]
rows.append(["status", status.status])
if let client = status.client {
rows.append(["client.version", client.version])
rows.append(["client.build", client.build])
rows.append(["client.commit", client.commit])
}
if let host = status.host {
rows.append(["host.os", host.operatingSystem])
rows.append(["host.architecture", host.architecture])
rows.append(["host.cpus", String(host.cpus)])
}
if let server = status.server {
rows.append(["server.version", server.version])
rows.append(["server.build", server.build])
rows.append(["server.commit", server.commit])
rows.append(["server.appName", server.appName])
}
if let paths = status.paths {
rows.append(["paths.appRoot", paths.appRoot])
rows.append(["paths.installRoot", paths.installRoot])
rows.append(["paths.logRoot", paths.logRoot ?? ""])
}
if let resources = status.resources {
rows.append(["containers.total", String(resources.containersTotal)])
rows.append(["containers.running", String(resources.containersRunning)])
if let images = resources.images {
rows.append(["images.total", String(images)])
}
}
return TableOutput(rows: rows).format()
}
}
struct StatusPayload: Codable {
let status: String
let client: ClientInfo?
let server: ServerInfo?
let host: HostInfo?
let paths: PathInfo?
let resources: ResourceCounts?
init(
status: String,
client: ClientInfo? = nil,
server: ServerInfo? = nil,
host: HostInfo? = nil,
paths: PathInfo? = nil,
resources: ResourceCounts? = nil
) {
self.status = status
self.client = client
self.server = server
self.host = host
self.paths = paths
self.resources = resources
}
}
struct ClientInfo: Codable {
let version: String
let build: String
let commit: String
let appName: String
}
struct ServerInfo: Codable {
let version: String
let build: String
let commit: String
let appName: String
}
struct HostInfo: Codable {
let architecture: String
let operatingSystem: String
let cpus: Int
}
struct PathInfo: Codable {
let appRoot: String
let installRoot: String
let logRoot: String?
}
struct ResourceCounts: Codable {
let containersTotal: Int
let containersRunning: Int
var images: Int?
}
}