Skip to content

Commit

Permalink
Add ChannelOptions to extract base types. (#203)
Browse files Browse the repository at this point in the history
Motivation:

In some cases users may want to access APIs we haven't exposed
in NIOTransportServices. We should have a fallback that allows users
to do this.

Modifications:

- Add ChannelOptions for getting NWConnection and NWListener.

Result:

Users have an escape hatch
  • Loading branch information
Lukasa authored May 13, 2024
1 parent 715e317 commit 38ac822
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 0 deletions.
10 changes: 10 additions & 0 deletions Sources/NIOTransportServices/Datagram/NIOTSDatagramChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ internal final class NIOTSDatagramChannel: StateManagedNWConnectionChannel {
}

func getChannelSpecificOption0<Option>(option: Option) throws -> Option.Value where Option : ChannelOption {
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
switch option {
case is NIOTSChannelOptions.Types.NIOTSConnectionOption:
return self.connection as! Option.Value
default:
// Check the non-constrained options.
()
}
}

fatalError("option \(type(of: option)).\(option) not supported")
}

Expand Down
36 changes: 36 additions & 0 deletions Sources/NIOTransportServices/NIOTSChannelOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public struct NIOTSChannelOptions {

/// See: ``Types/NIOTSMultipathOption``
public static let multipathServiceType = NIOTSChannelOptions.Types.NIOTSMultipathOption()

/// See: ``Types/NIOTSConnectionOption``.
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public static let connection = NIOTSChannelOptions.Types.NIOTSConnectionOption()

/// See: ``Types/NIOTSListenerOption``.
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public static let listener = NIOTSChannelOptions.Types.NIOTSListenerOption()
}


Expand Down Expand Up @@ -143,6 +151,34 @@ extension NIOTSChannelOptions {

public init() {}
}

/// ``NIOTSConnectionOption`` accesses the `NWConnection` of the underlying connection.
///
/// > Warning: Callers must be extremely careful with this option, as it is easy to break an existing
/// > connection that uses it. NIOTS doesn't support arbitrary modifications of the `NWConnection`
/// > underlying a `Channel`.
///
/// This option is only valid with a `Channel` backed by an `NWConnection`.
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public struct NIOTSConnectionOption: ChannelOption, Equatable {
public typealias Value = NWConnection?

public init() {}
}

/// ``NIOTSListenerOption`` accesses the `NWListener` of the underlying connection.
///
/// > Warning: Callers must be extremely careful with this option, as it is easy to break an existing
/// > connection that uses it. NIOTS doesn't support arbitrary modifications of the `NWListener`
/// > underlying a `Channel`.
///
/// This option is only valid with a `Channel` backed by an `NWListener`.
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public struct NIOTSListenerOption: ChannelOption, Equatable {
public typealias Value = NWListener?

public init() {}
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/NIOTransportServices/NIOTSConnectionChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ internal final class NIOTSConnectionChannel: StateManagedNWConnectionChannel {
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
extension NIOTSConnectionChannel: Channel {
func getChannelSpecificOption0<Option>(option: Option) throws -> Option.Value where Option : ChannelOption {
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
switch option {
case is NIOTSChannelOptions.Types.NIOTSConnectionOption:
return self.connection as! Option.Value
default:
// Fallthrough to non-restricted options.
()
}
}

switch option {
case is NIOTSChannelOptions.Types.NIOTSMultipathOption:
return self.multipathServiceType as! Option.Value
Expand Down
10 changes: 10 additions & 0 deletions Sources/NIOTransportServices/StateManagedListenerChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,16 @@ extension StateManagedListenerChannel {
throw ChannelError.ioOnClosedChannel
}

if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
switch option {
case is NIOTSChannelOptions.Types.NIOTSListenerOption:
return self.nwListener as! Option.Value
default:
// Fallthrough to non-restricted options
()
}
}

switch option {
case is ChannelOptions.Types.AutoReadOption:
return autoRead as! Option.Value
Expand Down
29 changes: 29 additions & 0 deletions Tests/NIOTransportServicesTests/NIOTSConnectionChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -918,5 +918,34 @@ class NIOTSConnectionChannelTests: XCTestCase {
XCTAssertNoThrow(try connection.close().wait())
XCTAssertNoThrow(try testCompletePromise.futureResult.wait())
}

func testCanExtractTheConnection() throws {
guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else {
throw XCTSkip("Option not available")
}

let listener = try NIOTSListenerBootstrap(group: self.group)
.bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}

_ = try NIOTSConnectionBootstrap(group: self.group)
.channelInitializer { channel in
let conn = try! channel.syncOptions!.getOption(NIOTSChannelOptions.connection)
XCTAssertNil(conn)
return channel.eventLoop.makeSucceededVoidFuture()
}.connect(to: listener.localAddress!).flatMap {
$0.getOption(NIOTSChannelOptions.connection)
}.always { result in
switch result {
case .success(let connection):
// Make sure we unwrap the connection.
XCTAssertNotNil(connection)
case .failure(let error):
XCTFail("Unexpected error: \(error)")
}
}.wait()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,58 @@ final class NIOTSDatagramConnectionChannelTests: XCTestCase {
_ = try serverHandle.waitForDatagrams(count: 1)
XCTAssertNoThrow(try connection.close().wait())
}

func testCanExtractTheConnection() throws {
guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else {
throw XCTSkip("Option not available")
}

let listener = try NIOTSDatagramListenerBootstrap(group: self.group)
.bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}

_ = try NIOTSDatagramBootstrap(group: self.group)
.channelInitializer { channel in
let conn = try! channel.syncOptions!.getOption(NIOTSChannelOptions.connection)
XCTAssertNil(conn)
return channel.eventLoop.makeSucceededVoidFuture()
}.connect(to: listener.localAddress!).flatMap {
$0.getOption(NIOTSChannelOptions.connection)
}.always { result in
switch result {
case .success(let connection):
// Make sure we unwrap the connection.
XCTAssertNotNil(connection)
case .failure(let error):
XCTFail("Unexpected error: \(error)")
}
}.wait()
}


func testCanExtractTheListener() throws {
guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else {
throw XCTSkip("Option not available")
}

let listener = try NIOTSDatagramListenerBootstrap(group: self.group)
.serverChannelInitializer { channel in
let underlyingListener = try! channel.syncOptions!.getOption(NIOTSChannelOptions.listener)
XCTAssertNil(underlyingListener)
return channel.eventLoop.makeSucceededVoidFuture()
}
.bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}

let listenerFuture: EventLoopFuture<NWListener?> = listener.getOption(NIOTSChannelOptions.listener)

try listenerFuture.map { listener in
XCTAssertNotNil(listener)
}.wait()
}
}
#endif
23 changes: 23 additions & 0 deletions Tests/NIOTransportServicesTests/NIOTSListenerChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,5 +332,28 @@ class NIOTSListenerChannelTests: XCTestCase {

XCTAssertNoThrow(try listener.close().wait())
}

func testCanExtractTheListener() throws {
guard #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) else {
throw XCTSkip("Listener option not available")
}

let listener = try NIOTSListenerBootstrap(group: self.group)
.serverChannelInitializer { channel in
let underlyingListener = try! channel.syncOptions!.getOption(NIOTSChannelOptions.listener)
XCTAssertNil(underlyingListener)
return channel.eventLoop.makeSucceededVoidFuture()
}
.bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}

let listenerFuture: EventLoopFuture<NWListener?> = listener.getOption(NIOTSChannelOptions.listener)

try listenerFuture.map { listener in
XCTAssertNotNil(listener)
}.wait()
}
}
#endif

0 comments on commit 38ac822

Please sign in to comment.