213 lines
7.1 KiB
Swift
213 lines
7.1 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 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)
|
|
|
|
import ArgumentParser
|
|
import Cgroup
|
|
import Containerization
|
|
import ContainerizationError
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import GRPCCore
|
|
import Logging
|
|
import NIOCore
|
|
import NIOPosix
|
|
|
|
#if os(Linux)
|
|
#if canImport(Musl)
|
|
import Musl
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
#endif
|
|
import LCShim
|
|
#endif
|
|
|
|
public struct AgentCommand: AsyncParsableCommand {
|
|
public static let configuration = CommandConfiguration(
|
|
commandName: "agent",
|
|
abstract: "Run the vminitd agent daemon"
|
|
)
|
|
|
|
private static let foregroundEnvVar = "FOREGROUND"
|
|
public static let vsockPort = 1024
|
|
|
|
@OptionGroup var options: LogLevelOption
|
|
|
|
public init() {}
|
|
|
|
/// Bootstrap the vminitd environment and create an Initd server.
|
|
/// Handles mounts, cgroups, memory monitoring, and all pre-serve setup.
|
|
public static func bootstrap(options: LogLevelOption) async throws -> Initd {
|
|
let log = makeLogger(label: "vminitd", level: options.resolvedLogLevel())
|
|
try adjustLimits(log)
|
|
|
|
// When running under debug mode, launch vminitd as a sub process of pid1
|
|
// so that we get a chance to collect better logs and errors before pid1 exists
|
|
// and the kernel panics.
|
|
#if DEBUG
|
|
log.info("DEBUG mode active, checking FOREGROUND env var")
|
|
let environment = ProcessInfo.processInfo.environment
|
|
let foreground = environment[foregroundEnvVar]
|
|
log.info("checking for shim var \(foregroundEnvVar)=\(String(describing: foreground))")
|
|
|
|
if foreground == nil {
|
|
try runInForeground(log, logLevel: options.logLevel)
|
|
_exit(0)
|
|
}
|
|
|
|
log.info("FOREGROUND is set, running as subprocess, setting subreaper")
|
|
// Since we are not running as pid1 in this mode we must set ourselves
|
|
// as a subreaper so that all child processes are reaped by us and not
|
|
// passed onto our parent.
|
|
CZ_set_sub_reaper()
|
|
#endif
|
|
|
|
signal(SIGPIPE, SIG_IGN)
|
|
|
|
log.info("vminitd booting", metadata: versionMetadata())
|
|
|
|
// Set of mounts necessary to be mounted prior to taking any RPCs.
|
|
// 1. /proc as the sysctl rpc wouldn't make sense if it wasn't there (NOTE: This is done before this method
|
|
// due to Swift seemingly requiring /proc to be present for the async runtime to spin up).
|
|
// 2. /run as that is where we store container state.
|
|
// 3. /sys as we need it for /sys/fs/cgroup
|
|
// 4. /sys/fs/cgroup to add the agent to a cgroup, as well as containers later.
|
|
let mounts = [
|
|
ContainerizationOS.Mount(
|
|
type: "tmpfs",
|
|
source: "tmpfs",
|
|
target: "/run",
|
|
options: []
|
|
),
|
|
ContainerizationOS.Mount(
|
|
type: "sysfs",
|
|
source: "sysfs",
|
|
target: "/sys",
|
|
options: []
|
|
),
|
|
ContainerizationOS.Mount(
|
|
type: "cgroup2",
|
|
source: "none",
|
|
target: "/sys/fs/cgroup",
|
|
options: []
|
|
),
|
|
]
|
|
|
|
for mnt in mounts {
|
|
log.info("mounting \(mnt.target)")
|
|
|
|
try mnt.mount(createWithPerms: 0o755)
|
|
}
|
|
try Binfmt.mount()
|
|
|
|
let cgManager = Cgroup2Manager(
|
|
group: URL(filePath: "/vminitd"),
|
|
logger: log
|
|
)
|
|
try cgManager.create()
|
|
try cgManager.toggleAllAvailableControllers(enable: true)
|
|
|
|
// Set memory.high threshold to 80 MiB
|
|
let high: UInt64 = 80 * 1024 * 1024
|
|
// Set memory.low to 50 MiB to avoid reclaiming vminitd's memory
|
|
let low: UInt64 = 50 * 1024 * 1024
|
|
|
|
try cgManager.setMemoryHigh(bytes: high)
|
|
try cgManager.setMemoryLow(bytes: low)
|
|
try cgManager.addProcess(pid: getpid())
|
|
|
|
let memoryMonitor = try MemoryMonitor(
|
|
cgroupManager: cgManager,
|
|
threshold: high,
|
|
logger: log
|
|
) { [log] (currentUsage, highMark) in
|
|
log.warning(
|
|
"vminitd memory threshold exceeded",
|
|
metadata: [
|
|
"threshold_bytes": "\(high)",
|
|
"current_bytes": "\(currentUsage)",
|
|
"high_events_total": "\(highMark)",
|
|
])
|
|
}
|
|
|
|
let t = Thread { [log] in
|
|
do {
|
|
try memoryMonitor.run()
|
|
} catch {
|
|
log.error("memory monitor failed: \(error)")
|
|
}
|
|
}
|
|
t.start()
|
|
|
|
let eg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
let blockingPool = NIOThreadPool(numberOfThreads: 2)
|
|
blockingPool.start()
|
|
return Initd(log: log, group: eg, blockingPool: blockingPool)
|
|
}
|
|
|
|
public mutating func run() async throws {
|
|
let server = try await Self.bootstrap(options: options)
|
|
|
|
do {
|
|
server.log.info("serving vminitd API")
|
|
try await server.serve(port: Self.vsockPort)
|
|
server.log.info("vminitd API returned, syncing filesystems")
|
|
|
|
#if os(Linux)
|
|
sync()
|
|
#endif
|
|
} catch {
|
|
server.log.error("vminitd boot error \(error)")
|
|
|
|
#if os(Linux)
|
|
sync()
|
|
#endif
|
|
|
|
_exit(1)
|
|
}
|
|
}
|
|
|
|
private static func runInForeground(_ log: Logger, logLevel: String) throws {
|
|
log.info("running vminitd under pid1")
|
|
|
|
var command = Command("/sbin/vminitd", arguments: ["agent", "--log-level", logLevel])
|
|
command.attrs = .init(setsid: true)
|
|
command.stdin = .standardInput
|
|
command.stdout = .standardOutput
|
|
command.stderr = .standardError
|
|
command.environment = ["\(foregroundEnvVar)=1"]
|
|
|
|
try command.start()
|
|
let exitCode = try command.wait()
|
|
log.info("child process exited with code: \(exitCode)")
|
|
}
|
|
|
|
private static func adjustLimits(_ log: Logger) throws {
|
|
let nrOpen = try String(contentsOfFile: "/proc/sys/fs/nr_open", encoding: .utf8)
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let max = UInt64(nrOpen) else {
|
|
throw POSIXError(.EINVAL)
|
|
}
|
|
log.debug("setting RLIMIT_NOFILE to \(max)")
|
|
guard CZ_setrlimit(CZ_RLIMIT_NOFILE, max, max) == 0 else {
|
|
throw POSIXError(.init(rawValue: errno)!)
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|