Skip to content

Fixed sendability warnings when -require-explict-sendable flag is enabled #211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors"
linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors"
linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"

benchmarks:
name: Benchmarks
Expand All @@ -29,3 +29,5 @@ jobs:
with:
runner_pool: nightly
build_scheme: swift-nio-ssh-Package
xcode_16_2_build_arguments_override: "-Xswiftc -Xfrontend -Xswiftc -require-explicit-sendable"
xcode_16_3_build_arguments_override: "-Xswiftc -Xfrontend -Xswiftc -require-explicit-sendable"
10 changes: 6 additions & 4 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors"
linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors"
linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"

benchmarks:
name: Benchmarks
Expand All @@ -36,3 +36,5 @@ jobs:
with:
runner_pool: general
build_scheme: swift-nio-ssh-Package
xcode_16_2_build_arguments_override: "-Xswiftc -Xfrontend -Xswiftc -require-explicit-sendable"
xcode_16_3_build_arguments_override: "-Xswiftc -Xfrontend -Xswiftc -require-explicit-sendable"
2 changes: 1 addition & 1 deletion Sources/NIOSSH/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

public enum Constants {
public enum Constants: Sendable {
static let version = "SSH-2.0-SwiftNIOSSH_1.0"

public static let bundledTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [
Expand Down
2 changes: 2 additions & 0 deletions Sources/NIOSSH/GlobalRequestDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import NIOCore
public protocol GlobalRequestDelegate {
/// The client wants to manage TCP port forwarding.
///
/// The eventLoop associated with the promise passed in must be the same as the one used to create the handler.
///
/// The default implementation rejects all requests to establish TCP port forwarding.
func tcpForwardingRequest(
_: GlobalRequest.TCPForwardingRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import NIOCore
/// A server user authentication delegate that denies all authentication attempts.
///
/// Not really useful in and of itself, but a helpful default option.
public final class DenyAllServerAuthDelegate {}
public final class DenyAllServerAuthDelegate: Sendable {}

extension DenyAllServerAuthDelegate: NIOSSHServerUserAuthenticationDelegate {
public var supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods {
Expand Down
9 changes: 5 additions & 4 deletions Sources/NIOSSHServer/DataToBufferCodec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Dispatch
import Foundation
Expand All @@ -21,14 +21,15 @@ import NIOFoundationCompat
import NIOPosix
import NIOSSH

final class DataToBufferCodec: ChannelDuplexHandler {
final class DataToBufferCodec: ChannelDuplexHandler, Sendable {
typealias InboundIn = SSHChannelData
typealias InboundOut = ByteBuffer
typealias OutboundIn = ByteBuffer
typealias OutboundOut = SSHChannelData

func handlerAdded(context: ChannelHandlerContext) {
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).assumeIsolated().whenFailure {
error in
context.fireErrorCaught(error)
}
}
Expand Down Expand Up @@ -60,4 +61,4 @@ func createOutboundConnection(targetHost: String, targetPort: Int, loop: EventLo
}.connect(host: targetHost, port: targetPort)
}

#endif // canImport(Foundation.Process)
#endif
63 changes: 33 additions & 30 deletions Sources/NIOSSHServer/ExecHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Dispatch
import Foundation
Expand Down Expand Up @@ -40,7 +40,8 @@ final class ExampleExecHandler: ChannelDuplexHandler {
var environment: [String: String] = [:]

func handlerAdded(context: ChannelHandlerContext) {
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).assumeIsolated().whenFailure {
error in
context.fireErrorCaught(error)
}
}
Expand Down Expand Up @@ -90,39 +91,42 @@ final class ExampleExecHandler: ChannelDuplexHandler {
}

private func exec(_ event: SSHChannelRequestEvent.ExecRequest, channel: Channel) {
// Kick this off to a background queue
self.queue.async {
do {
// We're not a shell, so we just do our "best".
let executable = URL(fileURLWithPath: "/usr/local/bin/bash")
let process = Process()
process.executableURL = executable
process.arguments = ["-c", event.command]
process.terminationHandler = { process in
// The process terminated. Check its return code, fire it, and then move on.
let rcode = process.terminationStatus
channel.triggerUserOutboundEvent(SSHChannelRequestEvent.ExitStatus(exitStatus: Int(rcode)))
.whenComplete { _ in
channel.close(promise: nil)
}
// We're not a shell, so we just do our "best".
let executable = URL(fileURLWithPath: "/usr/local/bin/bash")
let process = Process()
process.executableURL = executable
process.arguments = ["-c", event.command]
process.terminationHandler = { process in
// The process terminated. Check its return code, fire it, and then move on.
let rcode = process.terminationStatus
channel.triggerUserOutboundEvent(SSHChannelRequestEvent.ExitStatus(exitStatus: Int(rcode)))
.whenComplete { _ in
channel.close(promise: nil)
}
}

let inPipe = Pipe()
let outPipe = Pipe()
let errPipe = Pipe()

process.standardInput = inPipe
process.standardOutput = outPipe
process.standardError = errPipe
process.environment = self.environment
let inPipe = Pipe()
let outPipe = Pipe()
let errPipe = Pipe()

let (ours, theirs) = GlueHandler.matchedPair()
try channel.pipeline.addHandler(ours).wait()
process.standardInput = inPipe
process.standardOutput = outPipe
process.standardError = errPipe
process.environment = self.environment
self.process = process

// Kick this off to a background queue
self.queue.async {
do {
_ = try NIOPipeBootstrap(group: channel.eventLoop)
.channelOption(ChannelOptions.allowRemoteHalfClosure, value: true)
.channelInitializer { pipeChannel in
pipeChannel.pipeline.addHandler(theirs)
pipeChannel.eventLoop.makeCompletedFuture {
let (ours, theirs) = GlueHandler.matchedPair()
try channel.pipeline.syncOperations.addHandler(ours)
try pipeChannel.pipeline.syncOperations.addHandler(theirs)
}

}.takingOwnershipOfDescriptors(
input: dup(outPipe.fileHandleForReading.fileDescriptor),
output: dup(inPipe.fileHandleForWriting.fileDescriptor)
Expand All @@ -149,7 +153,6 @@ final class ExampleExecHandler: ChannelDuplexHandler {
}

try process.run()
self.process = process
} catch {
if event.wantReply {
channel.triggerUserOutboundEvent(ChannelFailureEvent()).whenComplete { _ in
Expand All @@ -163,4 +166,4 @@ final class ExampleExecHandler: ChannelDuplexHandler {
}
}

#endif // canImport(Foundation.Process)
#endif
4 changes: 2 additions & 2 deletions Sources/NIOSSHServer/GlueHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import NIOCore

Expand Down Expand Up @@ -125,4 +125,4 @@ extension GlueHandler: ChannelDuplexHandler {
}
}

#endif // canImport(Foundation.Process)
#endif
61 changes: 32 additions & 29 deletions Sources/NIOSSHServer/RemotePortForwarding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Foundation.Process)
#if os(macOS) || os(Linux)

import Dispatch
import Foundation
Expand All @@ -26,16 +26,16 @@ import NIOSSH
// Please note that, as with the rest of this example, there are important security features missing from
// this demo.
final class RemotePortForwarder {
private var serverChannel: Channel?

private var inboundSSHHandler: NIOSSHHandler
private let inboundSSHHandler: NIOSSHHandler

init(inboundSSHHandler: NIOSSHHandler) {
self.inboundSSHHandler = inboundSSHHandler
}

func beginListening(on host: String, port: Int, loop: EventLoop) -> EventLoopFuture<Int?> {
ServerBootstrap(group: loop).serverChannelOption(
let loopBoundHandler = NIOLoopBound(inboundSSHHandler, eventLoop: loop)

return ServerBootstrap(group: loop).serverChannelOption(
ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SocketOptionName(SO_REUSEADDR)),
value: 1
)
Expand All @@ -45,29 +45,34 @@ final class RemotePortForwarder {
value: 1
)
.childChannelInitializer { childChannel in
let (ours, theirs) = GlueHandler.matchedPair()

// Ok, ask for the remote channel to be created. This needs remote half closure turned on and to be
// set up for data I/O.
let promise = loop.makePromise(of: Channel.self)
self.inboundSSHHandler.createChannel(
promise,
channelType: .forwardedTCPIP(
.init(
listeningHost: host,
listeningPort: childChannel.localAddress!.port!,
originatorAddress: childChannel.remoteAddress!
// Great, now we add the glue handler to the newly-accepted channel, and then we don't allow this channel to go
// active until the SSH channel has. Both should go active at once.
childChannel.eventLoop.makeCompletedFuture {
let (ours, theirs) = GlueHandler.matchedPair()

// Ok, ask for the remote channel to be created. This needs remote half closure turned on and to be
// set up for data I/O.
let promise = loop.makePromise(of: Channel.self)
loopBoundHandler.value.createChannel(
promise,
channelType: .forwardedTCPIP(
.init(
listeningHost: host,
listeningPort: childChannel.localAddress!.port!,
originatorAddress: childChannel.remoteAddress!
)
)
)
) { sshChildChannel, _ in
sshChildChannel.pipeline.addHandlers([DataToBufferCodec(), theirs]).flatMap {
sshChildChannel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
) { sshChildChannel, _ in
sshChildChannel.eventLoop.makeCompletedFuture {
try sshChildChannel.pipeline.syncOperations.addHandlers([DataToBufferCodec(), theirs])
}.flatMap {
sshChildChannel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
}

}
}

// Great, now we add the glue handler to the newly-accepted channel, and then we don't allow this channel to go
// active until the SSH channel has. Both should go active at once.
return childChannel.pipeline.addHandler(ours).flatMap { _ in promise.futureResult }.map { _ in () }
try childChannel.pipeline.syncOperations.addHandler(ours)
}
}
.bind(host: host, port: port).map { channel in
if port == 0 {
Expand All @@ -78,9 +83,7 @@ final class RemotePortForwarder {
}
}

func stopListening() {
self.serverChannel?.close(promise: nil)
}
func stopListening() {}
}

final class RemotePortForwarderGlobalRequestDelegate: GlobalRequestDelegate {
Expand Down Expand Up @@ -119,4 +122,4 @@ final class RemotePortForwarderGlobalRequestDelegate: GlobalRequestDelegate {
}
}

#endif // canImport(Foundation.Process)
#endif
Loading