1016 lines
45 KiB
Swift
1016 lines
45 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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 XCTest
|
|
import NIOCore
|
|
@testable import NIOPosix
|
|
import NIOTestUtils
|
|
import Atomics
|
|
|
|
private extension Array {
|
|
/// A helper function that asserts that a predicate is true for all elements.
|
|
func assertAll(_ predicate: (Element) -> Bool) {
|
|
self.enumerated().forEach { (index: Int, element: Element) in
|
|
if !predicate(element) {
|
|
XCTFail("Entry \(index) failed predicate, contents: \(element)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class SocketChannelTest : XCTestCase {
|
|
/// Validate that channel options are applied asynchronously.
|
|
public func testAsyncSetOption() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
// Create two channels with different event loops.
|
|
let channelA = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
let channelB: Channel = try {
|
|
while true {
|
|
let channel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
if channel.eventLoop !== channelA.eventLoop {
|
|
return channel
|
|
}
|
|
}
|
|
}()
|
|
XCTAssert(channelA.eventLoop !== channelB.eventLoop)
|
|
|
|
// Ensure we can dispatch two concurrent set option's on each others
|
|
// event loops.
|
|
let condition = ManagedAtomic(0)
|
|
let futureA = channelA.eventLoop.submit {
|
|
condition.wrappingIncrement(ordering: .relaxed)
|
|
while condition.load(ordering: .relaxed) < 2 { }
|
|
_ = channelB.setOption(ChannelOptions.backlog, value: 1)
|
|
}
|
|
let futureB = channelB.eventLoop.submit {
|
|
condition.wrappingIncrement(ordering: .relaxed)
|
|
while condition.load(ordering: .relaxed) < 2 { }
|
|
_ = channelA.setOption(ChannelOptions.backlog, value: 1)
|
|
}
|
|
try futureA.wait()
|
|
try futureB.wait()
|
|
}
|
|
|
|
public func testDelayedConnectSetsUpRemotePeerAddress() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
|
.serverChannelOption(ChannelOptions.backlog, value: 256)
|
|
.bind(host: "127.0.0.1", port: 0).wait())
|
|
|
|
// The goal of this test is to try to trigger at least one channel to have connection setup that is not
|
|
// instantaneous. Due to the design of NIO this is not really observable to us, and due to the complex
|
|
// overlapping interactions between SYN queues and loopback interfaces in modern kernels it's not
|
|
// trivial to trigger this behaviour. The easiest thing we can do here is to try to slow the kernel down
|
|
// enough that a connection eventually is delayed. To do this we're going to submit 50 connections more
|
|
// or less at once.
|
|
var clientConnectionFutures: [EventLoopFuture<Channel>] = []
|
|
clientConnectionFutures.reserveCapacity(50)
|
|
let clientBootstrap = ClientBootstrap(group: group)
|
|
|
|
for _ in 0..<50 {
|
|
let conn = clientBootstrap.connect(to: serverChannel.localAddress!)
|
|
clientConnectionFutures.append(conn)
|
|
}
|
|
|
|
let remoteAddresses = try clientConnectionFutures.map { try $0.wait() }.map { $0.remoteAddress }
|
|
|
|
// Now we want to check that they're all the same. The bug we're catching here is one where delayed connection
|
|
// setup causes us to get nil as the remote address, even though we connected (and we did, as these are all
|
|
// up right now).
|
|
remoteAddresses.assertAll { $0 != nil }
|
|
}
|
|
|
|
public func testAcceptFailsWithECONNABORTED() throws {
|
|
try assertAcceptFails(error: ECONNABORTED, active: true)
|
|
}
|
|
|
|
public func testAcceptFailsWithEMFILE() throws {
|
|
try assertAcceptFails(error: EMFILE, active: true)
|
|
}
|
|
|
|
public func testAcceptFailsWithENFILE() throws {
|
|
try assertAcceptFails(error: ENFILE, active: true)
|
|
}
|
|
|
|
public func testAcceptFailsWithENOBUFS() throws {
|
|
try assertAcceptFails(error: ENOBUFS, active: true)
|
|
}
|
|
|
|
public func testAcceptFailsWithENOMEM() throws {
|
|
try assertAcceptFails(error: ENOMEM, active: true)
|
|
}
|
|
|
|
public func testAcceptFailsWithEFAULT() throws {
|
|
try assertAcceptFails(error: EFAULT, active: false)
|
|
}
|
|
|
|
private func assertAcceptFails(error: Int32, active: Bool) throws {
|
|
final class AcceptHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Channel
|
|
typealias InboundOut = Channel
|
|
|
|
private let promise: EventLoopPromise<IOError>
|
|
|
|
init(_ promise: EventLoopPromise<IOError>) {
|
|
self.promise = promise
|
|
}
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
XCTFail("Should not accept a Channel but got \(self.unwrapInboundIn(data))")
|
|
}
|
|
|
|
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
|
if let ioError = error as? IOError {
|
|
self.promise.succeed(ioError)
|
|
}
|
|
}
|
|
}
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
let socket = try NonAcceptingServerSocket(errors: [error])
|
|
let serverChannel = try assertNoThrowWithValue(ServerSocketChannel(serverSocket: socket,
|
|
eventLoop: group.next() as! SelectableEventLoop,
|
|
group: group))
|
|
let promise = serverChannel.eventLoop.makePromise(of: IOError.self)
|
|
|
|
XCTAssertNoThrow(try serverChannel.eventLoop.flatSubmit {
|
|
serverChannel.pipeline.addHandler(AcceptHandler(promise)).flatMap {
|
|
serverChannel.register()
|
|
}.flatMap {
|
|
serverChannel.bind(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 0))
|
|
}
|
|
}.wait() as Void)
|
|
|
|
XCTAssertEqual(active, try serverChannel.eventLoop.submit {
|
|
serverChannel.readable()
|
|
return serverChannel.isActive
|
|
}.wait())
|
|
|
|
if active {
|
|
XCTAssertNoThrow(try serverChannel.close().wait())
|
|
}
|
|
|
|
let ioError = try promise.futureResult.wait()
|
|
XCTAssertEqual(error, ioError.errnoCode)
|
|
}
|
|
|
|
public func testSetGetOptionClosedServerSocketChannel() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
// Create two channels with different event loops.
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
let clientChannel = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.connect(to: serverChannel.localAddress!)
|
|
.wait())
|
|
|
|
XCTAssertNoThrow(try assertSetGetOptionOnOpenAndClosed(channel: clientChannel,
|
|
option: ChannelOptions.allowRemoteHalfClosure,
|
|
value: true))
|
|
XCTAssertNoThrow(try assertSetGetOptionOnOpenAndClosed(channel: serverChannel,
|
|
option: ChannelOptions.backlog,
|
|
value: 100))
|
|
}
|
|
|
|
public func testConnect() throws {
|
|
final class ActiveVerificationHandler: ChannelInboundHandler {
|
|
typealias InboundIn = ByteBuffer
|
|
typealias InboundOut = ByteBuffer
|
|
|
|
private let promise: EventLoopPromise<Void>
|
|
|
|
init(_ promise: EventLoopPromise<Void>) {
|
|
self.promise = promise
|
|
}
|
|
|
|
func channelActive(context: ChannelHandlerContext) {
|
|
promise.succeed(())
|
|
}
|
|
}
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
|
|
class ConnectSocket: Socket {
|
|
private let promise: EventLoopPromise<Void>
|
|
init(promise: EventLoopPromise<Void>) throws {
|
|
self.promise = promise
|
|
try super.init(protocolFamily: .inet, type: .stream)
|
|
}
|
|
|
|
override func connect(to address: SocketAddress) throws -> Bool {
|
|
self.promise.succeed(())
|
|
return true
|
|
}
|
|
}
|
|
|
|
let eventLoop = group.next()
|
|
let connectPromise = eventLoop.makePromise(of: Void.self)
|
|
|
|
let channel = try assertNoThrowWithValue(SocketChannel(socket: ConnectSocket(promise: connectPromise),
|
|
parent: nil,
|
|
eventLoop: eventLoop as! SelectableEventLoop))
|
|
let promise = channel.eventLoop.makePromise(of: Void.self)
|
|
|
|
XCTAssertNoThrow(try channel.eventLoop.flatSubmit {
|
|
// We need to hop to the EventLoop here to make sure that we don't get an ECONNRESET before we manage
|
|
// to close.
|
|
channel.pipeline.addHandler(ActiveVerificationHandler(promise)).flatMap {
|
|
channel.register()
|
|
}.flatMap {
|
|
channel.connect(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 9999))
|
|
}.flatMap {
|
|
channel.close()
|
|
}
|
|
}.wait())
|
|
|
|
XCTAssertNoThrow(try channel.closeFuture.wait())
|
|
XCTAssertNoThrow(try promise.futureResult.wait())
|
|
XCTAssertNoThrow(try connectPromise.futureResult.wait())
|
|
}
|
|
|
|
public func testWriteServerSocketChannel() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
XCTAssertThrowsError(try serverChannel.writeAndFlush("test").wait()) { error in
|
|
XCTAssertEqual(.operationUnsupported, error as? ChannelError)
|
|
}
|
|
try serverChannel.close().wait()
|
|
}
|
|
|
|
|
|
public func testWriteAndFlushServerSocketChannel() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
XCTAssertThrowsError(try serverChannel.writeAndFlush("test").wait()) { error in
|
|
XCTAssertEqual(.operationUnsupported, error as? ChannelError)
|
|
}
|
|
try serverChannel.close().wait()
|
|
}
|
|
|
|
|
|
public func testConnectServerSocketChannel() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
XCTAssertThrowsError(try serverChannel.writeAndFlush("test").wait()) { error in
|
|
XCTAssertEqual(.operationUnsupported, error as? ChannelError)
|
|
}
|
|
}
|
|
|
|
public func testCloseDuringWriteFailure() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
let clientChannel = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.connect(to: serverChannel.localAddress!)
|
|
.wait())
|
|
|
|
// Put a write in the channel but don't flush it. We're then going to
|
|
// close the channel. This should trigger an error callback that will
|
|
// re-close the channel, which should fail with `alreadyClosed`.
|
|
var buffer = clientChannel.allocator.buffer(capacity: 12)
|
|
buffer.writeStaticString("hello")
|
|
let writeFut = clientChannel.write(buffer).map {
|
|
XCTFail("Must not succeed")
|
|
}.flatMapError { error in
|
|
XCTAssertEqual(error as? ChannelError, ChannelError.ioOnClosedChannel)
|
|
return clientChannel.close()
|
|
}
|
|
XCTAssertNoThrow(try clientChannel.close().wait())
|
|
|
|
XCTAssertThrowsError(try writeFut.wait()) { error in
|
|
XCTAssertEqual(.alreadyClosed, error as? ChannelError)
|
|
}
|
|
}
|
|
|
|
public func testWithConfiguredStreamSocket() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverSock = try Socket(protocolFamily: .inet, type: .stream)
|
|
try serverSock.bind(to: SocketAddress(ipAddress: "127.0.0.1", port: 0))
|
|
let serverChannelFuture = try serverSock.withUnsafeHandle {
|
|
ServerBootstrap(group: group).withBoundSocket(dup($0))
|
|
}
|
|
try serverSock.close()
|
|
let serverChannel = try serverChannelFuture.wait()
|
|
|
|
let clientSock = try Socket(protocolFamily: .inet, type: .stream)
|
|
let connected = try clientSock.connect(to: serverChannel.localAddress!)
|
|
XCTAssertEqual(connected, true)
|
|
let clientChannelFuture = try clientSock.withUnsafeHandle {
|
|
ClientBootstrap(group: group).withConnectedSocket(dup($0))
|
|
}
|
|
try clientSock.close()
|
|
let clientChannel = try clientChannelFuture.wait()
|
|
|
|
XCTAssertEqual(true, clientChannel.isActive)
|
|
|
|
try serverChannel.close().wait()
|
|
try clientChannel.close().wait()
|
|
}
|
|
|
|
public func testWithConfiguredDatagramSocket() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverSock = try Socket(protocolFamily: .inet, type: .datagram)
|
|
try serverSock.bind(to: SocketAddress(ipAddress: "127.0.0.1", port: 0))
|
|
let serverChannelFuture = try serverSock.withUnsafeHandle {
|
|
DatagramBootstrap(group: group).withBoundSocket(dup($0))
|
|
}
|
|
try serverSock.close()
|
|
let serverChannel = try serverChannelFuture.wait()
|
|
|
|
XCTAssertEqual(true, serverChannel.isActive)
|
|
|
|
try serverChannel.close().wait()
|
|
}
|
|
|
|
public func testPendingConnectNotificationOrder() throws {
|
|
|
|
class NotificationOrderHandler: ChannelDuplexHandler {
|
|
typealias InboundIn = Never
|
|
typealias OutboundIn = Never
|
|
|
|
private var connectPromise: EventLoopPromise<Void>?
|
|
|
|
public func channelInactive(context: ChannelHandlerContext) {
|
|
if let connectPromise = self.connectPromise {
|
|
XCTAssertTrue(connectPromise.futureResult.isFulfilled)
|
|
} else {
|
|
XCTFail("connect(...) not called before")
|
|
}
|
|
}
|
|
|
|
public func connect(context: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
|
|
XCTAssertNil(self.connectPromise)
|
|
self.connectPromise = promise
|
|
context.connect(to: address, promise: promise)
|
|
}
|
|
|
|
func handlerAdded(context: ChannelHandlerContext) {
|
|
XCTAssertNil(self.connectPromise)
|
|
}
|
|
|
|
func handlerRemoved(context: ChannelHandlerContext) {
|
|
if let connectPromise = self.connectPromise {
|
|
XCTAssertTrue(connectPromise.futureResult.isFulfilled)
|
|
} else {
|
|
XCTFail("connect(...) not called before")
|
|
}
|
|
}
|
|
}
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
defer { XCTAssertNoThrow(try serverChannel.close().wait()) }
|
|
|
|
let eventLoop = group.next()
|
|
let promise = eventLoop.makePromise(of: Void.self)
|
|
|
|
class ConnectPendingSocket: Socket {
|
|
let promise: EventLoopPromise<Void>
|
|
init(promise: EventLoopPromise<Void>) throws {
|
|
self.promise = promise
|
|
try super.init(protocolFamily: .inet, type: .stream)
|
|
}
|
|
|
|
override func connect(to address: SocketAddress) throws -> Bool {
|
|
// We want to return false here to have a pending connect.
|
|
_ = try super.connect(to: address)
|
|
self.promise.succeed(())
|
|
return false
|
|
}
|
|
}
|
|
|
|
let channel = try SocketChannel(socket: ConnectPendingSocket(promise: promise), parent: nil, eventLoop: eventLoop as! SelectableEventLoop)
|
|
let connectPromise = channel.eventLoop.makePromise(of: Void.self)
|
|
let closePromise = channel.eventLoop.makePromise(of: Void.self)
|
|
|
|
closePromise.futureResult.whenComplete { (_: Result<Void, Error>) in
|
|
XCTAssertTrue(connectPromise.futureResult.isFulfilled)
|
|
}
|
|
connectPromise.futureResult.whenComplete { (_: Result<Void, Error>) in
|
|
XCTAssertFalse(closePromise.futureResult.isFulfilled)
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NotificationOrderHandler()).wait())
|
|
|
|
// We need to call submit {...} here to ensure then {...} is called while on the EventLoop already to not have
|
|
// a ECONNRESET sneak in.
|
|
XCTAssertNoThrow(try channel.eventLoop.flatSubmit {
|
|
channel.register().map { () -> Void in
|
|
channel.connect(to: serverChannel.localAddress!, promise: connectPromise)
|
|
}.map { () -> Void in
|
|
XCTAssertFalse(connectPromise.futureResult.isFulfilled)
|
|
// The close needs to happen in the then { ... } block to ensure we close the channel
|
|
// before we have the chance to register it for .write.
|
|
channel.close(promise: closePromise)
|
|
}
|
|
}.wait() as Void)
|
|
|
|
XCTAssertThrowsError(try connectPromise.futureResult.wait()) { error in
|
|
XCTAssertEqual(.ioOnClosedChannel, error as? ChannelError)
|
|
}
|
|
XCTAssertNoThrow(try closePromise.futureResult.wait())
|
|
XCTAssertNoThrow(try channel.closeFuture.wait())
|
|
XCTAssertNoThrow(try promise.futureResult.wait())
|
|
}
|
|
|
|
public func testLocalAndRemoteAddressNotNilInChannelInactiveAndHandlerRemoved() throws {
|
|
|
|
class AddressVerificationHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Never
|
|
typealias OutboundIn = Never
|
|
|
|
enum HandlerState {
|
|
case created
|
|
case inactive
|
|
case removed
|
|
}
|
|
|
|
let promise: EventLoopPromise<Void>
|
|
var state = HandlerState.created
|
|
|
|
init(promise: EventLoopPromise<Void>) {
|
|
self.promise = promise
|
|
}
|
|
|
|
func channelInactive(context: ChannelHandlerContext) {
|
|
XCTAssertNotNil(context.localAddress)
|
|
XCTAssertNotNil(context.remoteAddress)
|
|
XCTAssertEqual(.created, state)
|
|
state = .inactive
|
|
}
|
|
|
|
func handlerRemoved(context: ChannelHandlerContext) {
|
|
XCTAssertNotNil(context.localAddress)
|
|
XCTAssertNotNil(context.remoteAddress)
|
|
XCTAssertEqual(.inactive, state)
|
|
state = .removed
|
|
|
|
context.channel.closeFuture.whenComplete { (_: Result<Void, Error>) in
|
|
XCTAssertNil(context.localAddress)
|
|
XCTAssertNil(context.remoteAddress)
|
|
|
|
self.promise.succeed(())
|
|
}
|
|
}
|
|
}
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let handler = AddressVerificationHandler(promise: group.next().makePromise())
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.childChannelInitializer { $0.pipeline.addHandler(handler) }
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
defer { XCTAssertNoThrow(try serverChannel.close().wait()) }
|
|
|
|
let clientChannel = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.connect(to: serverChannel.localAddress!)
|
|
.wait())
|
|
|
|
XCTAssertNoThrow(try clientChannel.close().wait())
|
|
XCTAssertNoThrow(try handler.promise.futureResult.wait())
|
|
}
|
|
|
|
func testSocketFlagNONBLOCKWorks() throws {
|
|
var socket = try assertNoThrowWithValue(try ServerSocket(protocolFamily: .inet, setNonBlocking: true))
|
|
XCTAssertNoThrow(try socket.withUnsafeHandle { fd in
|
|
let flags = try assertNoThrowWithValue(Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0))
|
|
XCTAssertEqual(O_NONBLOCK, flags & O_NONBLOCK)
|
|
})
|
|
XCTAssertNoThrow(try socket.close())
|
|
|
|
socket = try assertNoThrowWithValue(ServerSocket(protocolFamily: .inet, setNonBlocking: false))
|
|
XCTAssertNoThrow(try socket.withUnsafeHandle { fd in
|
|
var flags = try assertNoThrowWithValue(Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0))
|
|
XCTAssertEqual(0, flags & O_NONBLOCK)
|
|
let ret = try assertNoThrowWithValue(Posix.fcntl(descriptor: fd, command: F_SETFL, value: flags | O_NONBLOCK))
|
|
XCTAssertEqual(0, ret)
|
|
flags = try assertNoThrowWithValue(Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0))
|
|
XCTAssertEqual(O_NONBLOCK, flags & O_NONBLOCK)
|
|
})
|
|
XCTAssertNoThrow(try socket.close())
|
|
}
|
|
|
|
func testInstantTCPConnectionResetThrowsError() throws {
|
|
#if !os(Linux) && !os(Android)
|
|
// This test checks that we correctly fail with an error rather than
|
|
// asserting or silently ignoring if a client aborts the connection
|
|
// early with a RST during or immediately after accept().
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
// Handler that checks for the expected error.
|
|
final class ErrorHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Channel
|
|
typealias InboundOut = Channel
|
|
|
|
private let promise: EventLoopPromise<IOError>
|
|
|
|
init(_ promise: EventLoopPromise<IOError>) {
|
|
self.promise = promise
|
|
}
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
XCTFail("Should not accept a Channel but got \(self.unwrapInboundIn(data))")
|
|
self.promise.fail(ChannelError.inappropriateOperationForState) // any old error will do
|
|
}
|
|
|
|
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
|
if let ioError = error as? IOError, ioError.errnoCode == EINVAL {
|
|
self.promise.succeed(ioError)
|
|
} else {
|
|
self.promise.fail(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build server channel; after this point the server called listen()
|
|
let serverPromise = group.next().makePromise(of: IOError.self)
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
|
.serverChannelOption(ChannelOptions.backlog, value: 256)
|
|
.serverChannelOption(ChannelOptions.autoRead, value: false)
|
|
.serverChannelInitializer { channel in channel.pipeline.addHandler(ErrorHandler(serverPromise)) }
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait())
|
|
|
|
// Make a client socket to mess with the server. Setting SO_LINGER forces RST instead of FIN.
|
|
let clientSocket = try assertNoThrowWithValue(Socket(protocolFamily: .inet, type: .stream))
|
|
XCTAssertNoThrow(try clientSocket.setOption(level: .socket, name: .so_linger, value: linger(l_onoff: 1, l_linger: 0)))
|
|
XCTAssertNoThrow(try clientSocket.connect(to: serverChannel.localAddress!))
|
|
XCTAssertNoThrow(try clientSocket.close())
|
|
|
|
// Trigger accept() in the server
|
|
serverChannel.read()
|
|
|
|
// Wait for the server to have something
|
|
XCTAssertThrowsError(try serverPromise.futureResult.wait()) { error in
|
|
XCTAssert(error is NIOFcntlFailedError)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func testUnprocessedOutboundUserEventFailsOnServerSocketChannel() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
let channel = try ServerSocketChannel(eventLoop: group.next() as! SelectableEventLoop,
|
|
group: group, protocolFamily: .inet)
|
|
XCTAssertThrowsError(try channel.triggerUserOutboundEvent("event").wait()) { (error: Error) in
|
|
if let error = error as? ChannelError {
|
|
XCTAssertEqual(ChannelError.operationUnsupported, error)
|
|
} else {
|
|
XCTFail("unexpected error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testUnprocessedOutboundUserEventFailsOnSocketChannel() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
let channel = try SocketChannel(eventLoop: group.next() as! SelectableEventLoop,
|
|
protocolFamily: .inet)
|
|
XCTAssertThrowsError(try channel.triggerUserOutboundEvent("event").wait()) { (error: Error) in
|
|
if let error = error as? ChannelError {
|
|
XCTAssertEqual(ChannelError.operationUnsupported, error)
|
|
} else {
|
|
XCTFail("unexpected error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testSetSockOptDoesNotOverrideExistingFlags() throws {
|
|
let s = try assertNoThrowWithValue(Socket(protocolFamily: .inet,
|
|
type: .stream,
|
|
setNonBlocking: false))
|
|
// check initial flags
|
|
XCTAssertNoThrow(try s.withUnsafeHandle { fd in
|
|
let flags = try Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0)
|
|
XCTAssertEqual(0, flags & O_NONBLOCK)
|
|
})
|
|
|
|
// set other random flag
|
|
XCTAssertNoThrow(try s.withUnsafeHandle { fd in
|
|
let oldFlags = try Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0)
|
|
let ret = try Posix.fcntl(descriptor: fd, command: F_SETFL, value: oldFlags | O_ASYNC)
|
|
XCTAssertEqual(0, ret)
|
|
let newFlags = try Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0)
|
|
XCTAssertEqual(O_ASYNC, newFlags & O_ASYNC)
|
|
})
|
|
|
|
// enable non-blocking
|
|
XCTAssertNoThrow(try s.setNonBlocking())
|
|
|
|
// check both are enabled
|
|
XCTAssertNoThrow(try s.withUnsafeHandle { fd in
|
|
let flags = try Posix.fcntl(descriptor: fd, command: F_GETFL, value: 0)
|
|
XCTAssertEqual(O_ASYNC, flags & O_ASYNC)
|
|
XCTAssertEqual(O_NONBLOCK, flags & O_NONBLOCK)
|
|
})
|
|
|
|
XCTAssertNoThrow(try s.close())
|
|
}
|
|
|
|
func testServerChannelDoesNotBreakIfAcceptingFailsWithEINVAL() throws {
|
|
// regression test for:
|
|
// - https://github.com/apple/swift-nio/issues/1030
|
|
// - https://github.com/apple/swift-nio/issues/1598
|
|
class HandsOutMoodySocketsServerSocket: ServerSocket {
|
|
let shouldAcceptsFail = ManagedAtomic(true)
|
|
override func accept(setNonBlocking: Bool = false) throws -> Socket? {
|
|
XCTAssertTrue(setNonBlocking)
|
|
if self.shouldAcceptsFail.load(ordering: .relaxed) {
|
|
throw NIOFcntlFailedError()
|
|
} else {
|
|
return try Socket(protocolFamily: .inet,
|
|
type: .stream,
|
|
setNonBlocking: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
class CloseAcceptedSocketsHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Channel
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
self.unwrapInboundIn(data).close(promise: nil)
|
|
}
|
|
|
|
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
|
XCTAssert(error is NIOFcntlFailedError, "unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
let serverSock = try assertNoThrowWithValue(HandsOutMoodySocketsServerSocket(protocolFamily: .inet,
|
|
setNonBlocking: true))
|
|
let serverChan = try assertNoThrowWithValue(ServerSocketChannel(serverSocket: serverSock,
|
|
eventLoop: group.next() as! SelectableEventLoop,
|
|
group: group))
|
|
XCTAssertNoThrow(try serverChan.setOption(ChannelOptions.maxMessagesPerRead, value: 1).wait())
|
|
XCTAssertNoThrow(try serverChan.setOption(ChannelOptions.autoRead, value: false).wait())
|
|
XCTAssertNoThrow(try serverChan.register().wait())
|
|
XCTAssertNoThrow(try serverChan.bind(to: .init(ipAddress: "127.0.0.1", port: 0)).wait())
|
|
|
|
let eventCounter = EventCounterHandler()
|
|
XCTAssertNoThrow(try serverChan.pipeline.addHandler(eventCounter).wait())
|
|
XCTAssertNoThrow(try serverChan.pipeline.addHandler(CloseAcceptedSocketsHandler()).wait())
|
|
|
|
XCTAssertEqual([], eventCounter.allTriggeredEvents())
|
|
XCTAssertNoThrow(try serverChan.eventLoop.submit {
|
|
serverChan.readable()
|
|
}.wait())
|
|
XCTAssertEqual(["channelReadComplete", "errorCaught"], eventCounter.allTriggeredEvents())
|
|
XCTAssertEqual(1, eventCounter.channelReadCompleteCalls)
|
|
XCTAssertEqual(1, eventCounter.errorCaughtCalls)
|
|
|
|
serverSock.shouldAcceptsFail.store(false, ordering: .relaxed)
|
|
|
|
XCTAssertNoThrow(try serverChan.eventLoop.submit {
|
|
serverChan.readable()
|
|
}.wait())
|
|
XCTAssertEqual(["errorCaught", "channelRead", "channelReadComplete"],
|
|
eventCounter.allTriggeredEvents())
|
|
XCTAssertEqual(1, eventCounter.errorCaughtCalls)
|
|
XCTAssertEqual(1, eventCounter.channelReadCalls)
|
|
XCTAssertEqual(2, eventCounter.channelReadCompleteCalls)
|
|
}
|
|
|
|
func testWeAreInterestedInReadEOFWhenChannelIsConnectedOnTheServerSide() throws {
|
|
guard isEarlyEOFDeliveryWorkingOnThisOS else {
|
|
#if os(Linux) || os(Android)
|
|
preconditionFailure("this should only ever be entered on Darwin.")
|
|
#else
|
|
return
|
|
#endif
|
|
}
|
|
// This test makes sure that we notice EOFs early, even if we never register for read (by dropping all the reads
|
|
// on the floor. This is the same test as below but this one is for TCP servers.
|
|
for mode in [DropAllReadsOnTheFloorHandler.Mode.halfClosureEnabled, .halfClosureDisabled] {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
|
|
let channelInactivePromise = group.next().makePromise(of: Void.self)
|
|
let channelHalfClosedPromise = group.next().makePromise(of: Void.self)
|
|
let waitUntilWriteFailedPromise = group.next().makePromise(of: Void.self)
|
|
let channelActivePromise = group.next().makePromise(of: Void.self)
|
|
if mode == .halfClosureDisabled {
|
|
// if we don't support half-closure these two promises would otherwise never be fulfilled
|
|
channelInactivePromise.futureResult.cascade(to: waitUntilWriteFailedPromise)
|
|
channelInactivePromise.futureResult.cascade(to: channelHalfClosedPromise)
|
|
}
|
|
let eventCounter = EventCounterHandler()
|
|
var numberOfAcceptedChannels = 0
|
|
let server = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: mode == .halfClosureEnabled)
|
|
.childChannelInitializer { channel in
|
|
numberOfAcceptedChannels += 1
|
|
XCTAssertEqual(1, numberOfAcceptedChannels)
|
|
let drop = DropAllReadsOnTheFloorHandler(mode: mode,
|
|
channelInactivePromise: channelInactivePromise,
|
|
channelHalfClosedPromise: channelHalfClosedPromise,
|
|
waitUntilWriteFailedPromise: waitUntilWriteFailedPromise,
|
|
channelActivePromise: channelActivePromise)
|
|
return channel.pipeline.addHandlers([eventCounter, drop])
|
|
}
|
|
.bind(to: .init(ipAddress: "127.0.0.1", port: 0)).wait())
|
|
let client = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.connect(to: server.localAddress!).wait())
|
|
XCTAssertNoThrow(try channelActivePromise.futureResult.flatMap { () -> EventLoopFuture<Void> in
|
|
XCTAssertTrue(client.isActive)
|
|
XCTAssertEqual(["register", "channelActive", "channelRegistered"], eventCounter.allTriggeredEvents())
|
|
XCTAssertEqual(1, eventCounter.channelActiveCalls)
|
|
XCTAssertEqual(1, eventCounter.channelRegisteredCalls)
|
|
return client.close()
|
|
}.wait())
|
|
|
|
XCTAssertNoThrow(try channelHalfClosedPromise.futureResult.wait())
|
|
XCTAssertNoThrow(try channelInactivePromise.futureResult.wait())
|
|
XCTAssertNoThrow(try waitUntilWriteFailedPromise.futureResult.wait())
|
|
}
|
|
}
|
|
|
|
func testWeAreInterestedInReadEOFWhenChannelIsConnectedOnTheClientSide() throws {
|
|
guard isEarlyEOFDeliveryWorkingOnThisOS else {
|
|
#if os(Linux) || os(Android)
|
|
preconditionFailure("this should only ever be entered on Darwin.")
|
|
#else
|
|
return
|
|
#endif
|
|
}
|
|
// This test makes sure that we notice EOFs early, even if we never register for read (by dropping all the reads
|
|
// on the floor. This is the same test as above but this one is for TCP clients.
|
|
enum Mode {
|
|
case halfClosureEnabled
|
|
case halfClosureDisabled
|
|
}
|
|
for mode in [DropAllReadsOnTheFloorHandler.Mode.halfClosureEnabled, .halfClosureDisabled] {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
|
|
let acceptedServerChannel: EventLoopPromise<Channel> = group.next().makePromise()
|
|
let channelInactivePromise = group.next().makePromise(of: Void.self)
|
|
let channelHalfClosedPromise = group.next().makePromise(of: Void.self)
|
|
let waitUntilWriteFailedPromise = group.next().makePromise(of: Void.self)
|
|
if mode == .halfClosureDisabled {
|
|
// if we don't support half-closure these two promises would otherwise never be fulfilled
|
|
channelInactivePromise.futureResult.cascade(to: waitUntilWriteFailedPromise)
|
|
channelInactivePromise.futureResult.cascade(to: channelHalfClosedPromise)
|
|
}
|
|
let eventCounter = EventCounterHandler()
|
|
let server = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.childChannelInitializer { channel in
|
|
acceptedServerChannel.succeed(channel)
|
|
return channel.eventLoop.makeSucceededFuture(())
|
|
}
|
|
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
|
|
.wait())
|
|
let client = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.channelOption(ChannelOptions.allowRemoteHalfClosure, value: mode == .halfClosureEnabled)
|
|
.channelInitializer { channel in
|
|
channel.pipeline.addHandlers([eventCounter,
|
|
DropAllReadsOnTheFloorHandler(mode: mode,
|
|
channelInactivePromise: channelInactivePromise,
|
|
channelHalfClosedPromise: channelHalfClosedPromise,
|
|
waitUntilWriteFailedPromise: waitUntilWriteFailedPromise)])
|
|
}
|
|
.connect(to: server.localAddress!).wait())
|
|
XCTAssertNoThrow(try acceptedServerChannel.futureResult.flatMap { channel -> EventLoopFuture<Void> in
|
|
XCTAssertEqual(["register", "channelActive", "channelRegistered", "connect"],
|
|
eventCounter.allTriggeredEvents())
|
|
XCTAssertEqual(1, eventCounter.channelActiveCalls)
|
|
XCTAssertEqual(1, eventCounter.channelRegisteredCalls)
|
|
return channel.close()
|
|
}.wait())
|
|
|
|
XCTAssertNoThrow(try channelHalfClosedPromise.futureResult.wait())
|
|
XCTAssertNoThrow(try channelInactivePromise.futureResult.wait())
|
|
XCTAssertNoThrow(try client.closeFuture.wait())
|
|
XCTAssertNoThrow(try waitUntilWriteFailedPromise.futureResult.wait())
|
|
}
|
|
}
|
|
|
|
func testServerClosesTheConnectionImmediately() throws {
|
|
// This is a regression test for a problem that the grpc-swift compatibility tests hit where everything would
|
|
// get stuck on a server that just insta-closes every accepted connection.
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
|
|
class WaitForChannelInactiveHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Never
|
|
typealias OutboundOut = ByteBuffer
|
|
|
|
let channelInactivePromise: EventLoopPromise<Void>
|
|
|
|
init(channelInactivePromise: EventLoopPromise<Void>) {
|
|
self.channelInactivePromise = channelInactivePromise
|
|
}
|
|
|
|
func channelActive(context: ChannelHandlerContext) {
|
|
var buffer = context.channel.allocator.buffer(capacity: 128)
|
|
buffer.writeString(String(repeating: "x", count: 517))
|
|
context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil)
|
|
}
|
|
|
|
func channelInactive(context: ChannelHandlerContext) {
|
|
self.channelInactivePromise.succeed(())
|
|
context.fireChannelInactive()
|
|
}
|
|
}
|
|
|
|
let serverSocket = try assertNoThrowWithValue(ServerSocket(protocolFamily: .inet))
|
|
XCTAssertNoThrow(try serverSocket.bind(to: .init(ipAddress: "127.0.0.1", port: 0)))
|
|
XCTAssertNoThrow(try serverSocket.listen())
|
|
let g = DispatchGroup()
|
|
DispatchQueue(label: "accept one client").async(group: g) {
|
|
if let socket = try! serverSocket.accept() {
|
|
try! socket.close()
|
|
}
|
|
}
|
|
let channelInactivePromise = group.next().makePromise(of: Void.self)
|
|
let eventCounter = EventCounterHandler()
|
|
let client = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.channelInitializer { channel in
|
|
channel.pipeline.addHandlers([eventCounter,
|
|
WaitForChannelInactiveHandler(channelInactivePromise: channelInactivePromise)])
|
|
}
|
|
.connect(to: try serverSocket.localAddress())
|
|
.wait())
|
|
XCTAssertNoThrow(try channelInactivePromise.futureResult.map { _ in
|
|
XCTAssertEqual(1, eventCounter.channelInactiveCalls)
|
|
}.wait())
|
|
XCTAssertNoThrow(try client.closeFuture.wait())
|
|
g.wait()
|
|
XCTAssertNoThrow(try serverSocket.close())
|
|
}
|
|
|
|
|
|
func testSimpleMPTCP() throws {
|
|
#if os(Linux)
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
|
|
|
|
let serverChannel: Channel
|
|
|
|
do {
|
|
serverChannel = try ServerBootstrap(group: group)
|
|
.enableMPTCP(true)
|
|
.bind(host: "127.0.0.1", port: 0)
|
|
.wait()
|
|
} catch let error as IOError {
|
|
// Older Linux kernel versions don't support MPTCP, which is fine.
|
|
if error.errnoCode != EINVAL && error.errnoCode != EPROTONOSUPPORT {
|
|
XCTFail("Unexpected error: \(error)")
|
|
}
|
|
return
|
|
}
|
|
|
|
let clientChannel = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.enableMPTCP(true)
|
|
.connect(to: serverChannel.localAddress!)
|
|
.wait())
|
|
|
|
do {
|
|
let serverInfo = try (serverChannel as? SocketOptionProvider)?.getMPTCPInfo().wait()
|
|
let clientInfo = try (clientChannel as? SocketOptionProvider)?.getMPTCPInfo().wait()
|
|
|
|
XCTAssertNotNil(serverInfo)
|
|
XCTAssertNotNil(clientInfo)
|
|
} catch let error as IOError {
|
|
// Some Linux kernel versions do support MPTCP but don't support the MPTCP_INFO
|
|
// option.
|
|
XCTAssertEqual(error.errnoCode, EOPNOTSUPP, "Unexpected error: \(error)")
|
|
return
|
|
}
|
|
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
class DropAllReadsOnTheFloorHandler: ChannelDuplexHandler {
|
|
typealias InboundIn = Never
|
|
typealias OutboundIn = Never
|
|
typealias OutboundOut = ByteBuffer
|
|
|
|
enum Mode {
|
|
case halfClosureEnabled
|
|
case halfClosureDisabled
|
|
}
|
|
|
|
let channelInactivePromise: EventLoopPromise<Void>
|
|
let channelHalfClosedPromise: EventLoopPromise<Void>
|
|
let waitUntilWriteFailedPromise: EventLoopPromise<Void>
|
|
let channelActivePromise: EventLoopPromise<Void>?
|
|
let mode: Mode
|
|
|
|
init(mode: Mode,
|
|
channelInactivePromise: EventLoopPromise<Void>,
|
|
channelHalfClosedPromise: EventLoopPromise<Void>,
|
|
waitUntilWriteFailedPromise: EventLoopPromise<Void>,
|
|
channelActivePromise: EventLoopPromise<Void>? = nil) {
|
|
self.mode = mode
|
|
self.channelInactivePromise = channelInactivePromise
|
|
self.channelHalfClosedPromise = channelHalfClosedPromise
|
|
self.waitUntilWriteFailedPromise = waitUntilWriteFailedPromise
|
|
self.channelActivePromise = channelActivePromise
|
|
}
|
|
|
|
func channelActive(context: ChannelHandlerContext) {
|
|
self.channelActivePromise?.succeed(())
|
|
}
|
|
|
|
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
|
|
if let event = event as? ChannelEvent, event == .inputClosed {
|
|
XCTAssertEqual(.halfClosureEnabled, self.mode)
|
|
self.channelHalfClosedPromise.succeed(())
|
|
var buffer = context.channel.allocator.buffer(capacity: 1_000_000)
|
|
buffer.writeBytes(Array(repeating: UInt8(ascii: "x"),
|
|
count: 1_000_000))
|
|
|
|
// What we're trying to do here is forcing a close without calling `close`. We know that the other side of
|
|
// the connection is fully closed but because we support half-closure, we need to write to 'learn' that the
|
|
// other side has actually fully closed the socket.
|
|
func writeUntilError() {
|
|
context.writeAndFlush(self.wrapOutboundOut(buffer)).map {
|
|
writeUntilError()
|
|
}.whenFailure { (_: Error) in
|
|
self.waitUntilWriteFailedPromise.succeed(())
|
|
}
|
|
}
|
|
writeUntilError()
|
|
}
|
|
context.fireUserInboundEventTriggered(event)
|
|
}
|
|
|
|
func channelInactive(context: ChannelHandlerContext) {
|
|
self.channelInactivePromise.succeed(())
|
|
context.fireChannelInactive()
|
|
}
|
|
|
|
func read(context: ChannelHandlerContext) {}
|
|
}
|