Files
container/Sources/ContainerizationOS/Socket/Socket.swift
T
Danny Canter 6bc4bf5124 Source code documentation updates (#9)
This change adds documentation to quite a few existing public types that
didn't have a blurb before.

Additionally, this fixes a couple things that I think either didn't make
sense when going to document them:
- Rename ConnectionStream to VsockConnectionStream. This type only
functions for vsock connections.
- Deletes NsLock+Closure. This was not used anywhere.
- Rename ContainerizationOCI/Config.swift to ImageConfig.swift.

Signed-off-by: Danny Canter <danny_canter@apple.com>
2025-06-05 16:16:18 -07:00

384 lines
11 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the containerization project authors. All rights reserved.
//
// 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 Foundation
import SendableProperty
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#else
#error("Socket not supported on this platform.")
#endif
#if !os(Windows)
let sysFchmod = fchmod
let sysRead = read
let sysUnlink = unlink
let sysSend = send
let sysClose = close
let sysShutdown = shutdown
let sysBind = bind
let sysSocket = socket
let sysSetsockopt = setsockopt
let sysGetsockopt = getsockopt
let sysListen = listen
let sysAccept = accept
let sysConnect = connect
let sysIoctl: @convention(c) (CInt, CUnsignedLong, UnsafeMutableRawPointer) -> CInt = ioctl
#endif
/// Thread-safe socket wrapper.
public final class Socket: Sendable {
public enum TimeoutOption {
case send
case receive
}
public enum ShutdownOption {
case read
case write
case readWrite
}
private enum SocketState {
case created
case connected
case listening
}
private struct State {
let socketState: SocketState
let handle: FileHandle?
let type: SocketType
let acceptSource: DispatchSourceRead?
}
private let _closeOnDeinit: Bool
private let _queue: DispatchQueue
@SendableProperty
private var _state: State
public var fileDescriptor: Int32 {
guard let handle = _state.handle else {
return -1
}
return handle.fileDescriptor
}
public convenience init(type: SocketType, closeOnDeinit: Bool = true) throws {
let sockFD = sysSocket(type.domain, type.type, 0)
if sockFD < 0 {
throw SocketError.withErrno("failed to create socket: \(sockFD)", errno: errno)
}
self.init(fd: sockFD, type: type, closeOnDeinit: closeOnDeinit)
}
init(fd: Int32, type: SocketType, closeOnDeinit: Bool) {
_queue = DispatchQueue(label: "com.apple.containerization.socket")
_closeOnDeinit = closeOnDeinit
_state = State(
socketState: .created,
handle: FileHandle(fileDescriptor: fd, closeOnDealloc: false),
type: type,
acceptSource: nil
)
}
deinit {
if _closeOnDeinit {
try? close()
}
}
}
extension Socket {
static func errnoToError(msg: String) -> SocketError {
SocketError.withErrno("\(msg) (\(_errnoString(errno)))", errno: errno)
}
public func connect() throws {
guard let handle = _state.handle else {
throw SocketError.closed
}
guard _state.socketState == .created else {
throw SocketError.invalidOperationOnSocket("connect")
}
var res: Int32 = 0
try _state.type.withSockAddr { (ptr, length) in
res = Syscall.retrying {
sysConnect(handle.fileDescriptor, ptr, length)
}
}
if res == -1 {
throw Socket.errnoToError(msg: "could not connect to socket \(_state.type)")
}
_state = State(
socketState: .connected,
handle: handle,
type: _state.type,
acceptSource: _state.acceptSource
)
}
public func listen() throws {
guard let handle = _state.handle else {
throw SocketError.closed
}
guard _state.socketState == .created else {
throw SocketError.invalidOperationOnSocket("listen")
}
try _state.type.beforeBind(fd: handle.fileDescriptor)
var rc: Int32 = 0
try _state.type.withSockAddr { (ptr, length) in
rc = sysBind(handle.fileDescriptor, ptr, length)
}
if rc < 0 {
throw Socket.errnoToError(msg: "could not bind to \(_state.type)")
}
try _state.type.beforeListen(fd: handle.fileDescriptor)
if sysListen(handle.fileDescriptor, SOMAXCONN) < 0 {
throw Socket.errnoToError(msg: "listen failed on \(_state.type)")
}
_state = State(
socketState: .listening,
handle: handle,
type: _state.type,
acceptSource: _state.acceptSource
)
}
public func close() throws {
// Already closed.
guard let handle = _state.handle else {
return
}
if let acceptSource = _state.acceptSource {
acceptSource.cancel()
}
try handle.close()
_state = State(
socketState: _state.socketState,
handle: nil,
type: _state.type,
acceptSource: nil
)
}
public func write(data: any DataProtocol) throws -> Int {
guard _state.socketState == .connected else {
throw SocketError.invalidOperationOnSocket("write")
}
guard let handle = _state.handle else {
throw SocketError.closed
}
if data.isEmpty {
return 0
}
try handle.write(contentsOf: data)
return data.count
}
public func acceptStream(closeOnDeinit: Bool = true) throws -> AsyncThrowingStream<Socket, Swift.Error> {
guard _state.socketState == .listening else {
throw SocketError.invalidOperationOnSocket("accept")
}
guard let handle = _state.handle else {
throw SocketError.closed
}
guard _state.acceptSource == nil else {
throw SocketError.acceptStreamExists
}
let source = DispatchSource.makeReadSource(
fileDescriptor: handle.fileDescriptor,
queue: _queue
)
_state = State(
socketState: _state.socketState,
handle: handle,
type: _state.type,
acceptSource: source
)
return AsyncThrowingStream { cont in
source.setCancelHandler {
cont.finish()
}
source.setEventHandler(handler: {
if source.data == 0 {
source.cancel()
return
}
do {
let connection = try self.accept(closeOnDeinit: closeOnDeinit)
cont.yield(connection)
} catch SocketError.closed {
source.cancel()
} catch {
cont.yield(with: .failure(error))
source.cancel()
}
})
source.activate()
}
}
public func accept(closeOnDeinit: Bool = true) throws -> Socket {
guard _state.socketState == .listening else {
throw SocketError.invalidOperationOnSocket("accept")
}
guard let handle = _state.handle else {
throw SocketError.closed
}
let (clientFD, socketType) = try _state.type.accept(fd: handle.fileDescriptor)
return Socket(
fd: clientFD,
type: socketType,
closeOnDeinit: closeOnDeinit
)
}
public func read(buffer: inout Data) throws -> Int {
guard _state.socketState == .connected else {
throw SocketError.invalidOperationOnSocket("read")
}
guard let handle = _state.handle else {
throw SocketError.closed
}
var bytesRead = 0
let bufferSize = buffer.count
try buffer.withUnsafeMutableBytes { pointer in
guard let baseAddress = pointer.baseAddress else {
throw SocketError.missingBaseAddress
}
bytesRead = Syscall.retrying {
sysRead(handle.fileDescriptor, baseAddress, bufferSize)
}
if bytesRead < 0 {
throw Socket.errnoToError(msg: "Error reading from connection")
} else if bytesRead == 0 {
throw SocketError.closed
}
}
return bytesRead
}
public func shutdown(how: ShutdownOption) throws {
guard let handle = _state.handle else {
throw SocketError.closed
}
var howOpt: Int32 = 0
switch how {
case .read:
howOpt = Int32(SHUT_RD)
case .write:
howOpt = Int32(SHUT_WR)
case .readWrite:
howOpt = Int32(SHUT_RDWR)
}
if sysShutdown(handle.fileDescriptor, howOpt) < 0 {
throw Socket.errnoToError(msg: "shutdown failed")
}
}
public func setSockOpt(sockOpt: Int32 = 0, ptr: UnsafeRawPointer, stride: UInt32) throws {
guard let handle = _state.handle else {
throw SocketError.closed
}
if setsockopt(handle.fileDescriptor, SOL_SOCKET, sockOpt, ptr, stride) < 0 {
throw Socket.errnoToError(msg: "failed to set sockopt")
}
}
public func setTimeout(option: TimeoutOption, seconds: Int) throws {
guard let handle = _state.handle else {
throw SocketError.closed
}
var sockOpt: Int32 = 0
switch option {
case .receive:
sockOpt = SO_RCVTIMEO
case .send:
sockOpt = SO_SNDTIMEO
}
var timer = timeval()
timer.tv_sec = seconds
timer.tv_usec = 0
if setsockopt(
handle.fileDescriptor,
SOL_SOCKET,
sockOpt,
&timer,
socklen_t(MemoryLayout<timeval>.size)
) < 0 {
throw Socket.errnoToError(msg: "failed to set read timeout")
}
}
static func _errnoString(_ err: Int32?) -> String {
String(validatingCString: strerror(errno)) ?? "error: \(errno)"
}
}
public enum SocketError: Error, Equatable, CustomStringConvertible {
case closed
case acceptStreamExists
case invalidOperationOnSocket(String)
case missingBaseAddress
case withErrno(_ msg: String, errno: Int32)
public var description: String {
switch self {
case .closed:
return "socket: closed"
case .acceptStreamExists:
return "accept stream already exists"
case .invalidOperationOnSocket(let operation):
return "socket: invalid operation on socket '\(operation)'"
case .missingBaseAddress:
return "socket: missing base address"
case .withErrno(let msg, _):
return "socket: error \(msg)"
}
}
}