Files
wehub-resource-sync 680845cb1c
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

771 lines
24 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization 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(Linux)
// NOTE: Ideally this should live in ContainerizationOS/Linux, or just ContainerizationCgroups
// or something similar, but it's not there yet. It does what we need, but it'd need a lot more
// features and testing before it's ready to be public.
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
import LCShim
import ContainerizationOCI
import ContainerizationOS
import Foundation
import Logging
package enum Cgroup2Controller: String {
case pids
case memory
case cpuset
case cpu
case io
case hugetlb
}
// Extremely simple cgroup manager. Our needs are simple for now, and this is
// reflected in the type.
public struct Cgroup2Manager: Sendable {
public static let defaultMountPoint = URL(filePath: "/sys/fs/cgroup")
private static let killFile = "cgroup.kill"
private static let procsFile = "cgroup.procs"
private static let subtreeControlFile = "cgroup.subtree_control"
private static let cg2Magic = 0x6367_7270
private let mountPoint: URL
private let path: URL
private let logger: Logger?
package init(
mountPoint: URL = Self.defaultMountPoint,
group: URL,
logger: Logger? = nil
) {
self.mountPoint = mountPoint
self.path = mountPoint.appending(path: group.path)
self.logger = logger
}
public static func load(
mountPoint: URL = Self.defaultMountPoint,
group: URL,
logger: Logger? = nil
) throws -> Cgroup2Manager {
let path = mountPoint.appending(path: group.path)
var s = statfs()
let res = statfs(path.path, &s)
if res != 0 {
throw Error.errno(errno: errno, message: "failed to statfs \(path.path)")
}
if Int64(s.f_type) != Self.cg2Magic {
throw Error.notCgroup
}
return Cgroup2Manager(
mountPoint: mountPoint,
group: group,
logger: logger
)
}
package static func loadFromPid(pid: Int32, logger: Logger? = nil) throws -> Cgroup2Manager {
let procCgPath = URL(filePath: "/proc/\(pid)/cgroup")
let fh = try FileHandle(forReadingFrom: procCgPath)
guard let data = try fh.readToEnd() else {
throw Error.errno(errno: errno, message: "failed to read \(procCgPath)")
}
// If this fails we have bigger problems.
let str = String(data: data, encoding: .utf8)!
let parts = str.split(separator: ":")
if parts[0] != "0" {
throw Error.cgroup1
}
// We should really read /proc/pid/mountinfo, but for now just assume
// it's always at /sys/fs/cgroup.
let path = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
return Cgroup2Manager(group: URL(filePath: String(path)), logger: logger)
}
package func create(perms: Int16 = 0o755) throws {
self.logger?.info(
"creating cgroup manager",
metadata: [
"mountpoint": "\(self.mountPoint.path)",
"path": "\(self.path.path)",
])
try FileManager.default.createDirectory(
at: self.path,
withIntermediateDirectories: true,
attributes: [.posixPermissions: perms]
)
}
private static func writeValue(path: URL, value: String, fileName: String) throws {
let file = path.appending(path: fileName)
let fd = open(file.path, O_WRONLY, 0)
if fd == -1 {
throw Error.errno(errno: errno, message: "failed to open \(file.path)")
}
defer { close(fd) }
let bytes = Array(value.utf8)
let res = Syscall.retrying {
bytes.withUnsafeBytes { write(fd, $0.baseAddress!, bytes.count) }
}
if res == -1 {
throw Error.errno(errno: errno, message: "failed to write to \(file.path)")
}
}
package func toggleSubtreeControllers(controllers: [Cgroup2Controller], enable: Bool) throws {
let value = controllers.map { (enable ? "+" : "-") + $0.rawValue }.joined(separator: " ")
let mountComponents = self.mountPoint.pathComponents
let pathComponents = self.path.pathComponents
// First ensure it's set on the root.
var current = self.mountPoint
try Self.writeValue(
path: current,
value: value,
fileName: Self.subtreeControlFile
)
// Toggle everything except the leaf, as otherwise we won't be able to write
// to cgroup.procs, and what fun is that :)
if mountComponents.count < pathComponents.count - 1 {
for i in mountComponents.count...pathComponents.count - 2 {
current = current.appending(path: pathComponents[i])
try Self.writeValue(
path: current,
value: value,
fileName: Self.subtreeControlFile
)
}
}
}
package func toggleAllAvailableControllers(enable: Bool) throws {
// Read available controllers from cgroup.controllers
let controllersFile = self.mountPoint.appending(path: "cgroup.controllers")
let controllersContent = try String(contentsOf: controllersFile, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
// Parse controller names and convert to our enum
let availableControllers =
controllersContent
.split(separator: " ")
.compactMap { Cgroup2Controller(rawValue: String($0)) }
if !availableControllers.isEmpty {
try toggleSubtreeControllers(controllers: availableControllers, enable: enable)
}
}
public func addProcess(pid: Int32) throws {
self.logger?.debug(
"adding new proc to cgroup",
metadata: [
"mountpoint": "\(self.mountPoint.path)",
"path": "\(self.path.path)",
])
let pidStr = String(pid)
try Self.writeValue(
path: self.path,
value: pidStr,
fileName: Self.procsFile
)
}
public func applyResources(resources: ContainerizationOCI.LinuxResources) throws {
self.logger?.debug(
"applying cgroup resources",
metadata: [
"path": "\(self.path.path)"
])
if let memory = resources.memory, let limit = memory.limit {
// The OCI spec defines -1 as unlimited; cgroup v2 expects "max".
let value = limit < 0 ? "max" : String(limit)
try Self.writeValue(
path: self.path,
value: value,
fileName: "memory.max"
)
}
if let cpu = resources.cpu, let quota = cpu.quota, let period = cpu.period {
// cpu.max format is "quota period"
let value = "\(quota) \(period)"
try Self.writeValue(
path: self.path,
value: value,
fileName: "cpu.max"
)
}
if let pids = resources.pids {
// The OCI spec defines -1 as unlimited; cgroup v2 expects "max".
let value = pids.limit < 0 ? "max" : String(pids.limit)
try Self.writeValue(
path: self.path,
value: value,
fileName: "pids.max"
)
}
}
package func setMemoryHigh(bytes: UInt64) throws {
self.logger?.debug(
"setting memory.high",
metadata: [
"path": "\(self.path.path)",
"bytes": "\(bytes)",
])
try Self.writeValue(
path: self.path,
value: String(bytes),
fileName: "memory.high"
)
}
package func setMemoryLow(bytes: UInt64) throws {
self.logger?.debug(
"setting memory.low",
metadata: [
"path": "\(self.path.path)",
"bytes": "\(bytes)",
]
)
try Self.writeValue(
path: self.path,
value: String(bytes),
fileName: "memory.low")
}
package func getMemoryEvents() throws -> MemoryEvents {
let content = try readFileContent(fileName: "memory.events")
let values = parseKeyValuePairs(content)
return MemoryEvents(
low: values["low"] ?? 0,
high: values["high"] ?? 0,
max: values["max"] ?? 0,
oom: values["oom"] ?? 0,
oomKill: values["oom_kill"] ?? 0
)
}
package func getMemoryEventsPath() -> String {
self.path.appending(path: "memory.events").path
}
package func kill() throws {
try Self.writeValue(
path: self.path,
value: "1",
fileName: Self.killFile
)
}
package func delete(force: Bool = false) throws {
self.logger?.info(
"deleting cgroup manager",
metadata: [
"mountpoint": "\(self.mountPoint.path)",
"path": "\(self.path.path)",
])
if force {
try self.kill()
}
// Recursively remove child cgroups first
try removeChildCgroups(at: self.path, force: force)
let result = rmdir(self.path.path)
if result != 0 {
throw Error.errno(errno: errno, message: "failed to remove cgroup directory \(self.path.path)")
}
}
private func removeChildCgroups(at path: URL, force: Bool) throws {
let fileManager = FileManager.default
guard let contents = try? fileManager.contentsOfDirectory(atPath: path.path) else {
return
}
// Remove child directories (potential nested cgroups) first
for item in contents {
let childPath = path.appending(path: item)
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: childPath.path, isDirectory: &isDirectory) && isDirectory.boolValue {
if force {
try Self.writeValue(
path: childPath,
value: "1",
fileName: Self.killFile
)
}
try removeChildCgroups(at: childPath, force: force)
let result = rmdir(childPath.path)
if result != 0 {
throw Error.errno(errno: errno, message: "failed to remove child cgroup \(childPath.path)")
}
}
}
}
package func stats(_ categories: Cgroup2StatsCategory = .all) throws -> Cgroup2Stats {
Cgroup2Stats(
pids: categories.contains(.pids) ? try self.readPidsStats() : nil,
memory: categories.contains(.memory) ? try self.readMemoryStats() : nil,
cpu: categories.contains(.cpu) ? try self.readCPUStats() : nil,
io: categories.contains(.io) ? try self.readIOStats() : nil
)
}
private func readFileContent(fileName: String) throws -> String? {
let filePath = self.path.appending(path: fileName)
guard FileManager.default.fileExists(atPath: filePath.path) else {
return nil
}
return try String(contentsOf: filePath, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func parseSingleValue(_ content: String?) -> UInt64? {
guard let content = content, !content.isEmpty else { return nil }
if content == "max" {
return UInt64.max
}
return UInt64(content)
}
private func parseKeyValuePairs(_ content: String?) -> [String: UInt64] {
guard let content = content else { return [:] }
var result: [String: UInt64] = [:]
for line in content.components(separatedBy: .newlines) {
let parts = line.components(separatedBy: .whitespaces)
if parts.count == 2, let value = UInt64(parts[1]) {
result[parts[0]] = value
}
}
return result
}
private func readPidsStats() throws -> PidsStats? {
guard let currentContent = try readFileContent(fileName: "pids.current"),
let current = parseSingleValue(currentContent)
else {
return nil
}
let maxContent = try readFileContent(fileName: "pids.max")
let max = parseSingleValue(maxContent)
return PidsStats(current: current, max: max)
}
private func readMemoryStats() throws -> MemoryStats? {
guard let usageContent = try readFileContent(fileName: "memory.current"),
let usage = parseSingleValue(usageContent)
else {
return nil
}
let usageLimit = parseSingleValue(try readFileContent(fileName: "memory.max"))
let swapUsage = parseSingleValue(try readFileContent(fileName: "memory.swap.current"))
let swapLimit = parseSingleValue(try readFileContent(fileName: "memory.swap.max"))
let statContent = try readFileContent(fileName: "memory.stat")
let statValues = parseKeyValuePairs(statContent)
return MemoryStats(
usage: usage,
usageLimit: usageLimit,
swapUsage: swapUsage,
swapLimit: swapLimit,
anon: statValues["anon"] ?? 0,
file: statValues["file"] ?? 0,
kernelStack: statValues["kernel_stack"] ?? 0,
slab: statValues["slab"] ?? 0,
sock: statValues["sock"] ?? 0,
shmem: statValues["shmem"] ?? 0,
fileMapped: statValues["file_mapped"] ?? 0,
fileDirty: statValues["file_dirty"] ?? 0,
fileWriteback: statValues["file_writeback"] ?? 0,
pgfault: statValues["pgfault"] ?? 0,
pgmajfault: statValues["pgmajfault"] ?? 0,
workingsetRefaultAnon: statValues["workingset_refault_anon"] ?? 0,
workingsetRefaultFile: statValues["workingset_refault_file"] ?? 0,
workingsetActivate: statValues["workingset_activate"] ?? 0,
workingsetNodereclaim: statValues["workingset_nodereclaim"] ?? 0,
pgstealKswapd: statValues["pgsteal_kswapd"] ?? 0,
pgstealDirect: statValues["pgsteal_direct"] ?? 0,
pgstealKhugepaged: statValues["pgsteal_khugepaged"] ?? 0,
inactiveAnon: statValues["inactive_anon"] ?? 0,
activeAnon: statValues["active_anon"] ?? 0,
inactiveFile: statValues["inactive_file"] ?? 0,
activeFile: statValues["active_file"] ?? 0
)
}
private func readCPUStats() throws -> CPUStats? {
let statContent = try readFileContent(fileName: "cpu.stat")
let statValues = parseKeyValuePairs(statContent)
guard !statValues.isEmpty else {
return nil
}
return CPUStats(
usageUsec: statValues["usage_usec"] ?? 0,
userUsec: statValues["user_usec"] ?? 0,
systemUsec: statValues["system_usec"] ?? 0,
nrPeriods: statValues["nr_periods"] ?? 0,
nrThrottled: statValues["nr_throttled"] ?? 0,
throttledUsec: statValues["throttled_usec"] ?? 0
)
}
private func readIOStats() throws -> IOStats? {
guard let statContent = try readFileContent(fileName: "io.stat") else {
return IOStats(entries: [])
}
var entries: [IOEntry] = []
for line in statContent.components(separatedBy: .newlines) {
guard !line.isEmpty else { continue }
let parts = line.components(separatedBy: .whitespaces)
guard parts.count >= 2 else { continue }
let deviceParts = parts[0].components(separatedBy: ":")
guard deviceParts.count == 2,
let major = UInt64(deviceParts[0]),
let minor = UInt64(deviceParts[1])
else {
continue
}
var rbytes: UInt64 = 0
var wbytes: UInt64 = 0
var rios: UInt64 = 0
var wios: UInt64 = 0
var dbytes: UInt64 = 0
var dios: UInt64 = 0
for i in 1..<parts.count {
let keyValue = parts[i].components(separatedBy: "=")
guard keyValue.count == 2, let value = UInt64(keyValue[1]) else { continue }
switch keyValue[0] {
case "rbytes":
rbytes = value
case "wbytes":
wbytes = value
case "rios":
rios = value
case "wios":
wios = value
case "dbytes":
dbytes = value
case "dios":
dios = value
default:
break
}
}
entries.append(
IOEntry(
major: major,
minor: minor,
rbytes: rbytes,
wbytes: wbytes,
rios: rios,
wios: wios,
dbytes: dbytes,
dios: dios
))
}
return IOStats(entries: entries)
}
}
// Selects which cgroup stat groups to read.
package struct Cgroup2StatsCategory: OptionSet, Sendable {
package let rawValue: UInt8
package init(rawValue: UInt8) {
self.rawValue = rawValue
}
package static let pids = Cgroup2StatsCategory(rawValue: 1 << 0)
package static let memory = Cgroup2StatsCategory(rawValue: 1 << 1)
package static let cpu = Cgroup2StatsCategory(rawValue: 1 << 2)
package static let io = Cgroup2StatsCategory(rawValue: 1 << 3)
package static let all: Cgroup2StatsCategory = [.pids, .memory, .cpu, .io]
}
package struct Cgroup2Stats: Sendable {
package var pids: PidsStats?
package var memory: MemoryStats?
package var cpu: CPUStats?
package var io: IOStats?
package init(
pids: PidsStats? = nil,
memory: MemoryStats? = nil,
cpu: CPUStats? = nil,
io: IOStats? = nil
) {
self.pids = pids
self.memory = memory
self.cpu = cpu
self.io = io
}
}
package struct PidsStats: Sendable {
package var current: UInt64
package var max: UInt64?
package init(current: UInt64, max: UInt64? = nil) {
self.current = current
self.max = max
}
}
package struct MemoryStats: Sendable {
package var usage: UInt64
package var usageLimit: UInt64?
package var swapUsage: UInt64?
package var swapLimit: UInt64?
package var anon: UInt64
package var file: UInt64
package var kernelStack: UInt64
package var slab: UInt64
package var sock: UInt64
package var shmem: UInt64
package var fileMapped: UInt64
package var fileDirty: UInt64
package var fileWriteback: UInt64
package var pgfault: UInt64
package var pgmajfault: UInt64
package var workingsetRefaultAnon: UInt64
package var workingsetRefaultFile: UInt64
package var workingsetActivate: UInt64
package var workingsetNodereclaim: UInt64
package var pgstealKswapd: UInt64
package var pgstealDirect: UInt64
package var pgstealKhugepaged: UInt64
package var inactiveAnon: UInt64
package var activeAnon: UInt64
package var inactiveFile: UInt64
package var activeFile: UInt64
package init(
usage: UInt64,
usageLimit: UInt64? = nil,
swapUsage: UInt64? = nil,
swapLimit: UInt64? = nil,
anon: UInt64 = 0,
file: UInt64 = 0,
kernelStack: UInt64 = 0,
slab: UInt64 = 0,
sock: UInt64 = 0,
shmem: UInt64 = 0,
fileMapped: UInt64 = 0,
fileDirty: UInt64 = 0,
fileWriteback: UInt64 = 0,
pgfault: UInt64 = 0,
pgmajfault: UInt64 = 0,
workingsetRefaultAnon: UInt64 = 0,
workingsetRefaultFile: UInt64 = 0,
workingsetActivate: UInt64 = 0,
workingsetNodereclaim: UInt64 = 0,
pgstealKswapd: UInt64 = 0,
pgstealDirect: UInt64 = 0,
pgstealKhugepaged: UInt64 = 0,
inactiveAnon: UInt64 = 0,
activeAnon: UInt64 = 0,
inactiveFile: UInt64 = 0,
activeFile: UInt64 = 0
) {
self.usage = usage
self.usageLimit = usageLimit
self.swapUsage = swapUsage
self.swapLimit = swapLimit
self.anon = anon
self.file = file
self.kernelStack = kernelStack
self.slab = slab
self.sock = sock
self.shmem = shmem
self.fileMapped = fileMapped
self.fileDirty = fileDirty
self.fileWriteback = fileWriteback
self.pgfault = pgfault
self.pgmajfault = pgmajfault
self.workingsetRefaultAnon = workingsetRefaultAnon
self.workingsetRefaultFile = workingsetRefaultFile
self.workingsetActivate = workingsetActivate
self.workingsetNodereclaim = workingsetNodereclaim
self.pgstealKswapd = pgstealKswapd
self.pgstealDirect = pgstealDirect
self.pgstealKhugepaged = pgstealKhugepaged
self.inactiveAnon = inactiveAnon
self.activeAnon = activeAnon
self.inactiveFile = inactiveFile
self.activeFile = activeFile
}
}
package struct CPUStats: Sendable {
package var usageUsec: UInt64
package var userUsec: UInt64
package var systemUsec: UInt64
package var nrPeriods: UInt64
package var nrThrottled: UInt64
package var throttledUsec: UInt64
package init(
usageUsec: UInt64 = 0,
userUsec: UInt64 = 0,
systemUsec: UInt64 = 0,
nrPeriods: UInt64 = 0,
nrThrottled: UInt64 = 0,
throttledUsec: UInt64 = 0
) {
self.usageUsec = usageUsec
self.userUsec = userUsec
self.systemUsec = systemUsec
self.nrPeriods = nrPeriods
self.nrThrottled = nrThrottled
self.throttledUsec = throttledUsec
}
}
package struct IOStats: Sendable {
package var entries: [IOEntry]
package init(entries: [IOEntry] = []) {
self.entries = entries
}
}
package struct IOEntry: Sendable {
package var major: UInt64
package var minor: UInt64
package var rbytes: UInt64
package var wbytes: UInt64
package var rios: UInt64
package var wios: UInt64
package var dbytes: UInt64
package var dios: UInt64
package init(
major: UInt64,
minor: UInt64,
rbytes: UInt64 = 0,
wbytes: UInt64 = 0,
rios: UInt64 = 0,
wios: UInt64 = 0,
dbytes: UInt64 = 0,
dios: UInt64 = 0
) {
self.major = major
self.minor = minor
self.rbytes = rbytes
self.wbytes = wbytes
self.rios = rios
self.wios = wios
self.dbytes = dbytes
self.dios = dios
}
}
package struct MemoryEvents: Sendable {
package var low: UInt64
package var high: UInt64
package var max: UInt64
package var oom: UInt64
package var oomKill: UInt64
package init(
low: UInt64 = 0,
high: UInt64 = 0,
max: UInt64 = 0,
oom: UInt64 = 0,
oomKill: UInt64 = 0
) {
self.low = low
self.high = high
self.max = max
self.oom = oom
self.oomKill = oomKill
}
}
extension Cgroup2Manager {
package enum Error: Swift.Error, CustomStringConvertible {
case notCgroup
case cgroup1
case errno(errno: Int32, message: String)
case notExist(path: String)
package var description: String {
switch self {
case .errno(let errno, let message):
return "failed with errno \(errno): \(message)"
case .notExist(let path):
return "cgroup at path \(path) does not exist"
case .cgroup1:
return "tried to load a cgroup v1 path"
case .notCgroup:
return "path is not a cgroup mountpoint"
}
}
}
}
#endif