Skip to content

Commit e1a0025

Browse files
authored
allow client/server to be initialised with a connected socket (#1385)
Motivation: #1353 Modifications: - A new entry on ConnectionTarget - New API on the ClientConnection builder - Some validation that we're not trying to use this API with a NIOTSEventLoopGroup or NIOTSEventLoop (as far as I can tell, using a file descriptor directly is not possible with Network.framework) - Tests Result: This allows greater flexibility in spawning the client/server; in particular, it allows unix domain sockets for sandboxed apps on macOS.
1 parent 80055e5 commit e1a0025

File tree

6 files changed

+123
-3
lines changed

6 files changed

+123
-3
lines changed

Sources/GRPC/ClientConnection.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ public struct ConnectionTarget {
266266
case hostAndPort(String, Int)
267267
case unixDomainSocket(String)
268268
case socketAddress(SocketAddress)
269+
case connectedSocket(NIOBSDSocket.Handle)
269270
}
270271

271272
internal var wrapped: Wrapped
@@ -293,6 +294,11 @@ public struct ConnectionTarget {
293294
return ConnectionTarget(.socketAddress(address))
294295
}
295296

297+
/// A connected NIO socket.
298+
public static func connectedSocket(_ socket: NIOBSDSocket.Handle) -> ConnectionTarget {
299+
return ConnectionTarget(.connectedSocket(socket))
300+
}
301+
296302
@usableFromInline
297303
var host: String {
298304
switch self.wrapped {
@@ -302,7 +308,7 @@ public struct ConnectionTarget {
302308
return address.host
303309
case let .socketAddress(.v6(address)):
304310
return address.host
305-
case .unixDomainSocket, .socketAddress(.unixDomainSocket):
311+
case .unixDomainSocket, .socketAddress(.unixDomainSocket), .connectedSocket:
306312
return "localhost"
307313
}
308314
}
@@ -540,6 +546,8 @@ extension ClientBootstrapProtocol {
540546

541547
case let .socketAddress(address):
542548
return self.connect(to: address)
549+
case let .connectedSocket(socket):
550+
return self.withConnectedSocket(socket)
543551
}
544552
}
545553
}

Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ extension ClientConnection {
8585
self.configuration.tlsConfiguration = self.maybeTLS
8686
return ClientConnection(configuration: self.configuration)
8787
}
88+
89+
public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> ClientConnection {
90+
precondition(
91+
!PlatformSupport.isTransportServicesEventLoopGroup(self.configuration.eventLoopGroup),
92+
"'\(#function)' requires 'group' to not be a 'NIOTransportServices.NIOTSEventLoopGroup' or 'NIOTransportServices.QoSEventLoop' (but was '\(type(of: self.configuration.eventLoopGroup))'"
93+
)
94+
self.configuration.target = .connectedSocket(socket)
95+
self.configuration.connectionBackoff =
96+
self.connectionBackoffIsEnabled ? self.connectionBackoff : nil
97+
self.configuration.tlsConfiguration = self.maybeTLS
98+
return ClientConnection(configuration: self.configuration)
99+
}
88100
}
89101
}
90102

Sources/GRPC/PlatformSupport.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,28 @@ public protocol ClientBootstrapProtocol {
117117
func connect(to: SocketAddress) -> EventLoopFuture<Channel>
118118
func connect(host: String, port: Int) -> EventLoopFuture<Channel>
119119
func connect(unixDomainSocketPath: String) -> EventLoopFuture<Channel>
120+
func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel>
120121

121122
func connectTimeout(_ timeout: TimeAmount) -> Self
122123
func channelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
123124
func channelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture<Void>) -> Self
124125
}
125126

127+
extension ClientBootstrapProtocol {
128+
public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel> {
129+
preconditionFailure("withConnectedSocket(_:) is not implemented")
130+
}
131+
}
132+
126133
extension ClientBootstrap: ClientBootstrapProtocol {}
127134

128135
#if canImport(Network)
129136
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
130-
extension NIOTSConnectionBootstrap: ClientBootstrapProtocol {}
137+
extension NIOTSConnectionBootstrap: ClientBootstrapProtocol {
138+
public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel> {
139+
preconditionFailure("NIOTSConnectionBootstrap does not support withConnectedSocket(_:)")
140+
}
141+
}
131142
#endif
132143

