From c1e2f093e8ca49498cb84498ce081f4fa527bf69 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Fri, 6 Aug 2021 13:42:29 +0100 Subject: [PATCH] Move MulticastChannel and NIONetworkDevice to NIOCore (#1931) Motivation: MultcastChannel is a general abstraction for expressing multicast capabilities on a given Channel. This abstraction doesn't have any particularly tight tie to the POSIX layer, so it belongs in NIOCore. This is also expressed in terms of NIONetworkDevice, so we need to move that over. That also encourages us to bring over System.enumerateDevices, and given that System.coreCount is also fairly general-purpose we may as well bring it along too. Modifications: - Move MulticastChannel to NIOCore - Move NIONetworkDevice to NIOCore - Move System to NIOCore Result: More general-purpose abstractions in NIOCore. --- Sources/NIO/Linux.swift | 54 +-- Sources/NIO/NetworkDevices.swift | 367 ------------------ Sources/NIO/SelectableChannel.swift | 14 - Sources/NIO/System.swift | 8 - Sources/NIO/Utilities.swift | 172 +------- Sources/NIOCore/Interfaces.swift | 325 +++++++++++++++- Sources/NIOCore/Linux.swift | 74 ++++ .../{NIO => NIOCore}/MulticastChannel.swift | 15 + Sources/NIOCore/SystemCallHelpers.swift | 21 + Sources/NIOCore/Utilities.swift | 170 ++++++++ Tests/NIOTests/LinuxTest.swift | 4 +- 11 files changed, 602 insertions(+), 622 deletions(-) delete mode 100644 Sources/NIO/NetworkDevices.swift create mode 100644 Sources/NIOCore/Linux.swift rename Sources/{NIO => NIOCore}/MulticastChannel.swift (95%) diff --git a/Sources/NIO/Linux.swift b/Sources/NIO/Linux.swift index 01e303ae..4d998a0f 100644 --- a/Sources/NIO/Linux.swift +++ b/Sources/NIO/Linux.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 @@ -118,9 +118,6 @@ internal enum Epoll { } internal enum Linux { - static let cfsQuotaPath = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us" - static let cfsPeriodPath = "/sys/fs/cgroup/cpu/cpu.cfs_period_us" - static let cpuSetPath = "/sys/fs/cgroup/cpuset/cpuset.cpus" #if os(Android) static let SOCK_CLOEXEC = Glibc.SOCK_CLOEXEC static let SOCK_NONBLOCK = Glibc.SOCK_NONBLOCK @@ -140,54 +137,5 @@ internal enum Linux { } return fd } - - private static func firstLineOfFile(path: String) throws -> Substring { - let fh = try NIOFileHandle(path: path) - defer { try! fh.close() } - // linux doesn't properly report /sys/fs/cgroup/* files lengths so we use a reasonable limit - var buf = ByteBufferAllocator().buffer(capacity: 1024) - try buf.writeWithUnsafeMutableBytes(minimumWritableBytes: buf.capacity) { ptr in - let res = try fh.withUnsafeFileDescriptor { fd -> IOResult in - return try Posix.read(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count) - } - switch res { - case .processed(let n): - return n - case .wouldBlock: - preconditionFailure("read returned EWOULDBLOCK despite a blocking fd") - } - } - return String(buffer: buf).prefix(while: { $0 != "\n" }) - } - - private static func countCoreIds(cores: Substring) -> Int { - let ids = cores.split(separator: "-", maxSplits: 1) - guard - let first = ids.first.flatMap({ Int($0, radix: 10) }), - let last = ids.last.flatMap({ Int($0, radix: 10) }), - last >= first - else { preconditionFailure("cpuset format is incorrect") } - return 1 + last - first - } - - static func coreCount(cpuset cpusetPath: String) -> Int? { - guard - let cpuset = try? firstLineOfFile(path: cpusetPath).split(separator: ","), - !cpuset.isEmpty - else { return nil } - return cpuset.map(countCoreIds).reduce(0, +) - } - - static func coreCount(quota quotaPath: String, period periodPath: String) -> Int? { - guard - let quota = try? Int(firstLineOfFile(path: quotaPath)), - quota > 0 - else { return nil } - guard - let period = try? Int(firstLineOfFile(path: periodPath)), - period > 0 - else { return nil } - return (quota - 1 + period) / period // always round up if fractional CPU quota requested - } } #endif diff --git a/Sources/NIO/NetworkDevices.swift b/Sources/NIO/NetworkDevices.swift deleted file mode 100644 index 077110e5..00000000 --- a/Sources/NIO/NetworkDevices.swift +++ /dev/null @@ -1,367 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 CNIOLinux -#endif - -#if 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 device on a system. -public struct NIONetworkDevice { - private var backing: Backing - - /// The name of the network device. - public var name: String { - get { - return self.backing.name - } - set { - self.uniquifyIfNeeded() - self.backing.name = newValue - } - } - - /// The address associated with the given network device. - public var address: SocketAddress? { - get { - return self.backing.address - } - set { - self.uniquifyIfNeeded() - self.backing.address = newValue - } - } - - /// The netmask associated with this address, if any. - public var netmask: SocketAddress? { - get { - return self.backing.netmask - } - set { - self.uniquifyIfNeeded() - self.backing.netmask = newValue - } - } - - /// The broadcast address associated with this socket interface, if it has one. Some - /// interfaces do not, especially those that have a `pointToPointDestinationAddress`. - public var broadcastAddress: SocketAddress? { - get { - return self.backing.broadcastAddress - } - set { - self.uniquifyIfNeeded() - self.backing.broadcastAddress = newValue - } - } - - /// 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 var pointToPointDestinationAddress: SocketAddress? { - get { - return self.backing.pointToPointDestinationAddress - } - set { - self.uniquifyIfNeeded() - self.backing.pointToPointDestinationAddress = newValue - } - } - - /// If the Interface supports Multicast - public var multicastSupported: Bool { - get { - return self.backing.multicastSupported - } - set { - self.uniquifyIfNeeded() - self.backing.multicastSupported = newValue - } - } - - /// The index of the interface, as provided by `if_nametoindex`. - public var interfaceIndex: Int { - get { - return self.backing.interfaceIndex - } - set { - self.uniquifyIfNeeded() - self.backing.interfaceIndex = newValue - } - } - - /// 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) { - guard let backing = Backing(pAdapter, pAddress) else { - return nil - } - self.backing = backing - } -#else - internal init?(_ caddr: ifaddrs) { - guard let backing = Backing(caddr) else { - return nil - } - - self.backing = backing - } -#endif - -#if !os(Windows) - /// Convert a `NIONetworkInterface` to a `NIONetworkDevice`. As `NIONetworkDevice`s are a superset of `NIONetworkInterface`s, - /// it is always possible to perform this conversion. - @available(*, deprecated, message: "This is a compatibility helper, and will be removed in a future release") - public init(_ interface: NIONetworkInterface) { - self.backing = Backing( - name: interface.name, - address: interface.address, - netmask: interface.netmask, - broadcastAddress: interface.broadcastAddress, - pointToPointDestinationAddress: interface.pointToPointDestinationAddress, - multicastSupported: interface.multicastSupported, - interfaceIndex: interface.interfaceIndex - ) - } -#endif - - public init(name: String, - address: SocketAddress?, - netmask: SocketAddress?, - broadcastAddress: SocketAddress?, - pointToPointDestinationAddress: SocketAddress, - multicastSupported: Bool, - interfaceIndex: Int) { - self.backing = Backing( - name: name, - address: address, - netmask: netmask, - broadcastAddress: broadcastAddress, - pointToPointDestinationAddress: pointToPointDestinationAddress, - multicastSupported: multicastSupported, - interfaceIndex: interfaceIndex - ) - } - - private mutating func uniquifyIfNeeded() { - if !isKnownUniquelyReferenced(&self.backing) { - self.backing = Backing(copying: self.backing) - } - } -} - -extension NIONetworkDevice { - fileprivate final class Backing { - /// The name of the network interface. - var name: String - - /// The address associated with the given network interface. - var address: SocketAddress? - - /// The netmask associated with this address, if any. - var netmask: SocketAddress? - - /// The broadcast address associated with this socket interface, if it has one. Some - /// interfaces do not, especially those that have a `pointToPointDestinationAddress`. - var 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. - var pointToPointDestinationAddress: SocketAddress? - - /// If the Interface supports Multicast - var multicastSupported: Bool - - /// The index of the interface, as provided by `if_nametoindex`. - var 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) - self.address = pAddress.pointee.Address.lpSockaddr.convert() - - // 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) - break - case ADDRESS_FAMILY(AF_INET6): - self.netmask = v6mask(pAddress.pointee.OnLinkPrefixLength) - self.interfaceIndex = Int(pAdapter.pointee.Ipv6IfIndex) - break - 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) - self.address = caddr.ifa_addr.flatMap { $0.convert() } - self.netmask = caddr.ifa_netmask.flatMap { $0.convert() } - - 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 - } - - self.multicastSupported = (caddr.ifa_flags & UInt32(IFF_MULTICAST)) != 0 - do { - self.interfaceIndex = Int(try Posix.if_nametoindex(caddr.ifa_name)) - } catch { - return nil - } - } -#endif - - init(copying original: Backing) { - self.name = original.name - self.address = original.address - self.netmask = original.netmask - self.broadcastAddress = original.broadcastAddress - self.pointToPointDestinationAddress = original.pointToPointDestinationAddress - self.multicastSupported = original.multicastSupported - self.interfaceIndex = original.interfaceIndex - } - - init(name: String, - address: SocketAddress?, - netmask: SocketAddress?, - broadcastAddress: SocketAddress?, - pointToPointDestinationAddress: SocketAddress?, - multicastSupported: Bool, - interfaceIndex: Int) { - self.name = name - self.address = address - self.netmask = netmask - self.broadcastAddress = broadcastAddress - self.pointToPointDestinationAddress = pointToPointDestinationAddress - self.multicastSupported = multicastSupported - self.interfaceIndex = interfaceIndex - } - } -} - -extension NIONetworkDevice: CustomDebugStringConvertible { - public var debugDescription: String { - let baseString = "Device \(self.name): address \(String(describing: self.address))" - let maskString = self.netmask != nil ? " netmask \(self.netmask!)" : "" - return baseString + maskString - } -} - -// Sadly, as this is class-backed we cannot synthesise the implementation. -extension NIONetworkDevice: Equatable { - public static func ==(lhs: NIONetworkDevice, rhs: NIONetworkDevice) -> 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 - } -} - -extension NIONetworkDevice: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.name) - hasher.combine(self.address) - hasher.combine(self.netmask) - hasher.combine(self.broadcastAddress) - hasher.combine(self.pointToPointDestinationAddress) - hasher.combine(self.interfaceIndex) - } -} diff --git a/Sources/NIO/SelectableChannel.swift b/Sources/NIO/SelectableChannel.swift index 0e8050ba..57f5eb75 100644 --- a/Sources/NIO/SelectableChannel.swift +++ b/Sources/NIO/SelectableChannel.swift @@ -49,17 +49,3 @@ internal protocol SelectableChannel: Channel { 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/System.swift b/Sources/NIO/System.swift index 35589805..92b3cc0b 100644 --- a/Sources/NIO/System.swift +++ b/Sources/NIO/System.swift @@ -99,7 +99,6 @@ private let sysDup: @convention(c) (CInt) -> CInt = dup #if !os(Windows) private let sysGetpeername: @convention(c) (CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt = getpeername private let sysGetsockname: @convention(c) (CInt, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt = getsockname -private let sysGetifaddrs: @convention(c) (UnsafeMutablePointer?>?) -> CInt = getifaddrs #endif private let sysFreeifaddrs: @convention(c) (UnsafeMutablePointer?) -> Void = freeifaddrs private let sysIfNameToIndex: @convention(c) (UnsafePointer?) -> CUnsignedInt = if_nametoindex @@ -488,13 +487,6 @@ internal enum Posix { return sysGetsockname(socket, address, addressLength) } } - - @inline(never) - internal static func getifaddrs(_ addrs: UnsafeMutablePointer?>) throws { - _ = try syscall(blocking: false) { - sysGetifaddrs(addrs) - } - } #endif @inline(never) diff --git a/Sources/NIO/Utilities.swift b/Sources/NIO/Utilities.swift index d5d360f1..43329b98 100644 --- a/Sources/NIO/Utilities.swift +++ b/Sources/NIO/Utilities.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,28 +12,6 @@ // //===----------------------------------------------------------------------===// -#if os(Linux) || os(FreeBSD) || os(Android) -import CNIOLinux -#endif - -#if os(Windows) -import let WinSDK.RelationProcessorCore - -import let WinSDK.AF_UNSPEC -import let WinSDK.ERROR_SUCCESS - -import func WinSDK.GetAdaptersAddresses -import func WinSDK.GetLastError -import func WinSDK.GetLogicalProcessorInformation - -import struct WinSDK.IP_ADAPTER_ADDRESSES -import struct WinSDK.IP_ADAPTER_UNICAST_ADDRESS -import struct WinSDK.SYSTEM_LOGICAL_PROCESSOR_INFORMATION -import struct WinSDK.ULONG - -import typealias WinSDK.DWORD -#endif - /// A utility function that runs the body code only in debug builds, without /// emitting compiler warnings. /// @@ -49,151 +27,3 @@ final class Box { let value: T init(_ value: T) { self.value = value } } - -public enum System { - /// A utility function that returns an estimate of the number of *logical* cores - /// on the system. - /// - /// This value can be used to help provide an estimate of how many threads to use with - /// the `MultiThreadedEventLoopGroup`. The exact ratio between this number and the number - /// of threads to use is a matter for the programmer, and can be determined based on the - /// specific execution behaviour of the program. - /// - /// - returns: The logical core count on the system. - public static var coreCount: Int { -#if os(Windows) - var dwLength: DWORD = 0 - _ = GetLogicalProcessorInformation(nil, &dwLength) - - let alignment: Int = - MemoryLayout.alignment - let pBuffer: UnsafeMutableRawPointer = - UnsafeMutableRawPointer.allocate(byteCount: Int(dwLength), - alignment: alignment) - defer { - pBuffer.deallocate() - } - - let dwSLPICount: Int = - Int(dwLength) / MemoryLayout.stride - let pSLPI: UnsafeMutablePointer = - pBuffer.bindMemory(to: SYSTEM_LOGICAL_PROCESSOR_INFORMATION.self, - capacity: dwSLPICount) - - let bResult: Bool = GetLogicalProcessorInformation(pSLPI, &dwLength) - precondition(bResult, "GetLogicalProcessorInformation: \(GetLastError())") - - return UnsafeBufferPointer(start: pSLPI, - count: dwSLPICount) - .filter { $0.Relationship == RelationProcessorCore } - .map { $0.ProcessorMask.nonzeroBitCount } - .reduce(0, +) -#elseif os(Linux) || os(Android) - if let quota = Linux.coreCount(quota: Linux.cfsQuotaPath, period: Linux.cfsPeriodPath) { - return quota - } else if let cpusetCount = Linux.coreCount(cpuset: Linux.cpuSetPath) { - return cpusetCount - } else { - return sysconf(CInt(_SC_NPROCESSORS_ONLN)) - } -#else - return sysconf(CInt(_SC_NPROCESSORS_ONLN)) -#endif - } - -#if !os(Windows) - /// A utility function that enumerates the available network interfaces on this machine. - /// - /// This function returns values that are true for a brief snapshot in time. These results can - /// change, and the returned values will not change to reflect them. This function must be called - /// again to get new results. - /// - /// - returns: An array of network interfaces available on this machine. - /// - throws: If an error is encountered while enumerating interfaces. - @available(*, deprecated, renamed: "enumerateDevices") - public static func enumerateInterfaces() throws -> [NIONetworkInterface] { - var interfaces: [NIONetworkInterface] = [] - interfaces.reserveCapacity(12) // Arbitrary choice. - - var interface: UnsafeMutablePointer? = nil - try Posix.getifaddrs(&interface) - let originalInterface = interface - defer { - freeifaddrs(originalInterface) - } - - while let concreteInterface = interface { - if let nioInterface = NIONetworkInterface._construct(from: concreteInterface.pointee) { - interfaces.append(nioInterface) - } - interface = concreteInterface.pointee.ifa_next - } - - return interfaces - } -#endif - - /// A utility function that enumerates the available network devices on this machine. - /// - /// This function returns values that are true for a brief snapshot in time. These results can - /// change, and the returned values will not change to reflect them. This function must be called - /// again to get new results. - /// - /// - returns: An array of network devices available on this machine. - /// - throws: If an error is encountered while enumerating interfaces. - public static func enumerateDevices() throws -> [NIONetworkDevice] { - var devices: [NIONetworkDevice] = [] - devices.reserveCapacity(12) // Arbitrary choice. - -#if os(Windows) - var ulSize: ULONG = 0 - _ = GetAdaptersAddresses(ULONG(AF_UNSPEC), 0, nil, nil, &ulSize) - - let stride: Int = MemoryLayout.stride - let pBuffer: UnsafeMutableBufferPointer = - UnsafeMutableBufferPointer.allocate(capacity: Int(ulSize) / stride) - defer { - pBuffer.deallocate() - } - - let ulResult: ULONG = - GetAdaptersAddresses(ULONG(AF_UNSPEC), 0, nil, pBuffer.baseAddress, - &ulSize) - guard ulResult == ERROR_SUCCESS else { - throw IOError(windows: ulResult, reason: "GetAdaptersAddresses") - } - - var pAdapter: UnsafeMutablePointer? = - UnsafeMutablePointer(pBuffer.baseAddress) - while pAdapter != nil { - let pUnicastAddresses: UnsafeMutablePointer? = - pAdapter!.pointee.FirstUnicastAddress - var pUnicastAddress: UnsafeMutablePointer? = - pUnicastAddresses - while pUnicastAddress != nil { - if let device = NIONetworkDevice(pAdapter!, pUnicastAddress!) { - devices.append(device) - } - pUnicastAddress = pUnicastAddress!.pointee.Next - } - pAdapter = pAdapter!.pointee.Next - } -#else - var interface: UnsafeMutablePointer? = nil - try Posix.getifaddrs(&interface) - let originalInterface = interface - defer { - freeifaddrs(originalInterface) - } - - while let concreteInterface = interface { - if let nioInterface = NIONetworkDevice(concreteInterface.pointee) { - devices.append(nioInterface) - } - interface = concreteInterface.pointee.ifa_next - } - -#endif - return devices - } -} diff --git a/Sources/NIOCore/Interfaces.swift b/Sources/NIOCore/Interfaces.swift index 60be6aee..fedf9873 100644 --- a/Sources/NIOCore/Interfaces.swift +++ b/Sources/NIOCore/Interfaces.swift @@ -81,7 +81,7 @@ public final class NIONetworkInterface { /// The index of the interface, as provided by `if_nametoindex`. public let interfaceIndex: Int - fileprivate init?(_ caddr: ifaddrs) { + internal init?(_ caddr: ifaddrs) { self.name = String(cString: caddr.ifa_name) guard caddr.ifa_addr != nil else { @@ -122,12 +122,6 @@ public final class NIONetworkInterface { 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") @@ -173,3 +167,320 @@ extension UnsafeMutablePointer where Pointee == sockaddr { } } } + +/// A representation of a single network device on a system. +public struct NIONetworkDevice { + private var backing: Backing + + /// The name of the network device. + public var name: String { + get { + return self.backing.name + } + set { + self.uniquifyIfNeeded() + self.backing.name = newValue + } + } + + /// The address associated with the given network device. + public var address: SocketAddress? { + get { + return self.backing.address + } + set { + self.uniquifyIfNeeded() + self.backing.address = newValue + } + } + + /// The netmask associated with this address, if any. + public var netmask: SocketAddress? { + get { + return self.backing.netmask + } + set { + self.uniquifyIfNeeded() + self.backing.netmask = newValue + } + } + + /// The broadcast address associated with this socket interface, if it has one. Some + /// interfaces do not, especially those that have a `pointToPointDestinationAddress`. + public var broadcastAddress: SocketAddress? { + get { + return self.backing.broadcastAddress + } + set { + self.uniquifyIfNeeded() + self.backing.broadcastAddress = newValue + } + } + + /// 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 var pointToPointDestinationAddress: SocketAddress? { + get { + return self.backing.pointToPointDestinationAddress + } + set { + self.uniquifyIfNeeded() + self.backing.pointToPointDestinationAddress = newValue + } + } + + /// If the Interface supports Multicast + public var multicastSupported: Bool { + get { + return self.backing.multicastSupported + } + set { + self.uniquifyIfNeeded() + self.backing.multicastSupported = newValue + } + } + + /// The index of the interface, as provided by `if_nametoindex`. + public var interfaceIndex: Int { + get { + return self.backing.interfaceIndex + } + set { + self.uniquifyIfNeeded() + self.backing.interfaceIndex = newValue + } + } + + /// 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) { + guard let backing = Backing(pAdapter, pAddress) else { + return nil + } + self.backing = backing + } +#else + internal init?(_ caddr: ifaddrs) { + guard let backing = Backing(caddr) else { + return nil + } + + self.backing = backing + } +#endif + +#if !os(Windows) + /// Convert a `NIONetworkInterface` to a `NIONetworkDevice`. As `NIONetworkDevice`s are a superset of `NIONetworkInterface`s, + /// it is always possible to perform this conversion. + @available(*, deprecated, message: "This is a compatibility helper, and will be removed in a future release") + public init(_ interface: NIONetworkInterface) { + self.backing = Backing( + name: interface.name, + address: interface.address, + netmask: interface.netmask, + broadcastAddress: interface.broadcastAddress, + pointToPointDestinationAddress: interface.pointToPointDestinationAddress, + multicastSupported: interface.multicastSupported, + interfaceIndex: interface.interfaceIndex + ) + } +#endif + + public init(name: String, + address: SocketAddress?, + netmask: SocketAddress?, + broadcastAddress: SocketAddress?, + pointToPointDestinationAddress: SocketAddress, + multicastSupported: Bool, + interfaceIndex: Int) { + self.backing = Backing( + name: name, + address: address, + netmask: netmask, + broadcastAddress: broadcastAddress, + pointToPointDestinationAddress: pointToPointDestinationAddress, + multicastSupported: multicastSupported, + interfaceIndex: interfaceIndex + ) + } + + private mutating func uniquifyIfNeeded() { + if !isKnownUniquelyReferenced(&self.backing) { + self.backing = Backing(copying: self.backing) + } + } +} + +extension NIONetworkDevice { + fileprivate final class Backing { + /// The name of the network interface. + var name: String + + /// The address associated with the given network interface. + var address: SocketAddress? + + /// The netmask associated with this address, if any. + var netmask: SocketAddress? + + /// The broadcast address associated with this socket interface, if it has one. Some + /// interfaces do not, especially those that have a `pointToPointDestinationAddress`. + var 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. + var pointToPointDestinationAddress: SocketAddress? + + /// If the Interface supports Multicast + var multicastSupported: Bool + + /// The index of the interface, as provided by `if_nametoindex`. + var 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) + self.address = pAddress.pointee.Address.lpSockaddr.convert() + + // 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) + break + case ADDRESS_FAMILY(AF_INET6): + self.netmask = v6mask(pAddress.pointee.OnLinkPrefixLength) + self.interfaceIndex = Int(pAdapter.pointee.Ipv6IfIndex) + break + 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) + self.address = caddr.ifa_addr.flatMap { $0.convert() } + self.netmask = caddr.ifa_netmask.flatMap { $0.convert() } + + 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 + } + + self.multicastSupported = (caddr.ifa_flags & UInt32(IFF_MULTICAST)) != 0 + do { + self.interfaceIndex = Int(try SystemCalls.if_nametoindex(caddr.ifa_name)) + } catch { + return nil + } + } +#endif + + init(copying original: Backing) { + self.name = original.name + self.address = original.address + self.netmask = original.netmask + self.broadcastAddress = original.broadcastAddress + self.pointToPointDestinationAddress = original.pointToPointDestinationAddress + self.multicastSupported = original.multicastSupported + self.interfaceIndex = original.interfaceIndex + } + + init(name: String, + address: SocketAddress?, + netmask: SocketAddress?, + broadcastAddress: SocketAddress?, + pointToPointDestinationAddress: SocketAddress?, + multicastSupported: Bool, + interfaceIndex: Int) { + self.name = name + self.address = address + self.netmask = netmask + self.broadcastAddress = broadcastAddress + self.pointToPointDestinationAddress = pointToPointDestinationAddress + self.multicastSupported = multicastSupported + self.interfaceIndex = interfaceIndex + } + } +} + +extension NIONetworkDevice: CustomDebugStringConvertible { + public var debugDescription: String { + let baseString = "Device \(self.name): address \(String(describing: self.address))" + let maskString = self.netmask != nil ? " netmask \(self.netmask!)" : "" + return baseString + maskString + } +} + +// Sadly, as this is class-backed we cannot synthesise the implementation. +extension NIONetworkDevice: Equatable { + public static func ==(lhs: NIONetworkDevice, rhs: NIONetworkDevice) -> 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 + } +} + +extension NIONetworkDevice: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.name) + hasher.combine(self.address) + hasher.combine(self.netmask) + hasher.combine(self.broadcastAddress) + hasher.combine(self.pointToPointDestinationAddress) + hasher.combine(self.interfaceIndex) + } +} + diff --git a/Sources/NIOCore/Linux.swift b/Sources/NIOCore/Linux.swift new file mode 100644 index 00000000..f463deec --- /dev/null +++ b/Sources/NIOCore/Linux.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// This is a companion to System.swift that provides only Linux specials: either things that exist +// only on Linux, or things that have Linux-specific extensions. + +#if os(Linux) || os(Android) +import CNIOLinux +enum Linux { + static let cfsQuotaPath = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us" + static let cfsPeriodPath = "/sys/fs/cgroup/cpu/cpu.cfs_period_us" + static let cpuSetPath = "/sys/fs/cgroup/cpuset/cpuset.cpus" + + private static func firstLineOfFile(path: String) throws -> Substring { + let fh = try NIOFileHandle(path: path) + defer { try! fh.close() } + // linux doesn't properly report /sys/fs/cgroup/* files lengths so we use a reasonable limit + var buf = ByteBufferAllocator().buffer(capacity: 1024) + try buf.writeWithUnsafeMutableBytes(minimumWritableBytes: buf.capacity) { ptr in + let res = try fh.withUnsafeFileDescriptor { fd -> CoreIOResult in + return try SystemCalls.read(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count) + } + switch res { + case .processed(let n): + return n + case .wouldBlock: + preconditionFailure("read returned EWOULDBLOCK despite a blocking fd") + } + } + return String(buffer: buf).prefix(while: { $0 != "\n" }) + } + + private static func countCoreIds(cores: Substring) -> Int { + let ids = cores.split(separator: "-", maxSplits: 1) + guard + let first = ids.first.flatMap({ Int($0, radix: 10) }), + let last = ids.last.flatMap({ Int($0, radix: 10) }), + last >= first + else { preconditionFailure("cpuset format is incorrect") } + return 1 + last - first + } + + static func coreCount(cpuset cpusetPath: String) -> Int? { + guard + let cpuset = try? firstLineOfFile(path: cpusetPath).split(separator: ","), + !cpuset.isEmpty + else { return nil } + return cpuset.map(countCoreIds).reduce(0, +) + } + + static func coreCount(quota quotaPath: String, period periodPath: String) -> Int? { + guard + let quota = try? Int(firstLineOfFile(path: quotaPath)), + quota > 0 + else { return nil } + guard + let period = try? Int(firstLineOfFile(path: periodPath)), + period > 0 + else { return nil } + return (quota - 1 + period) / period // always round up if fractional CPU quota requested + } +} +#endif diff --git a/Sources/NIO/MulticastChannel.swift b/Sources/NIOCore/MulticastChannel.swift similarity index 95% rename from Sources/NIO/MulticastChannel.swift rename to Sources/NIOCore/MulticastChannel.swift index 66476747..9669617d 100644 --- a/Sources/NIO/MulticastChannel.swift +++ b/Sources/NIOCore/MulticastChannel.swift @@ -162,3 +162,18 @@ extension MulticastChannel { promise?.fail(NIOMulticastNotImplementedError()) } } + +/// 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/NIOCore/SystemCallHelpers.swift b/Sources/NIOCore/SystemCallHelpers.swift index 35a6425c..9451e30c 100644 --- a/Sources/NIOCore/SystemCallHelpers.swift +++ b/Sources/NIOCore/SystemCallHelpers.swift @@ -33,8 +33,13 @@ 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 sysRead: @convention(c) (CInt, UnsafeMutableRawPointer?, size_t) -> size_t = read private let sysIfNameToIndex: @convention(c) (UnsafePointer?) -> CUnsignedInt = if_nametoindex +#if !os(Windows) +private let sysGetifaddrs: @convention(c) (UnsafeMutablePointer?>?) -> CInt = getifaddrs +#endif + private func isUnacceptableErrno(_ code: Int32) -> Bool { switch code { case EFAULT, EBADF: @@ -121,10 +126,26 @@ enum SystemCalls { }.result } + @inline(never) + internal static func read(descriptor: CInt, pointer: UnsafeMutableRawPointer, size: size_t) throws -> CoreIOResult { + return try syscall(blocking: true) { + sysRead(descriptor, pointer, size) + } + } + @inline(never) internal static func if_nametoindex(_ name: UnsafePointer?) throws -> CUnsignedInt { return try syscall(blocking: false) { sysIfNameToIndex(name) }.result } + + #if !os(Windows) + @inline(never) + internal static func getifaddrs(_ addrs: UnsafeMutablePointer?>) throws { + _ = try syscall(blocking: false) { + sysGetifaddrs(addrs) + } + } + #endif } diff --git a/Sources/NIOCore/Utilities.swift b/Sources/NIOCore/Utilities.swift index c3ca1d42..ccd1c0ba 100644 --- a/Sources/NIOCore/Utilities.swift +++ b/Sources/NIOCore/Utilities.swift @@ -11,6 +11,28 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +#if os(Linux) || os(FreeBSD) || os(Android) +import CNIOLinux +import Glibc +#elseif os(Windows) +import let WinSDK.RelationProcessorCore + +import let WinSDK.AF_UNSPEC +import let WinSDK.ERROR_SUCCESS + +import func WinSDK.GetAdaptersAddresses +import func WinSDK.GetLastError +import func WinSDK.GetLogicalProcessorInformation + +import struct WinSDK.IP_ADAPTER_ADDRESSES +import struct WinSDK.IP_ADAPTER_UNICAST_ADDRESS +import struct WinSDK.SYSTEM_LOGICAL_PROCESSOR_INFORMATION +import struct WinSDK.ULONG + +import typealias WinSDK.DWORD +#elseif os(macOS) || os(iOS) || os(tvOS) || os(watchOS) +import Darwin +#endif /// A utility function that runs the body code only in debug builds, without /// emitting compiler warnings. @@ -30,3 +52,151 @@ final class Box { init(_ value: T) { self.value = value } } +public enum System { + /// A utility function that returns an estimate of the number of *logical* cores + /// on the system. + /// + /// This value can be used to help provide an estimate of how many threads to use with + /// the `MultiThreadedEventLoopGroup`. The exact ratio between this number and the number + /// of threads to use is a matter for the programmer, and can be determined based on the + /// specific execution behaviour of the program. + /// + /// - returns: The logical core count on the system. + public static var coreCount: Int { +#if os(Windows) + var dwLength: DWORD = 0 + _ = GetLogicalProcessorInformation(nil, &dwLength) + + let alignment: Int = + MemoryLayout.alignment + let pBuffer: UnsafeMutableRawPointer = + UnsafeMutableRawPointer.allocate(byteCount: Int(dwLength), + alignment: alignment) + defer { + pBuffer.deallocate() + } + + let dwSLPICount: Int = + Int(dwLength) / MemoryLayout.stride + let pSLPI: UnsafeMutablePointer = + pBuffer.bindMemory(to: SYSTEM_LOGICAL_PROCESSOR_INFORMATION.self, + capacity: dwSLPICount) + + let bResult: Bool = GetLogicalProcessorInformation(pSLPI, &dwLength) + precondition(bResult, "GetLogicalProcessorInformation: \(GetLastError())") + + return UnsafeBufferPointer(start: pSLPI, + count: dwSLPICount) + .filter { $0.Relationship == RelationProcessorCore } + .map { $0.ProcessorMask.nonzeroBitCount } + .reduce(0, +) +#elseif os(Linux) || os(Android) + if let quota = Linux.coreCount(quota: Linux.cfsQuotaPath, period: Linux.cfsPeriodPath) { + return quota + } else if let cpusetCount = Linux.coreCount(cpuset: Linux.cpuSetPath) { + return cpusetCount + } else { + return sysconf(CInt(_SC_NPROCESSORS_ONLN)) + } +#else + return sysconf(CInt(_SC_NPROCESSORS_ONLN)) +#endif + } + +#if !os(Windows) + /// A utility function that enumerates the available network interfaces on this machine. + /// + /// This function returns values that are true for a brief snapshot in time. These results can + /// change, and the returned values will not change to reflect them. This function must be called + /// again to get new results. + /// + /// - returns: An array of network interfaces available on this machine. + /// - throws: If an error is encountered while enumerating interfaces. + @available(*, deprecated, renamed: "enumerateDevices") + public static func enumerateInterfaces() throws -> [NIONetworkInterface] { + var interfaces: [NIONetworkInterface] = [] + interfaces.reserveCapacity(12) // Arbitrary choice. + + var interface: UnsafeMutablePointer? = nil + try SystemCalls.getifaddrs(&interface) + let originalInterface = interface + defer { + freeifaddrs(originalInterface) + } + + while let concreteInterface = interface { + if let nioInterface = NIONetworkInterface(concreteInterface.pointee) { + interfaces.append(nioInterface) + } + interface = concreteInterface.pointee.ifa_next + } + + return interfaces + } +#endif + + /// A utility function that enumerates the available network devices on this machine. + /// + /// This function returns values that are true for a brief snapshot in time. These results can + /// change, and the returned values will not change to reflect them. This function must be called + /// again to get new results. + /// + /// - returns: An array of network devices available on this machine. + /// - throws: If an error is encountered while enumerating interfaces. + public static func enumerateDevices() throws -> [NIONetworkDevice] { + var devices: [NIONetworkDevice] = [] + devices.reserveCapacity(12) // Arbitrary choice. + +#if os(Windows) + var ulSize: ULONG = 0 + _ = GetAdaptersAddresses(ULONG(AF_UNSPEC), 0, nil, nil, &ulSize) + + let stride: Int = MemoryLayout.stride + let pBuffer: UnsafeMutableBufferPointer = + UnsafeMutableBufferPointer.allocate(capacity: Int(ulSize) / stride) + defer { + pBuffer.deallocate() + } + + let ulResult: ULONG = + GetAdaptersAddresses(ULONG(AF_UNSPEC), 0, nil, pBuffer.baseAddress, + &ulSize) + guard ulResult == ERROR_SUCCESS else { + throw IOError(windows: ulResult, reason: "GetAdaptersAddresses") + } + + var pAdapter: UnsafeMutablePointer? = + UnsafeMutablePointer(pBuffer.baseAddress) + while pAdapter != nil { + let pUnicastAddresses: UnsafeMutablePointer? = + pAdapter!.pointee.FirstUnicastAddress + var pUnicastAddress: UnsafeMutablePointer? = + pUnicastAddresses + while pUnicastAddress != nil { + if let device = NIONetworkDevice(pAdapter!, pUnicastAddress!) { + devices.append(device) + } + pUnicastAddress = pUnicastAddress!.pointee.Next + } + pAdapter = pAdapter!.pointee.Next + } +#else + var interface: UnsafeMutablePointer? = nil + try SystemCalls.getifaddrs(&interface) + let originalInterface = interface + defer { + freeifaddrs(originalInterface) + } + + while let concreteInterface = interface { + if let nioInterface = NIONetworkDevice(concreteInterface.pointee) { + devices.append(nioInterface) + } + interface = concreteInterface.pointee.ifa_next + } + +#endif + return devices + } +} + diff --git a/Tests/NIOTests/LinuxTest.swift b/Tests/NIOTests/LinuxTest.swift index 3538014e..292b8286 100644 --- a/Tests/NIOTests/LinuxTest.swift +++ b/Tests/NIOTests/LinuxTest.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2020 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 LinuxTest: XCTestCase { func testCoreCountQuota() {