aboutsummaryrefslogtreecommitdiff
path: root/Sources/swiftGopherClient/gopherClient.swift
blob: 389daf945149bd0fcbfce3bede633f7f3f3444f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//
//  gopherClient.swift
//
//
//  Created by Navan Chauhan on 12/12/23.
//

import Foundation
import GopherHelpers
import NIO
import NIOTransportServices

/// `GopherClient` is a class for handling network connections and requests to Gopher servers.
///
/// It utilizes `NIOTSEventLoopGroup` on iOS/macOS (Not sure why you would run this on watchOS/tvOS but it supports that as well) for network operations, falling back to `MultiThreadedEventLoopGroup` otherwise.
public class GopherClient {
    private let group: EventLoopGroup

    /// Initializes a new instance of `GopherClient`.
    ///
    /// It automatically chooses the appropriate `EventLoopGroup` based on the running platform.
    public init() {
        #if os(Linux)
            self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
        #else
            if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) {
                self.group = NIOTSEventLoopGroup()
            } else {
                self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
            }
        #endif
    }

    deinit {
        try? group.syncShutdownGracefully()
    }

    /// Sends a request to a Gopher server.
    ///
    /// - Parameters:
    ///   - host: The host address of the Gopher server.
    ///   - port: The port of the Gopher server. Defaults to 70.
    ///   - message: The message to be sent to the server.
    ///   - completion: A closure that handles the result of the request.
    ///
    /// The method asynchronously establishes a connection, sends the request, and calls the completion handler with the result.
    public func sendRequest(
        to host: String, port: Int = 70, message: String,
        completion: @escaping (Result<[gopherItem], Error>) -> Void
    ) {
        #if os(Linux)
            let bootstrap = ClientBootstrap(group: group)
                .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                .channelInitializer { channel in
                    channel.pipeline.addHandler(
                        GopherRequestResponseHandler(message: message, completion: completion))
                }
            bootstrap.connect(host: host, port: port).whenComplete { result in
                switch result {
                case .success(let channel):
                    channel.closeFuture.whenComplete { _ in
                        print("Connection closed")
                    }
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        #else

            if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) {
                let bootstrap = NIOTSConnectionBootstrap(group: group)
                    .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                    .channelInitializer { channel in
                        channel.pipeline.addHandler(
                            GopherRequestResponseHandler(message: message, completion: completion))
                    }
                bootstrap.connect(host: host, port: port).whenComplete { result in
                    switch result {
                    case .success(let channel):
                        channel.closeFuture.whenComplete { _ in
                            print("Connection closed")
                        }
                    case .failure(let error):
                        completion(.failure(error))
                    }
                }
            } else {
                let bootstrap = ClientBootstrap(group: group)
                    .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                    .channelInitializer { channel in
                        channel.pipeline.addHandler(
                            GopherRequestResponseHandler(message: message, completion: completion))
                    }
                bootstrap.connect(host: host, port: port).whenComplete { result in
                    switch result {
                    case .success(let channel):
                        channel.closeFuture.whenComplete { _ in
                            print("Connection closed")
                        }
                    case .failure(let error):
                        completion(.failure(error))
                    }
                }
            }
        #endif

    }

    @available(iOS 13.0, *)
    @available(macOS 10.15, *)
    public func sendRequest(to host: String, port: Int = 70, message: String) async throws
        -> [gopherItem]
    {
        return try await withCheckedThrowingContinuation { continuation in
            let bootstrap = self.createBootstrap(message: message) { result in
                continuation.resume(with: result)
            }

            bootstrap.connect(host: host, port: port).whenComplete { result in
                switch result {
                case .success(let channel):
                    channel.closeFuture.whenComplete { _ in
                        print("Connection Closed")
                    }
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }

    private func createBootstrap(
        message: String,
        completion: @escaping (Result<[gopherItem], Error>) -> Void
    ) -> NIOClientTCPBootstrapProtocol {
        let handler = GopherRequestResponseHandler(message: message, completion: completion)

        #if os(Linux)
            return ClientBootstrap(group: eventLoopGroup)
                .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                .channelInitializer { channel in
                    channel.pipeline.addHandler(handler)
                }
        #else
            if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) {
                return NIOTSConnectionBootstrap(group: group)
                    .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                    .channelInitializer { channel in
                        channel.pipeline.addHandler(handler)
                    }
            } else {
                return ClientBootstrap(group: group)
                    .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                    .channelInitializer { channel in
                        channel.pipeline.addHandler(handler)
                    }
            }
        #endif
    }

    /// Shuts down the event loop group, releasing any resources.
    ///
    /// This method is called during deinitialization to ensure clean shutdown of network resources.
    private func shutdownEventLoopGroup() {
        do {
            try group.syncShutdownGracefully()
        } catch {
            print("Error shutting down event loop group: \(error)")
        }
    }
}