133144
/// This protocol is intended as a layer of abstraction over `ServerBootstrap` and
@@ -136,6 +147,7 @@ public protocol ServerBootstrapProtocol {
136147
func bind(to: SocketAddress) -> EventLoopFuture<Channel>
137148
func bind(host: String, port: Int) -> EventLoopFuture<Channel>
138149
func bind(unixDomainSocketPath: String) -> EventLoopFuture<Channel>
150+
func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel>
139151

140152
func serverChannelInitializer(_ initializer: @escaping (Channel) -> EventLoopFuture<Void>) -> Self
141153
func serverChannelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
@@ -144,11 +156,21 @@ public protocol ServerBootstrapProtocol {
144156
func childChannelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
145157
}
146158

159+
extension ServerBootstrapProtocol {
160+
public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel> {
161+
preconditionFailure("withBoundSocket(_:) is not implemented")
162+
}
163+
}
164+
147165
extension ServerBootstrap: ServerBootstrapProtocol {}
148166

149167
#if canImport(Network)
150168
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
151-
extension NIOTSListenerBootstrap: ServerBootstrapProtocol {}
169+
extension NIOTSListenerBootstrap: ServerBootstrapProtocol {
170+
public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture<Channel> {
171+
preconditionFailure("NIOTSListenerBootstrap does not support withConnectedSocket(_:)")
172+
}
173+
}
152174
#endif
153175

154176
// MARK: - Bootstrap / EventLoopGroup helpers

Sources/GRPC/Server.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,9 @@ extension ServerBootstrapProtocol {
462462

463463
case let .socketAddress(address):
464464
return self.bind(to: address)
465+
466+
case let .connectedSocket(socket):
467+
return self.withBoundSocket(socket)
465468
}
466469
}
467470
}

Sources/GRPC/ServerBuilder.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ extension Server {
5454
self.configuration.tlsConfiguration = self.maybeTLS
5555
return Server.start(configuration: self.configuration)
5656
}
57+
58+
public func bind(unixDomainSocketPath path: String) -> EventLoopFuture<Server> {
59+
self.configuration.target = .unixDomainSocket(path)
60+
self.configuration.tlsConfiguration = self.maybeTLS
61+
return Server.start(configuration: self.configuration)
62+
}
5763
}
5864
}
5965

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2022, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import EchoImplementation
17+
import EchoModel
18+
@testable import GRPC
19+
import NIOCore
20+
import NIOPosix
21+
import XCTest
22+
23+
class WithConnectedSockettests: GRPCTestCase {
24+
func testWithConnectedSocket() throws {
25+
let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1)
26+
defer {
27+
XCTAssertNoThrow(try group.syncShutdownGracefully())
28+
}
29+
30+
let path = "/tmp/grpc-\(getpid()).sock"
31+
// Setup a server.
32+
let server = try Server.insecure(group: group)
33+
.withServiceProviders([EchoProvider()])
34+
.withLogger(self.serverLogger)
35+
.bind(unixDomainSocketPath: path)
36+
.wait()
37+
defer {
38+
XCTAssertNoThrow(try server.close().wait())
39+
}
40+
41+
#if os(Linux)
42+
let sockStream = CInt(SOCK_STREAM.rawValue)
43+
#else
44+
let sockStream = SOCK_STREAM
45+
#endif
46+
let clientSocket = socket(AF_UNIX, sockStream, 0)
47+
48+
XCTAssert(clientSocket != -1)
49+
let addr = try SocketAddress(unixDomainSocketPath: path)
50+
addr.withSockAddr { addr, size in
51+
let ret = connect(clientSocket, addr, UInt32(size))
52+
XCTAssert(ret != -1)
53+
}
54+
let flags = fcntl(clientSocket, F_GETFL, 0)
55+
XCTAssert(flags != -1)
56+
XCTAssert(fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK) == 0)
57+
58+
let connection = ClientConnection.insecure(group: group)
59+
.withBackgroundActivityLogger(self.clientLogger)
60+
.withConnectedSocket(clientSocket)
61+
defer {
62+
XCTAssertNoThrow(try connection.close().wait())
63+
}
64+
65+
let client = Echo_EchoClient(channel: connection)
66+
let resp = try client.get(Echo_EchoRequest(text: "Hello")).response.wait()
67+
XCTAssertEqual(resp.text, "Swift echo get: Hello")
68+
}
69+
}

0 commit comments

Comments
 (0)