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

190 lines
5.9 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 ContainerizationError
import ContainerizationOS
import Foundation
import Logging
import Synchronization
final class IOPair: Sendable {
private let io: Mutex<IO>
private let logger: Logger?
private let reason: String
private struct IO {
let from: IOCloser
let to: IOCloser
let buffer: UnsafeMutableBufferPointer<UInt8>
var closed: Bool
var registeredFd: Int32?
func drain() {
let readFrom = OSFile(fd: from.fileDescriptor)
let writeTo = OSFile(fd: to.fileDescriptor)
while true {
let r = readFrom.read(buffer)
if r.read > 0 {
let view = UnsafeMutableBufferPointer(
start: buffer.baseAddress,
count: r.read
)
let w = writeTo.write(view)
if w.wrote != r.read {
return
}
}
switch r.action {
case .eof, .again, .error(_):
return
default:
break
}
}
}
mutating func close(logger: Logger?) {
if self.closed {
return
}
// Try and drain IO first.
self.drain()
// Remove the fd from our global epoll instance first.
if let fd = self.registeredFd {
do {
try ProcessSupervisor.default.unregisterFd(fd)
} catch {
logger?.error("failed to delete fd from epoll \(fd): \(error)")
}
self.registeredFd = nil
}
do {
try self.from.close()
} catch {
logger?.error("failed to close reader fd for IOPair: \(error)")
}
do {
try self.to.close()
} catch {
logger?.error("failed to close writer fd for IOPair: \(error)")
}
self.buffer.deallocate()
self.closed = true
}
}
init(
readFrom: IOCloser,
writeTo: IOCloser,
reason: String,
logger: Logger? = nil
) {
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
self.io = Mutex(
IO(
from: readFrom,
to: writeTo,
buffer: buffer,
closed: false,
registeredFd: nil
))
self.reason = reason
self.logger = logger
}
func relay(ignoreHup: Bool = false) throws {
self.logger?.info("setting up relay for \(reason)")
let (readFromFd, writeToFd) = self.io.withLock { io in
io.registeredFd = io.from.fileDescriptor
return (io.from.fileDescriptor, io.to.fileDescriptor)
}
let readFrom = OSFile(fd: readFromFd)
let writeTo = OSFile(fd: writeToFd)
try ProcessSupervisor.default.registerFd(readFromFd, mask: .input) { mask in
self.io.withLock { io in
if io.closed {
return
}
if mask.isHangup && !mask.readyToRead {
self.logger?.debug("received EPOLLHUP with no EPOLLIN")
if !ignoreHup {
io.close(logger: self.logger)
}
return
}
// Loop so we drain fully.
while true {
let r = readFrom.read(io.buffer)
if r.read > 0 {
let view = UnsafeMutableBufferPointer(
start: io.buffer.baseAddress,
count: r.read
)
let w = writeTo.write(view)
if w.wrote != r.read {
self.logger?.error("stopping relay: short write for stdio")
io.close(logger: self.logger)
return
}
}
switch r.action {
case .error(let errno):
self.logger?.error("failed with errno \(errno) while reading for fd \(readFromFd)")
fallthrough
case .eof:
self.logger?.debug("closing relay for \(readFromFd)")
io.close(logger: self.logger)
return
case .again:
if mask.isHangup && !ignoreHup {
self.logger?.error("received EPOLLHUP and EAGAIN exiting")
self.close()
}
return
default:
break
}
}
}
}
}
func close() {
self.io.withLock { io in
self.logger?.info("closing relay for \(reason)")
io.close(logger: self.logger)
}
}
}
#endif