tcp-client 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -5
- data/lib/tcp-client/address.rb +1 -1
- data/lib/tcp-client/configuration.rb +14 -17
- data/lib/tcp-client/errors.rb +2 -2
- data/lib/tcp-client/mixin/io_with_deadline.rb +46 -16
- data/lib/tcp-client/version.rb +2 -1
- data/lib/tcp-client.rb +57 -20
- data/rakefile.rb +7 -1
- data/sample/google_ssl.rb +8 -5
- data/spec/tcp_client_spec.rb +227 -23
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e88146d7f1f966d6c9e7dce6a876c17dca7383ee88eb76a8e5caf36076a03720
|
4
|
+
data.tar.gz: 493d8cbf748789df4efd0301679082023c140f23dbc2a3d52ccfcf8d71a4a1b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 070ba42a0a185a67ae1ac4d1912220ab0aa297a6cdc4147d3afaf8593129f3b25da2ecad7a8569651c6d54dd1ba11c7705ebf8bcc1def2d525388025b45d12b3
|
7
|
+
data.tar.gz: 4c34127d89744a537d5b64c578ac4c1e2121c27e9412208f7c0abcf74999bb1e85c1c246bebfea83401c8caaf4c472daee7ff5e599989632076784881131b3ea
|
data/README.md
CHANGED
@@ -27,11 +27,14 @@ cfg = TCPClient::Configuration.create(
|
|
27
27
|
# - limit all network interactions to 1.5 seconds
|
28
28
|
# - use the Configuration cfg
|
29
29
|
# - send a simple HTTP get request
|
30
|
-
# - read
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
# - read the returned message and headers
|
31
|
+
response =
|
32
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
33
|
+
client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
34
|
+
client.readline("\r\n\r\n") #=> see response
|
35
|
+
end
|
36
|
+
|
37
|
+
puts(response)
|
35
38
|
```
|
36
39
|
|
37
40
|
For more samples see [the samples dir](https://github.com/mblumtritt/tcp-client/tree/main/sample)
|
data/lib/tcp-client/address.rb
CHANGED
@@ -41,7 +41,7 @@ class TCPClient
|
|
41
41
|
# @param addrinfo [Addrinfo] containing the addressed host and port
|
42
42
|
#
|
43
43
|
# @overload initialize(port)
|
44
|
-
#
|
44
|
+
# Addresses the port on the local machine.
|
45
45
|
#
|
46
46
|
# @example create an Address for localhost on port 80
|
47
47
|
# Address.new(80)
|
@@ -26,7 +26,7 @@ class TCPClient
|
|
26
26
|
# @example
|
27
27
|
# config = TCPClient::Configuration.create(buffered: false)
|
28
28
|
#
|
29
|
-
# @param options [
|
29
|
+
# @param options [{Symbol => Object}] see {#initialize} for details
|
30
30
|
#
|
31
31
|
# @return [Configuration] the initialized configuration
|
32
32
|
#
|
@@ -37,13 +37,13 @@ class TCPClient
|
|
37
37
|
end
|
38
38
|
|
39
39
|
#
|
40
|
-
#
|
40
|
+
# Initializes the instance with given options.
|
41
41
|
#
|
42
|
-
# @param options [
|
42
|
+
# @param options [{Symbol => Object}]
|
43
43
|
# @option options [Boolean] :buffered, see {#buffered}
|
44
44
|
# @option options [Boolean] :keep_alive, see {#keep_alive}
|
45
45
|
# @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
|
46
|
-
# @option options [
|
46
|
+
# @option options [{Symbol => Object}] :ssl_params, see {#ssl_params}
|
47
47
|
# @option options [Numeric] :connect_timeout, see {#connect_timeout}
|
48
48
|
# @option options [Class<Exception>] :connect_timeout_error, see
|
49
49
|
# {#connect_timeout_error}
|
@@ -56,7 +56,6 @@ class TCPClient
|
|
56
56
|
# @option options [Boolean] :normalize_network_errors, see
|
57
57
|
# {#normalize_network_errors}
|
58
58
|
#
|
59
|
-
#
|
60
59
|
def initialize(options = {})
|
61
60
|
@buffered = @keep_alive = @reverse_lookup = true
|
62
61
|
self.timeout = @ssl_params = nil
|
@@ -72,8 +71,8 @@ class TCPClient
|
|
72
71
|
#
|
73
72
|
# Enables/disables use of Socket-level buffering
|
74
73
|
#
|
75
|
-
# @return [Boolean]
|
76
|
-
# (default) or not
|
74
|
+
# @return [Boolean] whether the connection is allowed to use internal
|
75
|
+
# buffers (default) or not
|
77
76
|
#
|
78
77
|
attr_reader :buffered
|
79
78
|
|
@@ -84,7 +83,7 @@ class TCPClient
|
|
84
83
|
#
|
85
84
|
# Enables/disables use of Socket-level keep alive handling.
|
86
85
|
#
|
87
|
-
# @return [Boolean]
|
86
|
+
# @return [Boolean] whether the connection is allowed to use keep alive
|
88
87
|
# signals (default) or not
|
89
88
|
#
|
90
89
|
attr_reader :keep_alive
|
@@ -96,7 +95,7 @@ class TCPClient
|
|
96
95
|
#
|
97
96
|
# Enables/disables address lookup.
|
98
97
|
#
|
99
|
-
# @return [Boolean]
|
98
|
+
# @return [Boolean] whether the connection is allowed to lookup the address
|
100
99
|
# (default) or not
|
101
100
|
#
|
102
101
|
attr_reader :reverse_lookup
|
@@ -107,7 +106,7 @@ class TCPClient
|
|
107
106
|
|
108
107
|
#
|
109
108
|
# @!parse attr_reader :ssl?
|
110
|
-
# @return [Boolean]
|
109
|
+
# @return [Boolean] whether SSL is configured, see {#ssl_params}
|
111
110
|
#
|
112
111
|
def ssl?
|
113
112
|
@ssl_params ? true : false
|
@@ -117,7 +116,7 @@ class TCPClient
|
|
117
116
|
# Parameters used to initialize a SSL context. SSL/TLS will only be used if
|
118
117
|
# this attribute is not `nil`.
|
119
118
|
#
|
120
|
-
# @return [
|
119
|
+
# @return [{Symbol => Object}] SSL parameters for the SSL context
|
121
120
|
# @return [nil] if no SSL should be used (default)
|
122
121
|
#
|
123
122
|
attr_reader :ssl_params
|
@@ -226,7 +225,7 @@ class TCPClient
|
|
226
225
|
# @attribute [w] timeout
|
227
226
|
# Shorthand to set maximum time in seconds for all timeout monitoring.
|
228
227
|
#
|
229
|
-
# @return [Numeric] maximum time in seconds for any
|
228
|
+
# @return [Numeric] maximum time in seconds for any action
|
230
229
|
# @return [nil] if all timeout monitoring should be disabled (default)
|
231
230
|
#
|
232
231
|
# @see #connect_timeout
|
@@ -239,7 +238,7 @@ class TCPClient
|
|
239
238
|
|
240
239
|
#
|
241
240
|
# @attribute [w] timeout_error
|
242
|
-
# Shorthand to set the exception class
|
241
|
+
# Shorthand to set the exception class which will by raised by any reached
|
243
242
|
# timeout.
|
244
243
|
#
|
245
244
|
# @return [Class<Exception>] exception class raised
|
@@ -266,7 +265,7 @@ class TCPClient
|
|
266
265
|
# manner. If this option is set to true all these error cases are raised as
|
267
266
|
# {NetworkError} and can be easily captured.
|
268
267
|
#
|
269
|
-
# @return [Boolean]
|
268
|
+
# @return [Boolean] whether all network exceptions should be raised as
|
270
269
|
# {NetworkError}, or not (default)
|
271
270
|
#
|
272
271
|
attr_reader :normalize_network_errors
|
@@ -276,9 +275,7 @@ class TCPClient
|
|
276
275
|
end
|
277
276
|
|
278
277
|
#
|
279
|
-
#
|
280
|
-
#
|
281
|
-
# @return [Hash<Symbol, Object>]
|
278
|
+
# @return [{Symbol => Object}] Hash containing all attributes
|
282
279
|
#
|
283
280
|
# @see #initialize
|
284
281
|
#
|
data/lib/tcp-client/errors.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
class TCPClient
|
4
4
|
#
|
5
|
-
# Raised when a SSL connection should be
|
5
|
+
# Raised when a SSL connection should be established but the OpenSSL gem is
|
6
6
|
# not available.
|
7
7
|
#
|
8
8
|
class NoOpenSSLError < RuntimeError
|
@@ -38,7 +38,7 @@ class TCPClient
|
|
38
38
|
#
|
39
39
|
class UnknownAttributeError < ArgumentError
|
40
40
|
#
|
41
|
-
# @param attribute [Object] the undefined
|
41
|
+
# @param attribute [Object] the undefined attribute
|
42
42
|
#
|
43
43
|
def initialize(attribute)
|
44
44
|
super("unknown attribute - #{attribute}")
|
@@ -13,25 +13,30 @@ module IOWithDeadlineMixin # :nodoc:
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
def read_with_deadline(
|
16
|
+
def read_with_deadline(nbytes, deadline, exception)
|
17
17
|
raise(exception) unless deadline.remaining_time
|
18
|
-
if
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
return fetch_avail(deadline, exception) if nbytes.nil?
|
19
|
+
return ''.b if nbytes.zero?
|
20
|
+
@buf ||= ''.b
|
21
|
+
while @buf.bytesize < nbytes
|
22
|
+
read = fetch_next(deadline, exception) and next @buf << read
|
23
|
+
close
|
24
|
+
break
|
24
25
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
fetch_buffer_slice(nbytes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def readto_with_deadline(sep, deadline, exception)
|
30
|
+
raise(exception) unless deadline.remaining_time
|
31
|
+
@buf ||= ''.b
|
32
|
+
while (index = @buf.index(sep)).nil?
|
33
|
+
read = fetch_next(deadline, exception) and next @buf << read
|
32
34
|
close
|
33
35
|
break
|
34
36
|
end
|
37
|
+
index = @buf.index(sep) and return fetch_buffer_slice(index + sep.bytesize)
|
38
|
+
result = @buf
|
39
|
+
@buf = nil
|
35
40
|
result
|
36
41
|
end
|
37
42
|
|
@@ -44,12 +49,37 @@ module IOWithDeadlineMixin # :nodoc:
|
|
44
49
|
with_deadline(deadline, exception) do
|
45
50
|
write_nonblock(data, exception: false)
|
46
51
|
end
|
47
|
-
result += written
|
48
|
-
return result if result >= size
|
52
|
+
(result += written) >= size and return result
|
49
53
|
data = data.byteslice(written, data.bytesize - written)
|
50
54
|
end
|
51
55
|
end
|
52
56
|
|
57
|
+
private
|
58
|
+
|
59
|
+
def fetch_avail(deadline, exception)
|
60
|
+
if @buf.nil?
|
61
|
+
result = fetch_next(deadline, exception) and return result
|
62
|
+
close
|
63
|
+
return ''.b
|
64
|
+
end
|
65
|
+
result = @buf
|
66
|
+
@buf = nil
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def fetch_buffer_slice(size)
|
71
|
+
result = @buf.byteslice(0, size)
|
72
|
+
rest = @buf.bytesize - result.bytesize
|
73
|
+
@buf = rest.zero? ? nil : @buf.byteslice(size, rest)
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def fetch_next(deadline, exception)
|
78
|
+
with_deadline(deadline, exception) do
|
79
|
+
read_nonblock(65_536, exception: false)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
53
83
|
module ViaWaitMethod
|
54
84
|
private def with_deadline(deadline, exception)
|
55
85
|
loop do
|
data/lib/tcp-client/version.rb
CHANGED
data/lib/tcp-client.rb
CHANGED
@@ -16,12 +16,15 @@ require_relative 'tcp-client/version'
|
|
16
16
|
# terminate before given time limits - or raise an exception.
|
17
17
|
#
|
18
18
|
# @example request to Google.com and limit network interactions to 1.5 seconds
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# client.read(12)
|
22
|
-
# end
|
23
|
-
# # => "HTTP/1.1 200"
|
19
|
+
# # create a configuration to use at least TLS 1.2
|
20
|
+
# cfg = TCPClient::Configuration.create(ssl_params: {min_version: :TLS1_2})
|
24
21
|
#
|
22
|
+
# response =
|
23
|
+
# TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
24
|
+
# client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
25
|
+
# client.readline("\r\n\r\n") #=> see response
|
26
|
+
# end
|
27
|
+
# # response contains the returned message and header
|
25
28
|
#
|
26
29
|
class TCPClient
|
27
30
|
#
|
@@ -40,10 +43,10 @@ class TCPClient
|
|
40
43
|
#
|
41
44
|
# If an optional block is given, then the block's result is returned and the
|
42
45
|
# connection will be closed when the block execution ends.
|
43
|
-
# This can be used to create an ad-hoc connection which is
|
46
|
+
# This can be used to create an ad-hoc connection which is guaranteed to be
|
44
47
|
# closed.
|
45
48
|
#
|
46
|
-
# If no block is
|
49
|
+
# If no block is given the connected client instance is returned.
|
47
50
|
# This can be used as a shorthand to create & connect a client.
|
48
51
|
#
|
49
52
|
# @param address [Address, String, Addrinfo, Integer] the target address see
|
@@ -55,7 +58,7 @@ class TCPClient
|
|
55
58
|
#
|
56
59
|
def self.open(address, configuration = nil)
|
57
60
|
client = new
|
58
|
-
client.connect(
|
61
|
+
client.connect(address, configuration)
|
59
62
|
block_given? ? yield(client) : client
|
60
63
|
ensure
|
61
64
|
client.close if block_given?
|
@@ -67,9 +70,9 @@ class TCPClient
|
|
67
70
|
# the given time.
|
68
71
|
#
|
69
72
|
# It ensures to close the connection when the block execution ends and returns
|
70
|
-
# the block
|
73
|
+
# the block's result.
|
71
74
|
#
|
72
|
-
# This can be used to create an ad-hoc connection which is
|
75
|
+
# This can be used to create an ad-hoc connection which is guaranteed to be
|
73
76
|
# closed and which {#read}/{#write} call sequence should not last longer than
|
74
77
|
# the `timeout` seconds.
|
75
78
|
#
|
@@ -91,7 +94,6 @@ class TCPClient
|
|
91
94
|
def self.with_deadline(timeout, address, configuration = nil)
|
92
95
|
client = nil
|
93
96
|
raise(NoBlockGivenError) unless block_given?
|
94
|
-
address = Address.new(address)
|
95
97
|
client = new
|
96
98
|
client.with_deadline(timeout) do
|
97
99
|
yield(client.connect(address, configuration))
|
@@ -112,7 +114,7 @@ class TCPClient
|
|
112
114
|
|
113
115
|
#
|
114
116
|
# @!parse attr_reader :closed?
|
115
|
-
# @return [Boolean]
|
117
|
+
# @return [Boolean] whether the connection is closed
|
116
118
|
#
|
117
119
|
def closed?
|
118
120
|
@socket.nil? || @socket.closed?
|
@@ -121,7 +123,7 @@ class TCPClient
|
|
121
123
|
#
|
122
124
|
# Close the current connection if connected.
|
123
125
|
#
|
124
|
-
# @return [
|
126
|
+
# @return [TCPClient] itself
|
125
127
|
#
|
126
128
|
def close
|
127
129
|
@socket&.close
|
@@ -133,7 +135,7 @@ class TCPClient
|
|
133
135
|
end
|
134
136
|
|
135
137
|
#
|
136
|
-
# Establishes a new connection to a given `address`.
|
138
|
+
# Establishes a new connection to a server on given `address`.
|
137
139
|
#
|
138
140
|
# It accepts a connection-specific `configuration` or uses the
|
139
141
|
# {.default_configuration}.
|
@@ -141,7 +143,7 @@ class TCPClient
|
|
141
143
|
# The optional `timeout` and `exception` parameters allow to override the
|
142
144
|
# `connect_timeout` and `connect_timeout_error` values.
|
143
145
|
#
|
144
|
-
# @param address [Address, String, Addrinfo, Integer] the target address see
|
146
|
+
# @param address [Address, String, Addrinfo, Integer] the target address, see
|
145
147
|
# {Address#initialize} for valid formats
|
146
148
|
# @param configuration [Configuration] the {Configuration} to be used for
|
147
149
|
# this instance
|
@@ -149,7 +151,7 @@ class TCPClient
|
|
149
151
|
# @param exception [Class<Exception>] exception class to be used when the
|
150
152
|
# connect timeout reached
|
151
153
|
#
|
152
|
-
# @return [
|
154
|
+
# @return [TCPClient] itself
|
153
155
|
#
|
154
156
|
# @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
|
155
157
|
#
|
@@ -157,17 +159,17 @@ class TCPClient
|
|
157
159
|
#
|
158
160
|
def connect(address, configuration = nil, timeout: nil, exception: nil)
|
159
161
|
close if @socket
|
160
|
-
@address = Address.new(address)
|
161
162
|
@configuration = (configuration || Configuration.default).dup
|
162
163
|
raise(NoOpenSSLError) if @configuration.ssl? && !defined?(SSLSocket)
|
164
|
+
@address = stem_errors { Address.new(address) }
|
163
165
|
@socket = create_socket(timeout, exception)
|
164
166
|
self
|
165
167
|
end
|
166
168
|
|
167
169
|
#
|
168
|
-
# Flushes all internal buffers (write all
|
170
|
+
# Flushes all internal buffers (write all buffered data).
|
169
171
|
#
|
170
|
-
# @return [
|
172
|
+
# @return [TCPClient] itself
|
171
173
|
#
|
172
174
|
def flush
|
173
175
|
stem_errors { @socket&.flush }
|
@@ -201,6 +203,39 @@ class TCPClient
|
|
201
203
|
end
|
202
204
|
end
|
203
205
|
|
206
|
+
#
|
207
|
+
# Reads the next line from server.
|
208
|
+
#
|
209
|
+
# The standard record separator is used as `separator`.
|
210
|
+
#
|
211
|
+
# The optional `timeout` and `exception` parameters allow to override the
|
212
|
+
# `read_timeout` and `read_timeout_error` values of the used {#configuration}.
|
213
|
+
#
|
214
|
+
# @param separator [String] the line separator to be used
|
215
|
+
# @param timeout [Numeric] maximum time in seconds to read
|
216
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
217
|
+
# read timeout reached
|
218
|
+
#
|
219
|
+
# @return [String] the read line
|
220
|
+
#
|
221
|
+
# @raise [NotConnectedError] if {#connect} was not called before
|
222
|
+
#
|
223
|
+
# @see NetworkError
|
224
|
+
#
|
225
|
+
def readline(separator = $/, chomp: false, timeout: nil, exception: nil)
|
226
|
+
raise(NotConnectedError) if closed?
|
227
|
+
deadline = create_deadline(timeout, configuration.read_timeout)
|
228
|
+
unless deadline.valid?
|
229
|
+
return stem_errors { @socket.readline(separator, chomp: chomp) }
|
230
|
+
end
|
231
|
+
exception ||= configuration.read_timeout_error
|
232
|
+
line =
|
233
|
+
stem_errors(exception) do
|
234
|
+
@socket.readto_with_deadline(separator, deadline, exception)
|
235
|
+
end
|
236
|
+
chomp ? line.chomp : line
|
237
|
+
end
|
238
|
+
|
204
239
|
#
|
205
240
|
# @return [String] the currently used address as text.
|
206
241
|
#
|
@@ -229,7 +264,7 @@ class TCPClient
|
|
229
264
|
#
|
230
265
|
# @yieldparam client [TCPClient] self
|
231
266
|
#
|
232
|
-
# @return [Object] the block
|
267
|
+
# @return [Object] the block's result
|
233
268
|
#
|
234
269
|
# @raise [NoBlockGivenError] if the block is missing
|
235
270
|
#
|
@@ -259,6 +294,8 @@ class TCPClient
|
|
259
294
|
#
|
260
295
|
# @raise [NotConnectedError] if {#connect} was not called before
|
261
296
|
#
|
297
|
+
# @see NetworkError
|
298
|
+
#
|
262
299
|
def write(*messages, timeout: nil, exception: nil)
|
263
300
|
raise(NotConnectedError) if closed?
|
264
301
|
deadline = create_deadline(timeout, configuration.write_timeout)
|
data/rakefile.rb
CHANGED
@@ -6,7 +6,13 @@ require 'rspec/core/rake_task'
|
|
6
6
|
require 'yard'
|
7
7
|
|
8
8
|
$stdout.sync = $stderr.sync = true
|
9
|
-
|
9
|
+
|
10
|
+
CLEAN << 'prj' << 'doc'
|
11
|
+
|
12
|
+
CLOBBER << '.yardoc'
|
13
|
+
|
10
14
|
task(:default) { exec('rake --tasks') }
|
15
|
+
|
11
16
|
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
17
|
+
|
12
18
|
YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
|
data/sample/google_ssl.rb
CHANGED
@@ -18,8 +18,11 @@ cfg =
|
|
18
18
|
# - limit all network interactions to 1.5 seconds
|
19
19
|
# - use the Configuration cfg
|
20
20
|
# - send a simple HTTP get request
|
21
|
-
# - read
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
# - read the returned message and headers
|
22
|
+
response =
|
23
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
24
|
+
client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
25
|
+
client.readline("\r\n\r\n") #=> see response
|
26
|
+
end
|
27
|
+
|
28
|
+
puts(response)
|
data/spec/tcp_client_spec.rb
CHANGED
@@ -26,7 +26,7 @@ RSpec.describe TCPClient do
|
|
26
26
|
subject(:client) { TCPClient.new }
|
27
27
|
|
28
28
|
it 'is closed' do
|
29
|
-
expect(client
|
29
|
+
expect(client).to be_closed
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'has no address' do
|
@@ -64,7 +64,7 @@ RSpec.describe TCPClient do
|
|
64
64
|
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
65
65
|
|
66
66
|
it 'is not closed' do
|
67
|
-
expect(client
|
67
|
+
expect(client).not_to be_closed
|
68
68
|
end
|
69
69
|
|
70
70
|
it 'has an address' do
|
@@ -115,7 +115,7 @@ RSpec.describe TCPClient do
|
|
115
115
|
end
|
116
116
|
|
117
117
|
it 'is closed' do
|
118
|
-
expect(client
|
118
|
+
expect(client).to be_closed
|
119
119
|
end
|
120
120
|
|
121
121
|
it 'has an address' do
|
@@ -157,6 +157,8 @@ RSpec.describe TCPClient do
|
|
157
157
|
|
158
158
|
context 'when not using SSL' do
|
159
159
|
describe '#connect' do
|
160
|
+
subject(:client) { TCPClient.new }
|
161
|
+
|
160
162
|
it 'configures the socket' do
|
161
163
|
expect_any_instance_of(::Socket).to receive(:sync=).once.with(true)
|
162
164
|
expect_any_instance_of(::Socket).to receive(:setsockopt)
|
@@ -169,7 +171,7 @@ RSpec.describe TCPClient do
|
|
169
171
|
.once
|
170
172
|
.with(false)
|
171
173
|
expect_any_instance_of(::Socket).to receive(:connect)
|
172
|
-
|
174
|
+
client.connect('localhost:1234', configuration)
|
173
175
|
end
|
174
176
|
|
175
177
|
context 'when a timeout is specified' do
|
@@ -177,7 +179,26 @@ RSpec.describe TCPClient do
|
|
177
179
|
expect_any_instance_of(::Socket).to receive(:connect_nonblock)
|
178
180
|
.once
|
179
181
|
.with(kind_of(String), exception: false)
|
180
|
-
|
182
|
+
client.connect('localhost:1234', configuration, timeout: 10)
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'is returns itself' do
|
186
|
+
allow_any_instance_of(::Socket).to receive(:connect_nonblock).with(
|
187
|
+
kind_of(String),
|
188
|
+
exception: false
|
189
|
+
)
|
190
|
+
result = client.connect('localhost:1234', configuration, timeout: 10)
|
191
|
+
|
192
|
+
expect(client).to be client
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'is not closed' do
|
196
|
+
allow_any_instance_of(::Socket).to receive(:connect_nonblock).with(
|
197
|
+
kind_of(String),
|
198
|
+
exception: false
|
199
|
+
)
|
200
|
+
client.connect('localhost:1234', configuration, timeout: 10)
|
201
|
+
expect(client).not_to be_closed
|
181
202
|
end
|
182
203
|
|
183
204
|
context 'when the connection can not be established in time' do
|
@@ -188,25 +209,29 @@ RSpec.describe TCPClient do
|
|
188
209
|
|
189
210
|
it 'raises an exception' do
|
190
211
|
expect do
|
191
|
-
|
192
|
-
'localhost:1234',
|
193
|
-
configuration,
|
194
|
-
timeout: 0.25
|
195
|
-
)
|
212
|
+
client.connect('localhost:1234', configuration, timeout: 0.1)
|
196
213
|
end.to raise_error(TCPClient::ConnectTimeoutError)
|
197
214
|
end
|
198
215
|
|
199
216
|
it 'allows to raise a custom exception' do
|
200
217
|
exception = Class.new(StandardError)
|
201
218
|
expect do
|
202
|
-
|
219
|
+
client.connect(
|
203
220
|
'localhost:1234',
|
204
221
|
configuration,
|
205
|
-
timeout: 0.
|
222
|
+
timeout: 0.1,
|
206
223
|
exception: exception
|
207
224
|
)
|
208
225
|
end.to raise_error(exception)
|
209
226
|
end
|
227
|
+
|
228
|
+
it 'is still closed' do
|
229
|
+
begin
|
230
|
+
client.connect('localhost:1234', configuration, timeout: 0.1)
|
231
|
+
rescue TCPClient::ConnectTimeoutError
|
232
|
+
end
|
233
|
+
expect(client).to be_closed
|
234
|
+
end
|
210
235
|
end
|
211
236
|
end
|
212
237
|
|
@@ -226,7 +251,7 @@ RSpec.describe TCPClient do
|
|
226
251
|
end
|
227
252
|
|
228
253
|
SOCKET_ERRORS.each do |error_class|
|
229
|
-
it "raises
|
254
|
+
it "raises TCPClient::NetworkError when a #{error_class} appeared" do
|
230
255
|
allow_any_instance_of(::Socket).to receive(:connect) {
|
231
256
|
raise error_class
|
232
257
|
}
|
@@ -270,18 +295,61 @@ RSpec.describe TCPClient do
|
|
270
295
|
expect(client.read(timeout: 10)).to be data
|
271
296
|
end
|
272
297
|
|
298
|
+
context 'when socket closed before any data can be read' do
|
299
|
+
it 'returns empty buffer' do
|
300
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
301
|
+
.and_return(nil)
|
302
|
+
expect(client.read(timeout: 10)).to be_empty
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'is closed' do
|
306
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
307
|
+
.and_return(nil)
|
308
|
+
|
309
|
+
client.read(timeout: 10)
|
310
|
+
expect(client).to be_closed
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
273
314
|
context 'when data can not be fetched in a single chunk' do
|
274
315
|
it 'reads chunk by chunk' do
|
275
316
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
276
317
|
.once
|
277
|
-
.with(
|
318
|
+
.with(instance_of(Integer), exception: false)
|
278
319
|
.and_return(data)
|
279
320
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
280
321
|
.once
|
281
|
-
.with(
|
322
|
+
.with(instance_of(Integer), exception: false)
|
282
323
|
.and_return(data)
|
283
324
|
expect(client.read(data_size * 2, timeout: 10)).to eq data * 2
|
284
325
|
end
|
326
|
+
|
327
|
+
context 'when socket closed before enough data is avail' do
|
328
|
+
it 'returns available data only' do
|
329
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
330
|
+
.once
|
331
|
+
.with(instance_of(Integer), exception: false)
|
332
|
+
.and_return(data)
|
333
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
334
|
+
.once
|
335
|
+
.with(instance_of(Integer), exception: false)
|
336
|
+
.and_return(nil)
|
337
|
+
expect(client.read(data_size * 2, timeout: 10)).to eq data
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'is closed' do
|
341
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
342
|
+
.once
|
343
|
+
.with(instance_of(Integer), exception: false)
|
344
|
+
.and_return(data)
|
345
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
346
|
+
.once
|
347
|
+
.with(instance_of(Integer), exception: false)
|
348
|
+
.and_return(nil)
|
349
|
+
client.read(data_size * 2, timeout: 10)
|
350
|
+
expect(client).to be_closed
|
351
|
+
end
|
352
|
+
end
|
285
353
|
end
|
286
354
|
|
287
355
|
context 'when the data can not be read in time' do
|
@@ -329,6 +397,146 @@ RSpec.describe TCPClient do
|
|
329
397
|
end
|
330
398
|
end
|
331
399
|
|
400
|
+
describe '#readline' do
|
401
|
+
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
402
|
+
|
403
|
+
it 'reads from socket' do
|
404
|
+
expect_any_instance_of(::Socket).to receive(:readline)
|
405
|
+
.once
|
406
|
+
.with($/, chomp: false)
|
407
|
+
.and_return("Hello World\n")
|
408
|
+
expect(client.readline).to eq "Hello World\n"
|
409
|
+
end
|
410
|
+
|
411
|
+
context 'when a separator is specified' do
|
412
|
+
it 'forwards the separator' do
|
413
|
+
expect_any_instance_of(::Socket).to receive(:readline)
|
414
|
+
.once
|
415
|
+
.with('/', chomp: false)
|
416
|
+
.and_return('Hello/')
|
417
|
+
expect(client.readline('/')).to eq 'Hello/'
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
context 'when chomp is true' do
|
422
|
+
it 'forwards the flag' do
|
423
|
+
expect_any_instance_of(::Socket).to receive(:readline)
|
424
|
+
.once
|
425
|
+
.with($/, chomp: true)
|
426
|
+
.and_return('Hello World')
|
427
|
+
expect(client.readline(chomp: true)).to eq 'Hello World'
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
context 'when a timeout is specified' do
|
432
|
+
it 'checks the time' do
|
433
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
434
|
+
.and_return("Hello World\nHello World\n")
|
435
|
+
expect(client.readline(timeout: 10)).to eq "Hello World\n"
|
436
|
+
end
|
437
|
+
|
438
|
+
it 'optional chomps the line' do
|
439
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
440
|
+
.and_return("Hello World\nHello World\n")
|
441
|
+
expect(client.readline(chomp: true, timeout: 10)).to eq 'Hello World'
|
442
|
+
end
|
443
|
+
|
444
|
+
it 'uses the given separator' do
|
445
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
446
|
+
.and_return("Hello/World\n")
|
447
|
+
expect(client.readline('/', timeout: 10)).to eq 'Hello/'
|
448
|
+
end
|
449
|
+
|
450
|
+
context 'when data can not be fetched in a single chunk' do
|
451
|
+
it 'reads chunk by chunk' do
|
452
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
453
|
+
.once
|
454
|
+
.with(instance_of(Integer), exception: false)
|
455
|
+
.and_return('Hello ')
|
456
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
457
|
+
.once
|
458
|
+
.with(instance_of(Integer), exception: false)
|
459
|
+
.and_return('World')
|
460
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
461
|
+
.once
|
462
|
+
.with(instance_of(Integer), exception: false)
|
463
|
+
.and_return("\nAnd so...")
|
464
|
+
expect(client.readline(timeout: 10)).to eq "Hello World\n"
|
465
|
+
end
|
466
|
+
|
467
|
+
context 'when socket closed before enough data is avail' do
|
468
|
+
it 'returns available data only' do
|
469
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
470
|
+
.once
|
471
|
+
.with(instance_of(Integer), exception: false)
|
472
|
+
.and_return('Hello ')
|
473
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
474
|
+
.once
|
475
|
+
.with(instance_of(Integer), exception: false)
|
476
|
+
.and_return(nil)
|
477
|
+
expect(client.readline(timeout: 10)).to eq "Hello "
|
478
|
+
end
|
479
|
+
|
480
|
+
it 'is closed' do
|
481
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
482
|
+
.once
|
483
|
+
.with(instance_of(Integer), exception: false)
|
484
|
+
.and_return('Hello ')
|
485
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
486
|
+
.once
|
487
|
+
.with(instance_of(Integer), exception: false)
|
488
|
+
.and_return(nil)
|
489
|
+
client.readline(timeout: 10)
|
490
|
+
expect(client).to be_closed
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
context 'when the data can not be read in time' do
|
496
|
+
before do
|
497
|
+
allow_any_instance_of(::Socket).to receive(:read_nonblock)
|
498
|
+
.and_return(:wait_readable)
|
499
|
+
end
|
500
|
+
it 'raises an exception' do
|
501
|
+
expect { client.readline(timeout: 0.25) }.to raise_error(
|
502
|
+
TCPClient::ReadTimeoutError
|
503
|
+
)
|
504
|
+
end
|
505
|
+
|
506
|
+
it 'allows to raise a custom exception' do
|
507
|
+
exception = Class.new(StandardError)
|
508
|
+
expect do
|
509
|
+
client.read(timeout: 0.25, exception: exception)
|
510
|
+
end.to raise_error(exception)
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
context 'when a SocketError appears' do
|
516
|
+
it 'does not handle it' do
|
517
|
+
allow_any_instance_of(::Socket).to receive(:read) {
|
518
|
+
raise SocketError
|
519
|
+
}
|
520
|
+
expect { client.read(10) }.to raise_error(SocketError)
|
521
|
+
end
|
522
|
+
|
523
|
+
context 'when normalize_network_errors is configured' do
|
524
|
+
let(:configuration) do
|
525
|
+
TCPClient::Configuration.create(normalize_network_errors: true)
|
526
|
+
end
|
527
|
+
|
528
|
+
SOCKET_ERRORS.each do |error_class|
|
529
|
+
it "raises a TCPClient::NetworkError when a #{error_class} appeared" do
|
530
|
+
allow_any_instance_of(::Socket).to receive(:read) {
|
531
|
+
raise error_class
|
532
|
+
}
|
533
|
+
expect { client.read(12) }.to raise_error(TCPClient::NetworkError)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
332
540
|
describe '#write' do
|
333
541
|
let(:data) { 'some bytes' }
|
334
542
|
let(:data_size) { data.bytesize }
|
@@ -463,20 +671,16 @@ RSpec.describe TCPClient do
|
|
463
671
|
.with(kind_of(String), exception: false)
|
464
672
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
465
673
|
.once
|
466
|
-
.with(
|
467
|
-
.and_return('
|
674
|
+
.with(instance_of(Integer), exception: false)
|
675
|
+
.and_return('123456789012abcdefgAB')
|
468
676
|
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
469
677
|
.once
|
470
678
|
.with('123456', exception: false)
|
471
679
|
.and_return(6)
|
472
680
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
473
681
|
.once
|
474
|
-
.with(
|
475
|
-
.and_return('
|
476
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
477
|
-
.once
|
478
|
-
.with(7, exception: false)
|
479
|
-
.and_return('ABCDEFG')
|
682
|
+
.with(instance_of(Integer), exception: false)
|
683
|
+
.and_return('CDEFG')
|
480
684
|
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
481
685
|
.once
|
482
686
|
.with('abc', exception: false)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -129,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
131
|
requirements: []
|
132
|
-
rubygems_version: 3.
|
132
|
+
rubygems_version: 3.3.3
|
133
133
|
signing_key:
|
134
134
|
specification_version: 4
|
135
135
|
summary: A TCP client implementation with working timeout support.
|