Add ChannelOptions to extract base types. (#203)

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
This commit is contained in:
Cory Benfield 2024-05-13 18:30:13 +01:00 committed by GitHub
parent 715e3179d3
commit 38ac8221dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 171 additions and 0 deletions

View File

@ -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")
}

View File

@ -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()
}
@ -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() {}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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