460 lines
21 KiB
Swift
460 lines
21 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2020-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 Atomics
|
|
|
|
final class SALChannelTest: XCTestCase, SALTest {
|
|
var group: MultiThreadedEventLoopGroup!
|
|
var kernelToUserBox: LockedBox<KernelToUser>!
|
|
var userToKernelBox: LockedBox<UserToKernel>!
|
|
var wakeups: LockedBox<()>!
|
|
|
|
override func setUp() {
|
|
self.setUpSAL()
|
|
}
|
|
|
|
override func tearDown() {
|
|
self.tearDownSAL()
|
|
}
|
|
|
|
func testBasicConnectedChannel() throws {
|
|
let localAddress = try! SocketAddress(ipAddress: "0.1.2.3", port: 4)
|
|
let serverAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 5)
|
|
let buffer = ByteBuffer(string: "xxx")
|
|
|
|
let channel = try self.makeConnectedSocketChannel(localAddress: localAddress,
|
|
remoteAddress: serverAddress)
|
|
|
|
try channel.eventLoop.runSAL(syscallAssertions: {
|
|
try self.assertWrite(expectedFD: .max, expectedBytes: buffer, return: .processed(buffer.readableBytes))
|
|
try self.assertWritev(expectedFD: .max, expectedBytes: [buffer, buffer], return: .processed(2 * buffer.readableBytes))
|
|
try self.assertDeregister { selectable in
|
|
try selectable.withUnsafeHandle {
|
|
XCTAssertEqual(.max, $0)
|
|
}
|
|
return true
|
|
}
|
|
try self.assertClose(expectedFD: .max)
|
|
}) {
|
|
channel.writeAndFlush(buffer).flatMap {
|
|
channel.write(buffer, promise: nil)
|
|
return channel.writeAndFlush(buffer)
|
|
}.flatMap {
|
|
channel.close()
|
|
}
|
|
}.salWait()
|
|
}
|
|
|
|
func testWritesFromWritabilityNotificationsDoNotGetLostIfWePreviouslyWroteEverything() {
|
|
// This is a unit test, doing what
|
|
// testWriteAndFlushFromReentrantFlushNowTriggeredOutOfWritabilityWhereOuterSaysAllWrittenAndInnerDoesNot
|
|
// does but in a deterministic way, without having to send actual bytes.
|
|
|
|
let localAddress = try! SocketAddress(ipAddress: "0.1.2.3", port: 4)
|
|
let serverAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 5)
|
|
var buffer = ByteBuffer(string: "12")
|
|
|
|
let writableNotificationStepExpectation = ManagedAtomic(0)
|
|
|
|
final class DoWriteFromWritabilityChangedNotification: ChannelInboundHandler {
|
|
typealias InboundIn = ByteBuffer
|
|
typealias OutboundOut = ByteBuffer
|
|
|
|
var numberOfCalls = 0
|
|
|
|
var writableNotificationStepExpectation: ManagedAtomic<Int>
|
|
|
|
init(writableNotificationStepExpectation: ManagedAtomic<Int>) {
|
|
self.writableNotificationStepExpectation = writableNotificationStepExpectation
|
|
}
|
|
|
|
func channelWritabilityChanged(context: ChannelHandlerContext) {
|
|
self.numberOfCalls += 1
|
|
|
|
XCTAssertEqual(self.writableNotificationStepExpectation.load(ordering: .relaxed),
|
|
numberOfCalls)
|
|
switch self.numberOfCalls {
|
|
case 1:
|
|
// First, we should see a `false` here because 2 bytes is above the high watermark.
|
|
XCTAssertFalse(context.channel.isWritable)
|
|
case 2:
|
|
// Then, we should go back to `true` from a `writable` notification. Now, let's write 3 bytes which
|
|
// will exhaust the high watermark. We'll also set up (further down) that we only partially write
|
|
// those 3 bytes.
|
|
XCTAssertTrue(context.channel.isWritable)
|
|
var buffer = context.channel.allocator.buffer(capacity: 3)
|
|
buffer.writeString("ABC")
|
|
|
|
// We expect another channelWritabilityChanged notification
|
|
XCTAssertTrue(self.writableNotificationStepExpectation.compareExchange(expected: 2, desired: 3, ordering: .relaxed).exchanged)
|
|
context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil)
|
|
case 3:
|
|
// Next, we should go to false because we never send all the bytes.
|
|
XCTAssertFalse(context.channel.isWritable)
|
|
case 4:
|
|
// And finally, back to `true` because eventually, we'll write enough.
|
|
XCTAssertTrue(context.channel.isWritable)
|
|
default:
|
|
XCTFail("call \(self.numberOfCalls) unexpected (\(context.channel.isWritable))")
|
|
}
|
|
}
|
|
}
|
|
|
|
var maybeChannel: SocketChannel? = nil
|
|
XCTAssertNoThrow(maybeChannel = try self.makeConnectedSocketChannel(localAddress: localAddress,
|
|
remoteAddress: serverAddress))
|
|
guard let channel = maybeChannel else {
|
|
XCTFail("couldn't construct channel")
|
|
return
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.eventLoop.runSAL(syscallAssertions: {
|
|
// We get in a write of 2 bytes, and we claim we wrote 1 bytes of that.
|
|
try self.assertWrite(expectedFD: .max, expectedBytes: buffer, return: .processed(1))
|
|
|
|
// Next, we expect a reregistration which adds the `.write` notification
|
|
try self.assertReregister { selectable, eventSet in
|
|
XCTAssert(selectable as? Socket === channel.socket)
|
|
XCTAssertEqual([.read, .reset, .readEOF, .write], eventSet)
|
|
return true
|
|
}
|
|
|
|
// Before sending back the writable notification, we know that that'll trigger a Channel writability change
|
|
XCTAssertTrue(writableNotificationStepExpectation.compareExchange(expected: 1, desired: 2, ordering: .relaxed).exchanged)
|
|
let writableEvent = SelectorEvent(io: [.write],
|
|
registration: NIORegistration(channel: .socketChannel(channel),
|
|
interested: [.write],
|
|
registrationID: .initialRegistrationID))
|
|
try self.assertWaitingForNotification(result: writableEvent)
|
|
try self.assertWrite(expectedFD: .max,
|
|
expectedBytes: buffer.getSlice(at: 1, length: 1)!,
|
|
return: .processed(1))
|
|
buffer.clear()
|
|
buffer.writeString("ABC") // expected
|
|
|
|
// This time, the write again, just writes one byte, so we should remain registered for writable.
|
|
try self.assertWrite(expectedFD: .max,
|
|
expectedBytes: buffer,
|
|
return: .processed(1))
|
|
buffer.moveReaderIndex(forwardBy: 1)
|
|
|
|
// Let's send them another 'writable' notification:
|
|
try self.assertWaitingForNotification(result: writableEvent)
|
|
|
|
// This time, we'll make the write write everything which should also lead to a final channelWritability
|
|
// change.
|
|
XCTAssertTrue(writableNotificationStepExpectation.compareExchange(expected: 3, desired: 4, ordering: .relaxed).exchanged)
|
|
try self.assertWrite(expectedFD: .max,
|
|
expectedBytes: buffer,
|
|
return: .processed(2))
|
|
|
|
// And lastly, after having written everything, we'd expect to unregister for write
|
|
try self.assertReregister { selectable, eventSet in
|
|
XCTAssert(selectable as? Socket === channel.socket)
|
|
XCTAssertEqual([.read, .reset, .readEOF], eventSet)
|
|
return true
|
|
}
|
|
|
|
try self.assertParkedRightNow()
|
|
}) { () -> EventLoopFuture<Void> in
|
|
channel.setOption(ChannelOptions.writeSpin, value: 0).flatMap {
|
|
channel.setOption(ChannelOptions.writeBufferWaterMark, value: .init(low: 1, high: 1))
|
|
}.flatMap {
|
|
channel.pipeline.addHandler(DoWriteFromWritabilityChangedNotification(writableNotificationStepExpectation: writableNotificationStepExpectation))
|
|
}.flatMap {
|
|
// This write should cause a Channel writability change.
|
|
XCTAssertTrue(writableNotificationStepExpectation.compareExchange(expected: 0, desired: 1, ordering: .relaxed).exchanged)
|
|
return channel.writeAndFlush(buffer)
|
|
}
|
|
}.salWait())
|
|
}
|
|
|
|
func testWeSurviveIfIgnoringSIGPIPEFails() {
|
|
// We know this sometimes happens on Darwin, so let's test it.
|
|
let expectedError = IOError(errnoCode: EINVAL, reason: "bad")
|
|
XCTAssertThrowsError(try self.makeSocketChannelInjectingFailures(disableSIGPIPEFailure: expectedError)) { error in
|
|
XCTAssertEqual(expectedError.errnoCode, (error as? IOError)?.errnoCode)
|
|
}
|
|
}
|
|
|
|
func testBasicRead() {
|
|
let localAddress = try! SocketAddress(ipAddress: "0.1.2.3", port: 4)
|
|
let serverAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 5)
|
|
|
|
final class SignalGroupOnRead: ChannelInboundHandler {
|
|
typealias InboundIn = ByteBuffer
|
|
|
|
private let group: DispatchGroup
|
|
private var numberOfCalls = 0
|
|
|
|
init(group: DispatchGroup) {
|
|
self.group = group
|
|
}
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
self.numberOfCalls += 1
|
|
XCTAssertEqual("hello",
|
|
String(decoding: self.unwrapInboundIn(data).readableBytesView, as: Unicode.UTF8.self))
|
|
if self.numberOfCalls == 1 {
|
|
self.group.leave()
|
|
}
|
|
}
|
|
}
|
|
|
|
var maybeChannel: SocketChannel? = nil
|
|
XCTAssertNoThrow(maybeChannel = try self.makeConnectedSocketChannel(localAddress: localAddress,
|
|
remoteAddress: serverAddress))
|
|
guard let channel = maybeChannel else {
|
|
XCTFail("couldn't construct channel")
|
|
return
|
|
}
|
|
|
|
let buffer = ByteBuffer(string: "hello")
|
|
|
|
let g = DispatchGroup()
|
|
g.enter()
|
|
|
|
XCTAssertNoThrow(try channel.eventLoop.runSAL(syscallAssertions: {
|
|
let readEvent = SelectorEvent(io: [.read],
|
|
registration: NIORegistration(channel: .socketChannel(channel),
|
|
interested: [.read],
|
|
registrationID: .initialRegistrationID))
|
|
try self.assertWaitingForNotification(result: readEvent)
|
|
try self.assertRead(expectedFD: .max, expectedBufferSpace: 2048, return: buffer)
|
|
}) {
|
|
channel.pipeline.addHandler(SignalGroupOnRead(group: g))
|
|
})
|
|
|
|
g.wait()
|
|
}
|
|
|
|
func testBasicConnectWithClientBootstrap() {
|
|
guard let channel = try? self.makeSocketChannel() else {
|
|
XCTFail("couldn't make a channel")
|
|
return
|
|
}
|
|
let localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 5)
|
|
let serverAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 5)
|
|
XCTAssertNoThrow(try channel.eventLoop.runSAL(syscallAssertions: {
|
|
try self.assertSetOption(expectedLevel: .tcp, expectedOption: .tcp_nodelay) { value in
|
|
return (value as? SocketOptionValue) == 1
|
|
}
|
|
try self.assertConnect(expectedAddress: serverAddress, result: true)
|
|
try self.assertLocalAddress(address: localAddress)
|
|
try self.assertRemoteAddress(address: localAddress)
|
|
try self.assertRegister { selectable, event, Registration in
|
|
XCTAssertEqual([.reset], event)
|
|
return true
|
|
}
|
|
try self.assertReregister { selectable, event in
|
|
XCTAssertEqual([.reset, .readEOF], event)
|
|
return true
|
|
}
|
|
try self.assertDeregister { selectable in
|
|
return true
|
|
}
|
|
try self.assertClose(expectedFD: .max)
|
|
}) {
|
|
ClientBootstrap(group: channel.eventLoop)
|
|
.channelOption(ChannelOptions.autoRead, value: false)
|
|
.testOnly_connect(injectedChannel: channel, to: serverAddress)
|
|
.flatMap { channel in
|
|
channel.close()
|
|
}
|
|
}.salWait())
|
|
}
|
|
|
|
func testClientBootstrapBindIsDoneAfterSocketOptions() {
|
|
guard let channel = try? self.makeSocketChannel() else {
|
|
XCTFail("couldn't make a channel")
|
|
return
|
|
}
|
|
let localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 5)
|
|
let serverAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 5)
|
|
XCTAssertNoThrow(try channel.eventLoop.runSAL(syscallAssertions: {
|
|
try self.assertSetOption(expectedLevel: .tcp, expectedOption: .tcp_nodelay) { value in
|
|
return (value as? SocketOptionValue) == 1
|
|
}
|
|
// This is the important bit: We need to apply the socket options _before_ ...
|
|
try self.assertSetOption(expectedLevel: .socket, expectedOption: .so_reuseaddr) { value in
|
|
return (value as? SocketOptionValue) == 1
|
|
}
|
|
// ... we call bind.
|
|
try self.assertBind(expectedAddress: localAddress)
|
|
try self.assertLocalAddress(address: nil) // this is an inefficiency in `bind0`.
|
|
try self.assertConnect(expectedAddress: serverAddress, result: true)
|
|
try self.assertLocalAddress(address: localAddress)
|
|
try self.assertRemoteAddress(address: localAddress)
|
|
try self.assertRegister { selectable, event, Registration in
|
|
XCTAssertEqual([.reset], event)
|
|
return true
|
|
}
|
|
try self.assertReregister { selectable, event in
|
|
XCTAssertEqual([.reset, .readEOF], event)
|
|
return true
|
|
}
|
|
try self.assertDeregister { selectable in
|
|
return true
|
|
}
|
|
try self.assertClose(expectedFD: .max)
|
|
}) {
|
|
ClientBootstrap(group: channel.eventLoop)
|
|
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
|
.channelOption(ChannelOptions.autoRead, value: false)
|
|
.bind(to: localAddress)
|
|
.testOnly_connect(injectedChannel: channel, to: serverAddress)
|
|
.flatMap { channel in
|
|
channel.close()
|
|
}
|
|
}.salWait())
|
|
}
|
|
|
|
func testAcceptingInboundConnections() throws {
|
|
final class ConnectionRecorder: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
typealias InboundOut = Any
|
|
|
|
let readCount = ManagedAtomic(0)
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
readCount.wrappingIncrement(ordering: .sequentiallyConsistent)
|
|
context.fireChannelRead(data)
|
|
}
|
|
}
|
|
|
|
let localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 5)
|
|
let remoteAddress = try! SocketAddress(ipAddress: "5.6.7.8", port: 10)
|
|
let channel = try self.makeBoundServerSocketChannel(localAddress: localAddress)
|
|
|
|
let socket = try self.makeSocket()
|
|
|
|
let readRecorder = ConnectionRecorder()
|
|
XCTAssertNoThrow(try channel.eventLoop.runSAL(syscallAssertions: {
|
|
let readEvent = SelectorEvent(io: [.read],
|
|
registration: NIORegistration(channel: .serverSocketChannel(channel),
|
|
interested: [.read],
|
|
registrationID: .initialRegistrationID))
|
|
try self.assertWaitingForNotification(result: readEvent)
|
|
try self.assertAccept(expectedFD: .max, expectedNonBlocking: true, return: socket)
|
|
try self.assertLocalAddress(address: localAddress)
|
|
try self.assertRemoteAddress(address: remoteAddress)
|
|
|
|
// This accept is expected: we delay inbound channel registration by one EL tick.
|
|
try self.assertAccept(expectedFD: .max, expectedNonBlocking: true, return: nil)
|
|
|
|
// Then we register the inbound channel.
|
|
try self.assertRegister { selectable, eventSet, registration in
|
|
if case (.socketChannel(let channel), let registrationEventSet) =
|
|
(registration.channel, registration.interested) {
|
|
|
|
XCTAssertEqual(localAddress, channel.localAddress)
|
|
XCTAssertEqual(remoteAddress, channel.remoteAddress)
|
|
XCTAssertEqual(eventSet, registrationEventSet)
|
|
XCTAssertEqual(.reset, eventSet)
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
try self.assertReregister { selectable, eventSet in
|
|
XCTAssertEqual([.reset, .readEOF], eventSet)
|
|
return true
|
|
}
|
|
// because autoRead is on by default
|
|
try self.assertReregister { selectable, eventSet in
|
|
XCTAssertEqual([.reset, .readEOF, .read], eventSet)
|
|
return true
|
|
}
|
|
|
|
try self.assertParkedRightNow()
|
|
}) {
|
|
channel.pipeline.addHandler(readRecorder)
|
|
})
|
|
|
|
XCTAssertEqual(readRecorder.readCount.load(ordering: .sequentiallyConsistent), 1)
|
|
}
|
|
|
|
func testAcceptingInboundConnectionsDoesntUnregisterForReadIfTheSecondAcceptErrors() throws {
|
|
final class ConnectionRecorder: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
typealias InboundOut = Any
|
|
|
|
let readCount = ManagedAtomic(0)
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
readCount.wrappingIncrement(ordering: .sequentiallyConsistent)
|
|
context.fireChannelRead(data)
|
|
}
|
|
}
|
|
|
|
let localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 5)
|
|
let remoteAddress = try! SocketAddress(ipAddress: "5.6.7.8", port: 10)
|
|
let channel = try self.makeBoundServerSocketChannel(localAddress: localAddress)
|
|
|
|
let socket = try self.makeSocket()
|
|
|
|
let readRecorder = ConnectionRecorder()
|
|
XCTAssertNoThrow(try channel.eventLoop.runSAL(syscallAssertions: {
|
|
let readEvent = SelectorEvent(io: [.read],
|
|
registration: NIORegistration(channel: .serverSocketChannel(channel),
|
|
interested: [.read],
|
|
registrationID: .initialRegistrationID))
|
|
try self.assertWaitingForNotification(result: readEvent)
|
|
try self.assertAccept(expectedFD: .max, expectedNonBlocking: true, return: socket)
|
|
try self.assertLocalAddress(address: localAddress)
|
|
try self.assertRemoteAddress(address: remoteAddress)
|
|
|
|
// This accept is expected: we delay inbound channel registration by one EL tick. This one throws.
|
|
// We throw a deliberate error here: this one hits the buggy codepath.
|
|
try self.assertAccept(expectedFD: .max, expectedNonBlocking: true, throwing: NIOFcntlFailedError())
|
|
|
|
// Then we register the inbound channel from the first accept.
|
|
try self.assertRegister { selectable, eventSet, registration in
|
|
if case (.socketChannel(let channel), let registrationEventSet) =
|
|
(registration.channel, registration.interested) {
|
|
|
|
XCTAssertEqual(localAddress, channel.localAddress)
|
|
XCTAssertEqual(remoteAddress, channel.remoteAddress)
|
|
XCTAssertEqual(eventSet, registrationEventSet)
|
|
XCTAssertEqual(.reset, eventSet)
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
try self.assertReregister { selectable, eventSet in
|
|
XCTAssertEqual([.reset, .readEOF], eventSet)
|
|
return true
|
|
}
|
|
// because autoRead is on by default
|
|
try self.assertReregister { selectable, eventSet in
|
|
XCTAssertEqual([.reset, .readEOF, .read], eventSet)
|
|
return true
|
|
}
|
|
|
|
// Importantly, we should now be _parked_. This test is mostly testing in the absence:
|
|
// we expect not to see a reregister that removes readable.
|
|
try self.assertParkedRightNow()
|
|
}) {
|
|
channel.pipeline.addHandler(readRecorder)
|
|
})
|
|
|
|
XCTAssertEqual(readRecorder.readCount.load(ordering: .sequentiallyConsistent), 1)
|
|
}
|
|
|
|
}
|