From ca136e8f807f0155c03aa89189348d3ef104f7ad Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Mon, 2 Aug 2021 12:39:28 +0100 Subject: [PATCH] Move Channel abstractions to NIOCore. (#1920) Motivation: The most important API surface area in NIO are the Channel abstractions. These are shared in all NIO programs, and are also used by several projects to implement their I/O abstraction. There are several moving parts to this abstraction, all of which are moving: - Channel itself - ChannelPipeline - ChannelHandler As these all move, they force several other pieces of API to move with them. Most notably they force us to move NIOAny, which also forces us to move FileHandle and FileRegion. That also forces us to bring over part of our syscall abstraction. This duplication is acceptable due to its minimal surface area, but it is definitely a flaw in our abstraction design that we had to do that at all. We also need to move the channel option abstraction, AddressedEnvelope, and the DeadChannel. Modifications: - Moved a bunch of the Channel abstraction over. - Moved Channel-associated types. Result: Channel will be part of NIOCore. --- Sources/NIO/BaseStreamSocketChannel.swift | 2 +- Sources/NIO/Bootstrap.swift | 2 +- Sources/NIO/Embedded.swift | 5 +- Sources/NIO/FileDescriptor.swift | 22 +-- ...{Interfaces.swift => NetworkDevices.swift} | 163 +--------------- Sources/NIO/SelectableChannel.swift | 65 +++++++ Sources/NIO/SocketChannel.swift | 4 +- Sources/NIO/Utilities.swift | 2 +- .../{NIO => NIOCore}/AddressedEnvelope.swift | 0 Sources/{NIO => NIOCore}/Channel.swift | 52 +----- Sources/{NIO => NIOCore}/ChannelHandler.swift | 0 Sources/{NIO => NIOCore}/ChannelInvoker.swift | 0 Sources/{NIO => NIOCore}/ChannelOption.swift | 0 .../{NIO => NIOCore}/ChannelPipeline.swift | 0 Sources/{NIO => NIOCore}/DeadChannel.swift | 0 Sources/NIOCore/FileDescriptor.swift | 34 ++++ Sources/{NIO => NIOCore}/FileHandle.swift | 13 +- Sources/{NIO => NIOCore}/FileRegion.swift | 12 +- Sources/NIOCore/IO.swift | 23 +++ Sources/{NIO => NIOCore}/IOData.swift | 0 Sources/NIOCore/Interfaces.swift | 175 ++++++++++++++++++ Sources/{NIO => NIOCore}/NIOAny.swift | 0 Sources/NIOCore/SystemCallHelpers.swift | 130 +++++++++++++ .../TypeAssistedChannelHandler.swift | 0 .../ByteToMessageDecoderVerifierTest.swift | 5 +- Tests/NIOTests/BaseObjectsTest.swift | 4 +- Tests/NIOTests/ChannelPipelineTest.swift | 5 +- Tests/NIOTests/ChannelTests.swift | 8 +- .../PendingDatagramWritesManagerTests.swift | 4 +- 29 files changed, 472 insertions(+), 258 deletions(-) rename Sources/NIO/{Interfaces.swift => NetworkDevices.swift} (68%) create mode 100644 Sources/NIO/SelectableChannel.swift rename Sources/{NIO => NIOCore}/AddressedEnvelope.swift (100%) rename Sources/{NIO => NIOCore}/Channel.swift (89%) rename Sources/{NIO => NIOCore}/ChannelHandler.swift (100%) rename Sources/{NIO => NIOCore}/ChannelInvoker.swift (100%) rename Sources/{NIO => NIOCore}/ChannelOption.swift (100%) rename Sources/{NIO => NIOCore}/ChannelPipeline.swift (100%) rename Sources/{NIO => NIOCore}/DeadChannel.swift (100%) create mode 100644 Sources/NIOCore/FileDescriptor.swift rename Sources/{NIO => NIOCore}/FileHandle.swift (94%) rename Sources/{NIO => NIOCore}/FileRegion.swift (92%) rename Sources/{NIO => NIOCore}/IOData.swift (100%) create mode 100644 Sources/NIOCore/Interfaces.swift rename Sources/{NIO => NIOCore}/NIOAny.swift (100%) create mode 100644 Sources/NIOCore/SystemCallHelpers.swift rename Sources/{NIO => NIOCore}/TypeAssistedChannelHandler.swift (100%) diff --git a/Sources/NIO/BaseStreamSocketChannel.swift b/Sources/NIO/BaseStreamSocketChannel.swift index 89dc9510..cb467c52 100644 --- a/Sources/NIO/BaseStreamSocketChannel.swift +++ b/Sources/NIO/BaseStreamSocketChannel.swift @@ -247,7 +247,7 @@ class BaseStreamSocketChannel: BaseSocketChannel return } - let data = data.forceAsIOData() + let data = self.unwrapData(data, as: IOData.self) if !self.pendingWrites.add(data: data, promise: promise) { self.pipeline.syncOperations.fireChannelWritabilityChanged() diff --git a/Sources/NIO/Bootstrap.swift b/Sources/NIO/Bootstrap.swift index 8f96fdd4..667be61e 100644 --- a/Sources/NIO/Bootstrap.swift +++ b/Sources/NIO/Bootstrap.swift @@ -362,7 +362,7 @@ public final class ServerBootstrap { ctxEventLoop.assertInEventLoop() future.flatMap { (_) -> EventLoopFuture in ctxEventLoop.assertInEventLoop() - guard !context.pipeline.destroyed else { + guard context.channel.isActive else { return context.eventLoop.makeFailedFuture(ChannelError.ioOnClosedChannel) } context.fireChannelRead(data) diff --git a/Sources/NIO/Embedded.swift b/Sources/NIO/Embedded.swift index 940fe824..b88f2d3e 100644 --- a/Sources/NIO/Embedded.swift +++ b/Sources/NIO/Embedded.swift @@ -237,7 +237,8 @@ class EmbeddedChannelCore: ChannelCore { } deinit { - assert(self.pipeline.destroyed, "leaked an open EmbeddedChannel, maybe forgot to call channel.finish()?") + assert(!self.isOpen && !self.isActive, + "leaked an open EmbeddedChannel, maybe forgot to call channel.finish()?") isOpen = false closePromise.succeed(()) } @@ -631,7 +632,7 @@ public final class EmbeddedChannel: Channel { } let elem = buffer.removeFirst() guard let t = self._channelCore.tryUnwrapData(elem, as: T.self) else { - throw WrongTypeError(expected: T.self, actual: type(of: elem.forceAs(type: Any.self))) + throw WrongTypeError(expected: T.self, actual: type(of: self._channelCore.tryUnwrapData(elem, as: Any.self)!)) } return t } diff --git a/Sources/NIO/FileDescriptor.swift b/Sources/NIO/FileDescriptor.swift index 8d808183..450c8f10 100644 --- a/Sources/NIO/FileDescriptor.swift +++ b/Sources/NIO/FileDescriptor.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,26 +12,6 @@ // //===----------------------------------------------------------------------===// -public protocol FileDescriptor { - - /// Will be called with the file descriptor if still open, if not it will - /// throw an `IOError`. - /// - /// The ownership of the file descriptor must not escape the `body` as it's completely managed by the - /// implementation of the `FileDescriptor` protocol. - /// - /// - parameters: - /// - body: The closure to execute if the `FileDescriptor` is still open. - /// - throws: If either the `FileDescriptor` was closed before or the closure throws by itself. - func withUnsafeFileDescriptor(_ body: (CInt) throws -> T) throws -> T - - /// `true` if this `FileDescriptor` is open (which means it was not closed yet). - var isOpen: Bool { get } - - /// Close this `FileDescriptor`. - func close() throws -} - extension FileDescriptor { internal static func setNonBlocking(fileDescriptor: CInt) throws { let flags = try Posix.fcntl(descriptor: fileDescriptor, command: F_GETFL, value: 0) diff --git a/Sources/NIO/Interfaces.swift b/Sources/NIO/NetworkDevices.swift similarity index 68% rename from Sources/NIO/Interfaces.swift rename to Sources/NIO/NetworkDevices.swift index ca34f7f7..077110e5 100644 --- a/Sources/NIO/Interfaces.swift +++ b/Sources/NIO/NetworkDevices.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -11,12 +11,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -// -// Interfaces.swift -// NIO -// -// Created by Cory Benfield on 27/02/2018. -// #if os(Linux) || os(FreeBSD) || os(Android) import CNIOLinux @@ -371,158 +365,3 @@ extension NIONetworkDevice: Hashable { hasher.combine(self.interfaceIndex) } } - -/// A representation of a single network interface on a system. -@available(*, deprecated, renamed: "NIONetworkDevice") -public final class NIONetworkInterface { - // This is a class because in almost all cases this will carry - // four structs that are backed by classes, and so will incur 4 - // refcount operations each time it is copied. - - /// The name of the network interface. - public let name: String - - /// The address associated with the given network interface. - public let address: SocketAddress - - /// The netmask associated with this address, if any. - public let netmask: SocketAddress? - - /// The broadcast address associated with this socket interface, if it has one. Some - /// interfaces do not, especially those that have a `pointToPointDestinationAddress`. - public let broadcastAddress: SocketAddress? - - /// The address of the peer on a point-to-point interface, if this is one. Some - /// interfaces do not have such an address: most of those have a `broadcastAddress` - /// instead. - public let pointToPointDestinationAddress: SocketAddress? - - /// If the Interface supports Multicast - public let multicastSupported: Bool - - /// The index of the interface, as provided by `if_nametoindex`. - public let interfaceIndex: Int - - /// Create a brand new network interface. - /// - /// This constructor will fail if NIO does not understand the format of the underlying - /// socket address family. This is quite common: for example, Linux will return AF_PACKET - /// addressed interfaces on most platforms, which NIO does not currently understand. -#if os(Windows) - internal init?(_ pAdapter: UnsafeMutablePointer, - _ pAddress: UnsafeMutablePointer) { - self.name = String(decodingCString: pAdapter.pointee.FriendlyName, - as: UTF16.self) - - guard let address = pAddress.pointee.Address.lpSockaddr.convert() else { - return nil - } - self.address = address - - // TODO: convert the prefix length to the mask itself - let v4mask: (UINT8) -> SocketAddress? = { _ in - var buffer: [CChar] = - Array(repeating: 0, count: Int(INET_ADDRSTRLEN)) - var mask: sockaddr_in = sockaddr_in() - mask.sin_family = ADDRESS_FAMILY(AF_INET) - _ = buffer.withUnsafeMutableBufferPointer { - try! NIOBSDSocket.inet_ntop(af: .inet, src: &mask, - dst: $0.baseAddress!, - size: INET_ADDRSTRLEN) - } - return SocketAddress(mask) - } - let v6mask: (UINT8) -> SocketAddress? = { _ in - var buffer: [CChar] = - Array(repeating: 0, count: Int(INET6_ADDRSTRLEN)) - var mask: sockaddr_in6 = sockaddr_in6() - mask.sin6_family = ADDRESS_FAMILY(AF_INET6) - _ = buffer.withUnsafeMutableBufferPointer { - try! NIOBSDSocket.inet_ntop(af: .inet6, src: &mask, - dst: $0.baseAddress!, - size: INET6_ADDRSTRLEN) - } - return SocketAddress(mask) - } - - switch pAddress.pointee.Address.lpSockaddr.pointee.sa_family { - case ADDRESS_FAMILY(AF_INET): - self.netmask = v4mask(pAddress.pointee.OnLinkPrefixLength) - self.interfaceIndex = Int(pAdapter.pointee.IfIndex) - case ADDRESS_FAMILY(AF_INET6): - self.netmask = v6mask(pAddress.pointee.OnLinkPrefixLength) - self.interfaceIndex = Int(pAdapter.pointee.Ipv6IfIndex) - default: - return nil - } - - // TODO(compnerd) handle broadcast/ppp/multicast information - self.broadcastAddress = nil - self.pointToPointDestinationAddress = nil - self.multicastSupported = false - } -#else - internal init?(_ caddr: ifaddrs) { - self.name = String(cString: caddr.ifa_name) - - guard caddr.ifa_addr != nil else { - return nil - } - - guard let address = caddr.ifa_addr!.convert() else { - return nil - } - self.address = address - - if let netmask = caddr.ifa_netmask { - self.netmask = netmask.convert() - } else { - self.netmask = nil - } - - if (caddr.ifa_flags & UInt32(IFF_BROADCAST)) != 0, let addr = caddr.broadaddr { - self.broadcastAddress = addr.convert() - self.pointToPointDestinationAddress = nil - } else if (caddr.ifa_flags & UInt32(IFF_POINTOPOINT)) != 0, let addr = caddr.dstaddr { - self.broadcastAddress = nil - self.pointToPointDestinationAddress = addr.convert() - } else { - self.broadcastAddress = nil - self.pointToPointDestinationAddress = nil - } - - if (caddr.ifa_flags & UInt32(IFF_MULTICAST)) != 0 { - self.multicastSupported = true - } else { - self.multicastSupported = false - } - - do { - self.interfaceIndex = Int(try Posix.if_nametoindex(caddr.ifa_name)) - } catch { - return nil - } - } -#endif -} - -@available(*, deprecated, renamed: "NIONetworkDevice") -extension NIONetworkInterface: CustomDebugStringConvertible { - public var debugDescription: String { - let baseString = "Interface \(self.name): address \(self.address)" - let maskString = self.netmask != nil ? " netmask \(self.netmask!)" : "" - return baseString + maskString - } -} - -@available(*, deprecated, renamed: "NIONetworkDevice") -extension NIONetworkInterface: Equatable { - public static func ==(lhs: NIONetworkInterface, rhs: NIONetworkInterface) -> Bool { - return lhs.name == rhs.name && - lhs.address == rhs.address && - lhs.netmask == rhs.netmask && - lhs.broadcastAddress == rhs.broadcastAddress && - lhs.pointToPointDestinationAddress == rhs.pointToPointDestinationAddress && - lhs.interfaceIndex == rhs.interfaceIndex - } -} diff --git a/Sources/NIO/SelectableChannel.swift b/Sources/NIO/SelectableChannel.swift new file mode 100644 index 00000000..0e8050ba --- /dev/null +++ b/Sources/NIO/SelectableChannel.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOConcurrencyHelpers + +/// A `SelectableChannel` is a `Channel` that can be used with a `Selector` which notifies a user when certain events +/// are possible. On UNIX a `Selector` is usually an abstraction of `select`, `poll`, `epoll` or `kqueue`. +/// +/// - warning: `SelectableChannel` methods and properties are _not_ thread-safe (unless they also belong to `Channel`). +internal protocol SelectableChannel: Channel { + /// The type of the `Selectable`. A `Selectable` is usually wrapping a file descriptor that can be registered in a + /// `Selector`. + associatedtype SelectableType: Selectable + + var isOpen: Bool { get } + + /// The event(s) of interest. + var interestedEvent: SelectorEventSet { get } + + /// Called when the `SelectableChannel` is ready to be written. + func writable() + + /// Called when the `SelectableChannel` is ready to be read. + func readable() + + /// Called when the read side of the `SelectableChannel` hit EOF. + func readEOF() + + /// Called when the write side of the `SelectableChannel` hit EOF. + func writeEOF() + + /// Called when the `SelectableChannel` was reset (ie. is now unusable) + func reset() + + func register(selector: Selector, interested: SelectorEventSet) throws + + func deregister(selector: Selector, mode: CloseMode) throws + + func reregister(selector: Selector, interested: SelectorEventSet) throws +} + +/// Multicast is not supported on this interface. +public struct NIOMulticastNotSupportedError: Error { + public var device: NIONetworkDevice + + public init(device: NIONetworkDevice) { + self.device = device + } +} + +/// Multicast has not been properly implemented on this channel. +public struct NIOMulticastNotImplementedError: Error { + public init() {} +} diff --git a/Sources/NIO/SocketChannel.swift b/Sources/NIO/SocketChannel.swift index a5c4c822..6fdc7c7f 100644 --- a/Sources/NIO/SocketChannel.swift +++ b/Sources/NIO/SocketChannel.swift @@ -296,7 +296,7 @@ final class ServerSocketChannel: BaseSocketChannel { override public func channelRead0(_ data: NIOAny) { self.eventLoop.assertInEventLoop() - let ch = data.forceAsOther() as SocketChannel + let ch = self.unwrapData(data, as: SocketChannel.self) ch.eventLoop.execute { ch.register().flatMapThrowing { guard ch.isOpen else { @@ -669,7 +669,7 @@ final class DatagramChannel: BaseSocketChannel { } /// Buffer a write in preparation for a flush. override func bufferPendingWrite(data: NIOAny, promise: EventLoopPromise?) { - let data = data.forceAsByteEnvelope() + let data = self.unwrapData(data, as: AddressedEnvelope.self) if !self.pendingWrites.add(envelope: data, promise: promise) { assert(self.isActive) diff --git a/Sources/NIO/Utilities.swift b/Sources/NIO/Utilities.swift index fb2a9f0f..d5d360f1 100644 --- a/Sources/NIO/Utilities.swift +++ b/Sources/NIO/Utilities.swift @@ -123,7 +123,7 @@ public enum System { } while let concreteInterface = interface { - if let nioInterface = NIONetworkInterface(concreteInterface.pointee) { + if let nioInterface = NIONetworkInterface._construct(from: concreteInterface.pointee) { interfaces.append(nioInterface) } interface = concreteInterface.pointee.ifa_next diff --git a/Sources/NIO/AddressedEnvelope.swift b/Sources/NIOCore/AddressedEnvelope.swift similarity index 100% rename from Sources/NIO/AddressedEnvelope.swift rename to Sources/NIOCore/AddressedEnvelope.swift diff --git a/Sources/NIO/Channel.swift b/Sources/NIOCore/Channel.swift similarity index 89% rename from Sources/NIO/Channel.swift rename to Sources/NIOCore/Channel.swift index ece3f12c..833c7d87 100644 --- a/Sources/NIO/Channel.swift +++ b/Sources/NIOCore/Channel.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -166,42 +166,6 @@ public protocol NIOSynchronousChannelOptions { func getOption(_ option: Option) throws -> Option.Value } -/// A `SelectableChannel` is a `Channel` that can be used with a `Selector` which notifies a user when certain events -/// are possible. On UNIX a `Selector` is usually an abstraction of `select`, `poll`, `epoll` or `kqueue`. -/// -/// - warning: `SelectableChannel` methods and properties are _not_ thread-safe (unless they also belong to `Channel`). -internal protocol SelectableChannel: Channel { - /// The type of the `Selectable`. A `Selectable` is usually wrapping a file descriptor that can be registered in a - /// `Selector`. - associatedtype SelectableType: Selectable - - var isOpen: Bool { get } - - /// The event(s) of interest. - var interestedEvent: SelectorEventSet { get } - - /// Called when the `SelectableChannel` is ready to be written. - func writable() - - /// Called when the `SelectableChannel` is ready to be read. - func readable() - - /// Called when the read side of the `SelectableChannel` hit EOF. - func readEOF() - - /// Called when the write side of the `SelectableChannel` hit EOF. - func writeEOF() - - /// Called when the `SelectableChannel` was reset (ie. is now unusable) - func reset() - - func register(selector: Selector, interested: SelectorEventSet) throws - - func deregister(selector: Selector, mode: CloseMode) throws - - func reregister(selector: Selector, interested: SelectorEventSet) throws -} - /// Default implementations which will start on the head of the `ChannelPipeline`. extension Channel { @@ -414,20 +378,6 @@ extension ChannelError: Equatable { } /// The removal of a `ChannelHandler` using `ChannelPipeline.removeHandler` has been attempted more than once. public struct NIOAttemptedToRemoveHandlerMultipleTimesError: Error {} -/// Multicast is not supported on this interface. -public struct NIOMulticastNotSupportedError: Error { - public var device: NIONetworkDevice - - public init(device: NIONetworkDevice) { - self.device = device - } -} - -/// Multicast has not been properly implemented on this channel. -public struct NIOMulticastNotImplementedError: Error { - public init() {} -} - /// An `Channel` related event that is passed through the `ChannelPipeline` to notify the user. public enum ChannelEvent: Equatable { /// `ChannelOptions.allowRemoteHalfClosure` is `true` and input portion of the `Channel` was closed. diff --git a/Sources/NIO/ChannelHandler.swift b/Sources/NIOCore/ChannelHandler.swift similarity index 100% rename from Sources/NIO/ChannelHandler.swift rename to Sources/NIOCore/ChannelHandler.swift diff --git a/Sources/NIO/ChannelInvoker.swift b/Sources/NIOCore/ChannelInvoker.swift similarity index 100% rename from Sources/NIO/ChannelInvoker.swift rename to Sources/NIOCore/ChannelInvoker.swift diff --git a/Sources/NIO/ChannelOption.swift b/Sources/NIOCore/ChannelOption.swift similarity index 100% rename from Sources/NIO/ChannelOption.swift rename to Sources/NIOCore/ChannelOption.swift diff --git a/Sources/NIO/ChannelPipeline.swift b/Sources/NIOCore/ChannelPipeline.swift similarity index 100% rename from Sources/NIO/ChannelPipeline.swift rename to Sources/NIOCore/ChannelPipeline.swift diff --git a/Sources/NIO/DeadChannel.swift b/Sources/NIOCore/DeadChannel.swift similarity index 100% rename from Sources/NIO/DeadChannel.swift rename to Sources/NIOCore/DeadChannel.swift diff --git a/Sources/NIOCore/FileDescriptor.swift b/Sources/NIOCore/FileDescriptor.swift new file mode 100644 index 00000000..b97d874b --- /dev/null +++ b/Sources/NIOCore/FileDescriptor.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public protocol FileDescriptor { + + /// Will be called with the file descriptor if still open, if not it will + /// throw an `IOError`. + /// + /// The ownership of the file descriptor must not escape the `body` as it's completely managed by the + /// implementation of the `FileDescriptor` protocol. + /// + /// - parameters: + /// - body: The closure to execute if the `FileDescriptor` is still open. + /// - throws: If either the `FileDescriptor` was closed before or the closure throws by itself. + func withUnsafeFileDescriptor(_ body: (CInt) throws -> T) throws -> T + + /// `true` if this `FileDescriptor` is open (which means it was not closed yet). + var isOpen: Bool { get } + + /// Close this `FileDescriptor`. + func close() throws +} + diff --git a/Sources/NIO/FileHandle.swift b/Sources/NIOCore/FileHandle.swift similarity index 94% rename from Sources/NIO/FileHandle.swift rename to Sources/NIOCore/FileHandle.swift index 22b9c43f..afdbce6d 100644 --- a/Sources/NIO/FileHandle.swift +++ b/Sources/NIOCore/FileHandle.swift @@ -11,6 +11,13 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +#if os(Windows) +import ucrt +#elseif os(macOS) || os(iOS) || os(tvOS) || os(watchOS) +import Darwin +#elseif os(Linux) || os(Android) +import Glibc +#endif /// A `NIOFileHandle` is a handle to an open file. /// @@ -45,7 +52,7 @@ public final class NIOFileHandle: FileDescriptor { /// - returns: A new `NIOFileHandle` with a fresh underlying file descriptor but shared seek pointer. public func duplicate() throws -> NIOFileHandle { return try withUnsafeFileDescriptor { fd in - NIOFileHandle(descriptor: try Posix.dup(descriptor: fd)) + NIOFileHandle(descriptor: try SystemCalls.dup(descriptor: fd)) } } @@ -66,7 +73,7 @@ public final class NIOFileHandle: FileDescriptor { public func close() throws { try withUnsafeFileDescriptor { fd in - try Posix.close(descriptor: fd) + try SystemCalls.close(descriptor: fd) } self.isOpen = false @@ -141,7 +148,7 @@ extension NIOFileHandle { /// - mode: Access mode. Default mode is `.read`. /// - flags: Additional POSIX flags. public convenience init(path: String, mode: Mode = .read, flags: Flags = .default) throws { - let fd = try Posix.open(file: path, oFlag: mode.posixFlags | O_CLOEXEC | flags.posixFlags, mode: flags.posixMode) + let fd = try SystemCalls.open(file: path, oFlag: mode.posixFlags | O_CLOEXEC | flags.posixFlags, mode: flags.posixMode) self.init(descriptor: fd) } diff --git a/Sources/NIO/FileRegion.swift b/Sources/NIOCore/FileRegion.swift similarity index 92% rename from Sources/NIO/FileRegion.swift rename to Sources/NIOCore/FileRegion.swift index b945a03c..c787ba72 100644 --- a/Sources/NIO/FileRegion.swift +++ b/Sources/NIOCore/FileRegion.swift @@ -11,6 +11,14 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +#if os(Windows) +import ucrt +#elseif os(macOS) || os(iOS) || os(tvOS) || os(watchOS) +import Darwin +#elseif os(Linux) || os(Android) +import Glibc +#endif + /// A `FileRegion` represent a readable portion usually created to be sent over the network. /// @@ -80,8 +88,8 @@ extension FileRegion { /// - fileHandle: An open `NIOFileHandle` to the file. public init(fileHandle: NIOFileHandle) throws { let eof = try fileHandle.withUnsafeFileDescriptor { (fd: CInt) throws -> off_t in - let eof = try Posix.lseek(descriptor: fd, offset: 0, whence: SEEK_END) - try Posix.lseek(descriptor: fd, offset: 0, whence: SEEK_SET) + let eof = try SystemCalls.lseek(descriptor: fd, offset: 0, whence: SEEK_END) + try SystemCalls.lseek(descriptor: fd, offset: 0, whence: SEEK_SET) return eof } self.init(fileHandle: fileHandle, readerIndex: 0, endIndex: Int(eof)) diff --git a/Sources/NIOCore/IO.swift b/Sources/NIOCore/IO.swift index 71a81daf..da62e5ca 100644 --- a/Sources/NIOCore/IO.swift +++ b/Sources/NIOCore/IO.swift @@ -115,3 +115,26 @@ extension IOError: CustomStringConvertible { return reasonForError(errnoCode: self.errnoCode, reason: self.failureDescription) } } + +// FIXME: Duplicated with NIO. +/// An result for an IO operation that was done on a non-blocking resource. +enum CoreIOResult: Equatable { + + /// Signals that the IO operation could not be completed as otherwise we would need to block. + case wouldBlock(T) + + /// Signals that the IO operation was completed. + case processed(T) +} + +internal extension CoreIOResult where T: FixedWidthInteger { + var result: T { + switch self { + case .processed(let value): + return value + case .wouldBlock(_): + fatalError("cannot unwrap CoreIOResult") + } + } +} + diff --git a/Sources/NIO/IOData.swift b/Sources/NIOCore/IOData.swift similarity index 100% rename from Sources/NIO/IOData.swift rename to Sources/NIOCore/IOData.swift diff --git a/Sources/NIOCore/Interfaces.swift b/Sources/NIOCore/Interfaces.swift new file mode 100644 index 00000000..60be6aee --- /dev/null +++ b/Sources/NIOCore/Interfaces.swift @@ -0,0 +1,175 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if os(Linux) || os(FreeBSD) || os(Android) +import Glibc +import CNIOLinux +#elseif os(macOS) || os(iOS) || os(tvOS) || os(watchOS) +import Darwin +#elseif os(Windows) +import let WinSDK.AF_INET +import let WinSDK.AF_INET6 + +import let WinSDK.INET_ADDRSTRLEN +import let WinSDK.INET6_ADDRSTRLEN + +import struct WinSDK.ADDRESS_FAMILY +import struct WinSDK.IP_ADAPTER_ADDRESSES +import struct WinSDK.IP_ADAPTER_UNICAST_ADDRESS + +import typealias WinSDK.UINT8 +#endif + +#if !os(Windows) +private extension ifaddrs { + var dstaddr: UnsafeMutablePointer? { + #if os(Linux) || os(Android) + return self.ifa_ifu.ifu_dstaddr + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return self.ifa_dstaddr + #endif + } + + var broadaddr: UnsafeMutablePointer? { + #if os(Linux) || os(Android) + return self.ifa_ifu.ifu_broadaddr + #elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return self.ifa_dstaddr + #endif + } +} +#endif + +/// A representation of a single network interface on a system. +@available(*, deprecated, renamed: "NIONetworkDevice") +public final class NIONetworkInterface { + // This is a class because in almost all cases this will carry + // four structs that are backed by classes, and so will incur 4 + // refcount operations each time it is copied. + + /// The name of the network interface. + public let name: String + + /// The address associated with the given network interface. + public let address: SocketAddress + + /// The netmask associated with this address, if any. + public let netmask: SocketAddress? + + /// The broadcast address associated with this socket interface, if it has one. Some + /// interfaces do not, especially those that have a `pointToPointDestinationAddress`. + public let broadcastAddress: SocketAddress? + + /// The address of the peer on a point-to-point interface, if this is one. Some + /// interfaces do not have such an address: most of those have a `broadcastAddress` + /// instead. + public let pointToPointDestinationAddress: SocketAddress? + + /// If the Interface supports Multicast + public let multicastSupported: Bool + + /// The index of the interface, as provided by `if_nametoindex`. + public let interfaceIndex: Int + + fileprivate init?(_ caddr: ifaddrs) { + self.name = String(cString: caddr.ifa_name) + + guard caddr.ifa_addr != nil else { + return nil + } + + guard let address = caddr.ifa_addr!.convert() else { + return nil + } + self.address = address + + if let netmask = caddr.ifa_netmask { + self.netmask = netmask.convert() + } else { + self.netmask = nil + } + + if (caddr.ifa_flags & UInt32(IFF_BROADCAST)) != 0, let addr = caddr.broadaddr { + self.broadcastAddress = addr.convert() + self.pointToPointDestinationAddress = nil + } else if (caddr.ifa_flags & UInt32(IFF_POINTOPOINT)) != 0, let addr = caddr.dstaddr { + self.broadcastAddress = nil + self.pointToPointDestinationAddress = addr.convert() + } else { + self.broadcastAddress = nil + self.pointToPointDestinationAddress = nil + } + + if (caddr.ifa_flags & UInt32(IFF_MULTICAST)) != 0 { + self.multicastSupported = true + } else { + self.multicastSupported = false + } + + do { + self.interfaceIndex = Int(try SystemCalls.if_nametoindex(caddr.ifa_name)) + } catch { + return nil + } + } + + // This is public just so we can avoid needing to pull over any of the System helpers: they can + // construct this type directly. Ideally, we'd have avoided even needing this in NIOCore. + public static func _construct(from caddr: ifaddrs) -> NIONetworkInterface? { + return NIONetworkInterface(caddr) + } +} + +@available(*, deprecated, renamed: "NIONetworkDevice") +extension NIONetworkInterface: CustomDebugStringConvertible { + public var debugDescription: String { + let baseString = "Interface \(self.name): address \(self.address)" + let maskString = self.netmask != nil ? " netmask \(self.netmask!)" : "" + return baseString + maskString + } +} + +@available(*, deprecated, renamed: "NIONetworkDevice") +extension NIONetworkInterface: Equatable { + public static func ==(lhs: NIONetworkInterface, rhs: NIONetworkInterface) -> Bool { + return lhs.name == rhs.name && + lhs.address == rhs.address && + lhs.netmask == rhs.netmask && + lhs.broadcastAddress == rhs.broadcastAddress && + lhs.pointToPointDestinationAddress == rhs.pointToPointDestinationAddress && + lhs.interfaceIndex == rhs.interfaceIndex + } +} + +/// A helper extension for working with sockaddr pointers. +extension UnsafeMutablePointer where Pointee == sockaddr { + /// Converts the `sockaddr` to a `SocketAddress`. + fileprivate func convert() -> SocketAddress? { + switch NIOBSDSocket.AddressFamily(rawValue: CInt(pointee.sa_family)) { + case .inet: + return self.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { + SocketAddress($0.pointee) + } + case .inet6: + return self.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { + SocketAddress($0.pointee) + } + case .unix: + return self.withMemoryRebound(to: sockaddr_un.self, capacity: 1) { + SocketAddress($0.pointee) + } + default: + return nil + } + } +} diff --git a/Sources/NIO/NIOAny.swift b/Sources/NIOCore/NIOAny.swift similarity index 100% rename from Sources/NIO/NIOAny.swift rename to Sources/NIOCore/NIOAny.swift diff --git a/Sources/NIOCore/SystemCallHelpers.swift b/Sources/NIOCore/SystemCallHelpers.swift new file mode 100644 index 00000000..35a6425c --- /dev/null +++ b/Sources/NIOCore/SystemCallHelpers.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// This file contains code that ensures errno is captured correctly when doing syscalls and no ARC traffic can happen inbetween that *could* change the errno +// value before we were able to read it. +// It's important that all static methods are declared with `@inline(never)` so it's not possible any ARC traffic happens while we need to read errno. +// +// Created by Norman Maurer on 11/10/17. +// +// This file arguably shouldn't be here in NIOCore, but due to early design decisions we accidentally exposed a few types that +// know about system calls into the core API (looking at you, FileHandle). As a result we need support for a small number of system calls. +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin.C +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +import CNIOWindows +#else +#error("bad os") +#endif + +private let sysDup: @convention(c) (CInt) -> CInt = dup +private let sysClose: @convention(c) (CInt) -> CInt = close +private let sysOpenWithMode: @convention(c) (UnsafePointer, CInt, mode_t) -> CInt = open +private let sysLseek: @convention(c) (CInt, off_t, CInt) -> off_t = lseek +private let sysIfNameToIndex: @convention(c) (UnsafePointer?) -> CUnsignedInt = if_nametoindex + +private func isUnacceptableErrno(_ code: Int32) -> Bool { + switch code { + case EFAULT, EBADF: + return true + default: + return false + } +} + +private func preconditionIsNotUnacceptableErrno(err: CInt, where function: String) -> Void { + // strerror is documented to return "Unknown error: ..." for illegal value so it won't ever fail + precondition(!isUnacceptableErrno(err), "unacceptable errno \(err) \(String(cString: strerror(err)!)) in \(function))") +} + +/* + * Sorry, we really try hard to not use underscored attributes. In this case + * however we seem to break the inlining threshold which makes a system call + * take twice the time, ie. we need this exception. + */ +@inline(__always) +@discardableResult +internal func syscall(blocking: Bool, + where function: String = #function, + _ body: () throws -> T) + throws -> CoreIOResult { + while true { + let res = try body() + if res == -1 { + let err = errno + switch (err, blocking) { + case (EINTR, _): + continue + case (EWOULDBLOCK, true): + return .wouldBlock(0) + default: + preconditionIsNotUnacceptableErrno(err: err, where: function) + throw IOError(errnoCode: err, reason: function) + } + } + return .processed(res) + } +} + +enum SystemCalls { + @discardableResult + @inline(never) + internal static func dup(descriptor: CInt) throws -> CInt { + return try syscall(blocking: false) { + sysDup(descriptor) + }.result + } + + @inline(never) + internal static func close(descriptor: CInt) throws { + let res = sysClose(descriptor) + if res == -1 { + let err = errno + + // There is really nothing "sane" we can do when EINTR was reported on close. + // So just ignore it and "assume" everything is fine == we closed the file descriptor. + // + // For more details see: + // - https://bugs.chromium.org/p/chromium/issues/detail?id=269623 + // - https://lwn.net/Articles/576478/ + if err != EINTR { + preconditionIsNotUnacceptableErrno(err: err, where: #function) + throw IOError(errnoCode: err, reason: "close") + } + } + } + + @inline(never) + internal static func open(file: UnsafePointer, oFlag: CInt, mode: mode_t) throws -> CInt { + return try syscall(blocking: false) { + sysOpenWithMode(file, oFlag, mode) + }.result + } + + @discardableResult + @inline(never) + internal static func lseek(descriptor: CInt, offset: off_t, whence: CInt) throws -> off_t { + return try syscall(blocking: false) { + sysLseek(descriptor, offset, whence) + }.result + } + + @inline(never) + internal static func if_nametoindex(_ name: UnsafePointer?) throws -> CUnsignedInt { + return try syscall(blocking: false) { + sysIfNameToIndex(name) + }.result + } +} diff --git a/Sources/NIO/TypeAssistedChannelHandler.swift b/Sources/NIOCore/TypeAssistedChannelHandler.swift similarity index 100% rename from Sources/NIO/TypeAssistedChannelHandler.swift rename to Sources/NIOCore/TypeAssistedChannelHandler.swift diff --git a/Tests/NIOTestUtilsTests/ByteToMessageDecoderVerifierTest.swift b/Tests/NIOTestUtilsTests/ByteToMessageDecoderVerifierTest.swift index 18f15253..b5f0ae32 100644 --- a/Tests/NIOTestUtilsTests/ByteToMessageDecoderVerifierTest.swift +++ b/Tests/NIOTestUtilsTests/ByteToMessageDecoderVerifierTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2019-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,7 +12,8 @@ // //===----------------------------------------------------------------------===// -@testable import NIO +@testable import NIOCore +import NIO import NIOTestUtils import XCTest diff --git a/Tests/NIOTests/BaseObjectsTest.swift b/Tests/NIOTests/BaseObjectsTest.swift index 0a53f99d..d30cc40d 100644 --- a/Tests/NIOTests/BaseObjectsTest.swift +++ b/Tests/NIOTests/BaseObjectsTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import XCTest -@testable import NIO +@testable import NIOCore class BaseObjectTest: XCTestCase { func testNIOByteBufferConversion() { diff --git a/Tests/NIOTests/ChannelPipelineTest.swift b/Tests/NIOTests/ChannelPipelineTest.swift index 6b461e41..dd4c40b4 100644 --- a/Tests/NIOTests/ChannelPipelineTest.swift +++ b/Tests/NIOTests/ChannelPipelineTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -14,7 +14,8 @@ import XCTest import NIOConcurrencyHelpers -@testable import NIO +@testable import NIOCore +import NIO import NIOTestUtils private final class IndexWritingHandler: ChannelDuplexHandler { diff --git a/Tests/NIOTests/ChannelTests.swift b/Tests/NIOTests/ChannelTests.swift index 3470f703..cda177ca 100644 --- a/Tests/NIOTests/ChannelTests.swift +++ b/Tests/NIOTests/ChannelTests.swift @@ -254,7 +254,7 @@ public final class ChannelTests: XCTestCase { expectedSingleWritabilities: [Int]?, expectedVectorWritabilities: [[Int]]?, expectedFileWritabilities: [(Int, Int)]?, - returns: [IOResult], + returns: [NIO.IOResult], promiseStates: [[Bool]], file: StaticString = #file, line: UInt = #line) throws -> OverallWriteResult { @@ -2028,7 +2028,7 @@ public final class ChannelTests: XCTestCase { init(protocolFamily: NIOBSDSocket.ProtocolFamily) throws { try super.init(protocolFamily: protocolFamily, type: .stream, setNonBlocking: true) } - override func read(pointer: UnsafeMutableRawBufferPointer) throws -> IOResult { + override func read(pointer: UnsafeMutableRawBufferPointer) throws -> NIO.IOResult { defer { self.firstReadHappened = true } @@ -2443,11 +2443,11 @@ public final class ChannelTests: XCTestCase { try super.init(protocolFamily: .inet, type: .stream, setNonBlocking: true) } - override func write(pointer: UnsafeRawBufferPointer) throws -> IOResult { + override func write(pointer: UnsafeRawBufferPointer) throws -> NIO.IOResult { throw IOError(errnoCode: ETXTBSY, reason: "WriteAlwaysFailingSocket.write fake error") } - override func writev(iovecs: UnsafeBufferPointer) throws -> IOResult { + override func writev(iovecs: UnsafeBufferPointer) throws -> NIO.IOResult { throw IOError(errnoCode: ETXTBSY, reason: "WriteAlwaysFailingSocket.writev fake error") } } diff --git a/Tests/NIOTests/PendingDatagramWritesManagerTests.swift b/Tests/NIOTests/PendingDatagramWritesManagerTests.swift index a247fa66..2df84ea3 100644 --- a/Tests/NIOTests/PendingDatagramWritesManagerTests.swift +++ b/Tests/NIOTests/PendingDatagramWritesManagerTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -109,7 +109,7 @@ class PendingDatagramWritesManagerTests: XCTestCase { promises: [EventLoopPromise], expectedSingleWritabilities: [(Int, SocketAddress)]?, expectedVectorWritabilities: [[(Int, SocketAddress)]]?, - returns: [Result, Error>], + returns: [Result, Error>], promiseStates: [[Bool]], file: StaticString = #file, line: UInt = #line) throws -> OverallWriteResult {