tcp-client 0.1.5 → 0.3.2
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/lib/tcp-client.rb +42 -49
- data/lib/tcp-client/address.rb +5 -3
- data/lib/tcp-client/configuration.rb +57 -14
- data/lib/tcp-client/default_configuration.rb +2 -4
- data/lib/tcp-client/errors.rb +72 -0
- data/lib/tcp-client/mixin/io_with_deadline.rb +83 -0
- data/lib/tcp-client/ssl_socket.rb +10 -8
- data/lib/tcp-client/tcp_socket.rb +4 -3
- data/lib/tcp-client/version.rb +1 -1
- data/rakefile.rb +6 -6
- data/test/tcp-client/address_test.rb +1 -1
- data/test/tcp-client/configuration_test.rb +44 -1
- data/test/tcp_client_test.rb +17 -13
- data/test/test_helper.rb +2 -4
- metadata +5 -4
- data/lib/tcp-client/mixin/io_timeout.rb +0 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b265053761a87007b7de21c3a7a369b778f013ae726bb26220a4fd9aeeaf71c
|
4
|
+
data.tar.gz: 5a71b5dc86ed4ab9ef481e65c24aef0481c5e9d2c868f3f9975b801936d99691
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d16d436959c3e1c29962a5c474de8db3b4638e2d99aaf66fe4046b22e8159298cc433e014b14d9c9b3e01cafff1052743cf90896c396d86e1ccfc87d689553da
|
7
|
+
data.tar.gz: 70f4ce83fd7c7c8aa4c066fb94e96ef62a5087c3f78e079cd473e8d45ab22e106eea7b3d7bf5965063fecb9b77c58b8548598f4a873d9480c5b501bc7854dea9
|
data/lib/tcp-client.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'tcp-client/errors'
|
3
4
|
require_relative 'tcp-client/address'
|
4
5
|
require_relative 'tcp-client/tcp_socket'
|
5
6
|
require_relative 'tcp-client/ssl_socket'
|
@@ -8,30 +9,9 @@ require_relative 'tcp-client/default_configuration'
|
|
8
9
|
require_relative 'tcp-client/version'
|
9
10
|
|
10
11
|
class TCPClient
|
11
|
-
class NoOpenSSL < RuntimeError
|
12
|
-
def self.raise!
|
13
|
-
raise(self, 'OpenSSL is not avail', caller(1))
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class NotConnected < SocketError
|
18
|
-
def self.raise!(reason)
|
19
|
-
raise(self, "client not connected - #{reason}", caller(1))
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
TimeoutError = Class.new(IOError)
|
24
|
-
ConnectTimeoutError = Class.new(TimeoutError)
|
25
|
-
ReadTimeoutError = Class.new(TimeoutError)
|
26
|
-
WriteTimeoutError = Class.new(TimeoutError)
|
27
|
-
|
28
|
-
Timeout = TimeoutError # backward compatibility
|
29
|
-
deprecate_constant(:Timeout)
|
30
|
-
|
31
12
|
def self.open(addr, configuration = Configuration.default)
|
32
|
-
addr = Address.new(addr)
|
33
13
|
client = new
|
34
|
-
client.connect(addr, configuration)
|
14
|
+
client.connect(Address.new(addr), configuration)
|
35
15
|
block_given? ? yield(client) : client
|
36
16
|
ensure
|
37
17
|
client&.close if block_given?
|
@@ -40,34 +20,32 @@ class TCPClient
|
|
40
20
|
attr_reader :address
|
41
21
|
|
42
22
|
def initialize
|
43
|
-
@socket = @address = @
|
44
|
-
@deadline = nil
|
23
|
+
@socket = @address = @deadline = @cfg = nil
|
45
24
|
end
|
46
25
|
|
47
26
|
def to_s
|
48
27
|
@address ? @address.to_s : ''
|
49
28
|
end
|
50
29
|
|
51
|
-
def connect(addr, configuration, exception:
|
30
|
+
def connect(addr, configuration, exception: nil)
|
52
31
|
close
|
53
|
-
NoOpenSSL
|
32
|
+
raise(NoOpenSSL) if configuration.ssl? && !defined?(SSLSocket)
|
54
33
|
@address = Address.new(addr)
|
55
|
-
@
|
56
|
-
configuration.
|
34
|
+
@cfg = configuration.dup
|
35
|
+
exception ||= configuration.connect_timeout_error
|
36
|
+
@socket = TCPSocket.new(@address, @cfg, exception)
|
37
|
+
@cfg.ssl? &&
|
57
38
|
@socket = SSLSocket.new(@socket, @address, configuration, exception)
|
58
|
-
@write_timeout = configuration.write_timeout
|
59
|
-
@read_timeout = configuration.read_timeout
|
60
39
|
self
|
61
40
|
end
|
62
41
|
|
63
42
|
def close
|
64
|
-
|
65
|
-
socket&.close
|
43
|
+
@socket&.close
|
66
44
|
self
|
67
45
|
rescue IOError
|
68
46
|
self
|
69
47
|
ensure
|
70
|
-
@deadline = nil
|
48
|
+
@socket = @deadline = nil
|
71
49
|
end
|
72
50
|
|
73
51
|
def closed?
|
@@ -75,26 +53,32 @@ class TCPClient
|
|
75
53
|
end
|
76
54
|
|
77
55
|
def with_deadline(timeout)
|
78
|
-
|
79
|
-
raise(
|
56
|
+
previous_deadline = @deadline
|
57
|
+
raise(NoBlockGiven) unless block_given?
|
80
58
|
tm = timeout&.to_f
|
81
|
-
raise(
|
59
|
+
raise(InvalidDeadLine) unless tm&.positive?
|
82
60
|
@deadline = Time.now + tm
|
83
61
|
yield(self)
|
84
62
|
ensure
|
85
|
-
@deadline =
|
63
|
+
@deadline = previous_deadline
|
86
64
|
end
|
87
65
|
|
88
|
-
def read(nbytes, timeout: nil, exception:
|
89
|
-
|
90
|
-
|
91
|
-
|
66
|
+
def read(nbytes, timeout: nil, exception: nil)
|
67
|
+
raise(NotConnected) if closed?
|
68
|
+
timeout.nil? && @deadline and
|
69
|
+
return read_with_deadline(nbytes, @deadline, exception)
|
70
|
+
timeout = (timeout || @cfg.read_timeout).to_f
|
71
|
+
return @socket.read(nbytes) unless timeout.positive?
|
72
|
+
read_with_deadline(nbytes, Time.now + timeout, exception)
|
92
73
|
end
|
93
74
|
|
94
|
-
def write(*msg, timeout: nil, exception:
|
95
|
-
|
96
|
-
|
97
|
-
|
75
|
+
def write(*msg, timeout: nil, exception: nil)
|
76
|
+
raise(NotConnected) if closed?
|
77
|
+
timeout.nil? && @deadline and
|
78
|
+
return write_with_deadline(msg, @deadline, exception)
|
79
|
+
timeout = (timeout || @cfg.write_timeout).to_f
|
80
|
+
return @socket.write(*msg) unless timeout.positive?
|
81
|
+
write_with_deadline(msg, Time.now + timeout, exception)
|
98
82
|
end
|
99
83
|
|
100
84
|
def flush
|
@@ -104,9 +88,18 @@ class TCPClient
|
|
104
88
|
|
105
89
|
private
|
106
90
|
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
91
|
+
def read_with_deadline(nbytes, deadline, exception)
|
92
|
+
@socket.read_with_deadline(
|
93
|
+
nbytes,
|
94
|
+
deadline,
|
95
|
+
exception || @cfg.read_timeout_error
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def write_with_deadline(msg, deadline, exception)
|
100
|
+
exception ||= @cfg.write_timeout_error
|
101
|
+
msg.sum do |chunk|
|
102
|
+
@socket.write_with_deadline(chunk.b, deadline, exception)
|
103
|
+
end
|
111
104
|
end
|
112
105
|
end
|
data/lib/tcp-client/address.rb
CHANGED
@@ -10,10 +10,10 @@ class TCPClient
|
|
10
10
|
case addr
|
11
11
|
when self.class
|
12
12
|
init_from_selfclass(addr)
|
13
|
-
when Integer
|
14
|
-
init_from_addrinfo(Addrinfo.tcp(nil, addr))
|
15
13
|
when Addrinfo
|
16
14
|
init_from_addrinfo(addr)
|
15
|
+
when Integer
|
16
|
+
init_from_addrinfo(Addrinfo.tcp(nil, addr))
|
17
17
|
else
|
18
18
|
init_from_string(addr)
|
19
19
|
end
|
@@ -59,7 +59,9 @@ class TCPClient
|
|
59
59
|
def from_string(str)
|
60
60
|
idx = str.rindex(':') or return nil, str.to_i
|
61
61
|
name = str[0, idx]
|
62
|
-
|
62
|
+
if name.start_with?('[') && name.end_with?(']')
|
63
|
+
name = name[1, name.size - 2]
|
64
|
+
end
|
63
65
|
[name, str[idx + 1, str.size - idx].to_i]
|
64
66
|
end
|
65
67
|
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'errors'
|
4
|
+
|
1
5
|
class TCPClient
|
2
6
|
class Configuration
|
3
7
|
def self.create(options = {})
|
@@ -6,15 +10,38 @@ class TCPClient
|
|
6
10
|
ret
|
7
11
|
end
|
8
12
|
|
9
|
-
attr_reader :buffered,
|
13
|
+
attr_reader :buffered,
|
14
|
+
:keep_alive,
|
15
|
+
:reverse_lookup,
|
16
|
+
:timeout,
|
17
|
+
:connect_timeout,
|
18
|
+
:read_timeout,
|
19
|
+
:write_timeout,
|
20
|
+
:connect_timeout_error,
|
21
|
+
:read_timeout_error,
|
22
|
+
:write_timeout_error
|
10
23
|
attr_accessor :ssl_params
|
11
24
|
|
12
25
|
def initialize(options = {})
|
13
26
|
@buffered = @keep_alive = @reverse_lookup = true
|
14
27
|
self.timeout = @ssl_params = nil
|
28
|
+
@connect_timeout_error = ConnectTimeoutError
|
29
|
+
@read_timeout_error = ReadTimeoutError
|
30
|
+
@write_timeout_error = WriteTimeoutError
|
15
31
|
options.each_pair { |attribute, value| set(attribute, value) }
|
16
32
|
end
|
17
33
|
|
34
|
+
def freeze
|
35
|
+
@ssl_params.freeze
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize_copy(_org)
|
40
|
+
super
|
41
|
+
@ssl_params = @ssl_params.dup
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
18
45
|
def ssl?
|
19
46
|
@ssl_params ? true : false
|
20
47
|
end
|
@@ -38,32 +65,41 @@ class TCPClient
|
|
38
65
|
end
|
39
66
|
|
40
67
|
def timeout=(seconds)
|
41
|
-
@timeout =
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
def connect_timeout
|
46
|
-
@connect_timeout || @timeout
|
68
|
+
@timeout =
|
69
|
+
@connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
|
47
70
|
end
|
48
71
|
|
49
72
|
def connect_timeout=(seconds)
|
50
73
|
@connect_timeout = seconds(seconds)
|
51
74
|
end
|
52
75
|
|
53
|
-
def
|
54
|
-
@
|
76
|
+
def read_timeout=(seconds)
|
77
|
+
@read_timeout = seconds(seconds)
|
55
78
|
end
|
56
79
|
|
57
80
|
def write_timeout=(seconds)
|
58
81
|
@write_timeout = seconds(seconds)
|
59
82
|
end
|
60
83
|
|
61
|
-
def
|
62
|
-
|
84
|
+
def timeout_error=(exception)
|
85
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
86
|
+
@connect_timeout_error =
|
87
|
+
@read_timeout_error = @write_timeout_error = exception
|
63
88
|
end
|
64
89
|
|
65
|
-
def
|
66
|
-
|
90
|
+
def connect_timeout_error=(exception)
|
91
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
92
|
+
@connect_timeout_error = exception
|
93
|
+
end
|
94
|
+
|
95
|
+
def read_timeout_error=(exception)
|
96
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
97
|
+
@read_timeout_error = exception
|
98
|
+
end
|
99
|
+
|
100
|
+
def write_timeout_error=(exception)
|
101
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
102
|
+
@write_timeout_error = exception
|
67
103
|
end
|
68
104
|
|
69
105
|
def to_h
|
@@ -75,6 +111,9 @@ class TCPClient
|
|
75
111
|
connect_timeout: @connect_timeout,
|
76
112
|
read_timeout: @read_timeout,
|
77
113
|
write_timeout: @write_timeout,
|
114
|
+
connect_timeout_error: @connect_timeout_error,
|
115
|
+
read_timeout_error: @read_timeout_error,
|
116
|
+
write_timeout_error: @write_timeout_error,
|
78
117
|
ssl_params: @ssl_params
|
79
118
|
}
|
80
119
|
end
|
@@ -90,10 +129,14 @@ class TCPClient
|
|
90
129
|
|
91
130
|
private
|
92
131
|
|
132
|
+
def exception_class?(value)
|
133
|
+
value.is_a?(Class) && value < Exception
|
134
|
+
end
|
135
|
+
|
93
136
|
def set(attribute, value)
|
94
137
|
public_send("#{attribute}=", value)
|
95
138
|
rescue NoMethodError
|
96
|
-
raise(
|
139
|
+
raise(UnknownAttribute, attribute)
|
97
140
|
end
|
98
141
|
|
99
142
|
def seconds(value)
|
@@ -6,10 +6,8 @@ class TCPClient
|
|
6
6
|
class << self
|
7
7
|
attr_reader :default_configuration
|
8
8
|
|
9
|
-
def configure(options = {})
|
10
|
-
|
11
|
-
yield(cfg) if block_given?
|
12
|
-
@default_configuration = cfg
|
9
|
+
def configure(options = {}, &block)
|
10
|
+
@default_configuration = Configuration.create(options, &block)
|
13
11
|
end
|
14
12
|
end
|
15
13
|
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class TCPClient
|
6
|
+
class NoOpenSSL < RuntimeError
|
7
|
+
def initialize
|
8
|
+
super('OpenSSL is not avail')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoBlockGiven < ArgumentError
|
13
|
+
def initialize
|
14
|
+
super('no block given')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidDeadLine < ArgumentError
|
19
|
+
def initialize(timeout)
|
20
|
+
super("invalid deadline - #{timeout}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnknownAttribute < ArgumentError
|
25
|
+
def initialize(attribute)
|
26
|
+
super("unknown attribute - #{attribute}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NotAnException < TypeError
|
31
|
+
def initialize(object)
|
32
|
+
super("not a valid exception class - #{object.inspect}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class NotConnected < IOError
|
37
|
+
def initialize
|
38
|
+
super('client not connected')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class TimeoutError < IOError
|
43
|
+
def initialize(message = nil)
|
44
|
+
super(message || "unable to #{action} in time")
|
45
|
+
end
|
46
|
+
|
47
|
+
def action
|
48
|
+
:process
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class ConnectTimeoutError < TimeoutError
|
53
|
+
def action
|
54
|
+
:connect
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ReadTimeoutError < TimeoutError
|
59
|
+
def action
|
60
|
+
:read
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class WriteTimeoutError < TimeoutError
|
65
|
+
def action
|
66
|
+
:write
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Timeout = TimeoutError # backward compatibility
|
71
|
+
deprecate_constant(:Timeout)
|
72
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module IOWithDeadlineMixin
|
2
|
+
def self.included(mod)
|
3
|
+
methods = mod.instance_methods
|
4
|
+
if methods.index(:wait_writable) && methods.index(:wait_readable)
|
5
|
+
mod.include(ViaWaitMethod)
|
6
|
+
else
|
7
|
+
mod.include(ViaSelect)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_with_deadline(bytes_to_read, deadline, exception)
|
12
|
+
raise(exception) if Time.now > deadline
|
13
|
+
result = ''.b
|
14
|
+
return result if bytes_to_read <= 0
|
15
|
+
loop do
|
16
|
+
read =
|
17
|
+
with_deadline(deadline, exception) do
|
18
|
+
read_nonblock(bytes_to_read - result.bytesize, exception: false)
|
19
|
+
end
|
20
|
+
unless read
|
21
|
+
close
|
22
|
+
return result
|
23
|
+
end
|
24
|
+
result += read
|
25
|
+
return result if result.bytesize >= bytes_to_read
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_with_deadline(data, deadline, exception)
|
30
|
+
raise(exception) if Time.now > deadline
|
31
|
+
return 0 if (size = data.bytesize).zero?
|
32
|
+
result = 0
|
33
|
+
loop do
|
34
|
+
written =
|
35
|
+
with_deadline(deadline, exception) do
|
36
|
+
write_nonblock(data, exception: false)
|
37
|
+
end
|
38
|
+
result += written
|
39
|
+
return result if result >= size
|
40
|
+
data = data.byteslice(written, data.bytesize - written)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ViaWaitMethod
|
45
|
+
private def with_deadline(deadline, exception)
|
46
|
+
loop do
|
47
|
+
case ret = yield
|
48
|
+
when :wait_writable
|
49
|
+
raise(exception) if (remaining_time = deadline - Time.now) <= 0
|
50
|
+
raise(exception) if wait_writable(remaining_time).nil?
|
51
|
+
when :wait_readable
|
52
|
+
raise(exception) if (remaining_time = deadline - Time.now) <= 0
|
53
|
+
raise(exception) if wait_readable(remaining_time).nil?
|
54
|
+
else
|
55
|
+
return ret
|
56
|
+
end
|
57
|
+
end
|
58
|
+
rescue Errno::ETIMEDOUT
|
59
|
+
raise(exception)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ViaSelect
|
64
|
+
private def with_deadline(deadline, exception)
|
65
|
+
loop do
|
66
|
+
case ret = yield
|
67
|
+
when :wait_writable
|
68
|
+
raise(exception) if (remaining_time = deadline - Time.now) <= 0
|
69
|
+
raise(exception) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
70
|
+
when :wait_readable
|
71
|
+
raise(exception) if (remaining_time = deadline - Time.now) <= 0
|
72
|
+
raise(exception) if ::IO.select([self], nil, nil, remaining_time).nil?
|
73
|
+
else
|
74
|
+
return ret
|
75
|
+
end
|
76
|
+
end
|
77
|
+
rescue Errno::ETIMEDOUT
|
78
|
+
raise(exception)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private_constant(:ViaWaitMethod, :ViaSelect)
|
83
|
+
end
|
@@ -4,14 +4,15 @@ rescue LoadError
|
|
4
4
|
return
|
5
5
|
end
|
6
6
|
|
7
|
-
require_relative 'mixin/
|
7
|
+
require_relative 'mixin/io_with_deadline'
|
8
8
|
|
9
9
|
class TCPClient
|
10
10
|
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
11
|
-
include
|
11
|
+
include IOWithDeadlineMixin
|
12
12
|
|
13
13
|
def initialize(socket, address, configuration, exception)
|
14
14
|
ssl_params = Hash[configuration.ssl_params]
|
15
|
+
self.sync_close = true
|
15
16
|
super(socket, create_context(ssl_params))
|
16
17
|
connect_to(
|
17
18
|
address,
|
@@ -24,19 +25,20 @@ class TCPClient
|
|
24
25
|
private
|
25
26
|
|
26
27
|
def create_context(ssl_params)
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
context = OpenSSL::SSL::SSLContext.new
|
29
|
+
context.set_params(ssl_params)
|
30
|
+
context
|
30
31
|
end
|
31
32
|
|
32
33
|
def connect_to(address, check, timeout, exception)
|
33
34
|
self.hostname = address.hostname
|
34
|
-
|
35
|
+
timeout = timeout.to_f
|
36
|
+
if timeout.zero?
|
37
|
+
connect
|
38
|
+
else
|
35
39
|
with_deadline(Time.now + timeout, exception) do
|
36
40
|
connect_nonblock(exception: false)
|
37
41
|
end
|
38
|
-
else
|
39
|
-
connect
|
40
42
|
end
|
41
43
|
post_connection_check(address.hostname) if check
|
42
44
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'socket'
|
2
|
-
require_relative 'mixin/
|
2
|
+
require_relative 'mixin/io_with_deadline'
|
3
3
|
|
4
4
|
class TCPClient
|
5
5
|
class TCPSocket < ::Socket
|
6
|
-
include
|
6
|
+
include IOWithDeadlineMixin
|
7
7
|
|
8
8
|
def initialize(address, configuration, exception)
|
9
9
|
super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
|
@@ -19,7 +19,8 @@ class TCPClient
|
|
19
19
|
address.addrinfo.ip_port,
|
20
20
|
address.addrinfo.ip_address
|
21
21
|
)
|
22
|
-
|
22
|
+
timeout = timeout.to_f
|
23
|
+
return connect(addr) if timeout.zero?
|
23
24
|
with_deadline(Time.now + timeout, exception) do
|
24
25
|
connect_nonblock(addr, exception: false)
|
25
26
|
end
|
data/lib/tcp-client/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -6,12 +6,12 @@ require 'bundler/gem_tasks'
|
|
6
6
|
|
7
7
|
$stdout.sync = $stderr.sync = true
|
8
8
|
|
9
|
-
task(:default) { exec('rake --tasks') }
|
10
|
-
|
11
9
|
CLOBBER << 'prj'
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
task(:default) { exec('rake --tasks') }
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test) do |task|
|
14
|
+
task.test_files = FileList['test/**/*_test.rb']
|
15
|
+
task.ruby_opts = %w[-w]
|
16
|
+
task.verbose = true
|
17
17
|
end
|
@@ -13,7 +13,7 @@ class AddressTest < MiniTest::Test
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_create_from_addrinfo
|
16
|
-
addrinfo = Addrinfo.tcp('
|
16
|
+
addrinfo = Addrinfo.tcp('localhost', 42)
|
17
17
|
subject = TCPClient::Address.new(addrinfo)
|
18
18
|
assert_equal(addrinfo.getnameinfo[0], subject.hostname)
|
19
19
|
assert_equal(addrinfo, subject.addrinfo)
|
@@ -39,7 +39,8 @@ class ConfigurationTest < MiniTest::Test
|
|
39
39
|
connect_timeout: 1,
|
40
40
|
read_timeout: 2,
|
41
41
|
write_timeout: 3,
|
42
|
-
ssl: true
|
42
|
+
ssl: true,
|
43
|
+
connect_timeout_error: IOError
|
43
44
|
)
|
44
45
|
refute(subject.buffered)
|
45
46
|
refute(subject.keep_alive)
|
@@ -48,6 +49,8 @@ class ConfigurationTest < MiniTest::Test
|
|
48
49
|
assert_same(2, subject.read_timeout)
|
49
50
|
assert_same(3, subject.write_timeout)
|
50
51
|
assert(subject.ssl?)
|
52
|
+
assert_same(IOError, subject.connect_timeout_error)
|
53
|
+
assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
|
51
54
|
end
|
52
55
|
|
53
56
|
def test_invalid_option
|
@@ -88,6 +91,18 @@ class ConfigurationTest < MiniTest::Test
|
|
88
91
|
assert_same(42, subject.write_timeout)
|
89
92
|
end
|
90
93
|
|
94
|
+
def test_timeout_error_overwrite
|
95
|
+
subject = TCPClient::Configuration.new
|
96
|
+
assert_same(TCPClient::ConnectTimeoutError, subject.connect_timeout_error)
|
97
|
+
assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
|
98
|
+
assert_same(TCPClient::WriteTimeoutError, subject.write_timeout_error)
|
99
|
+
|
100
|
+
subject.timeout_error = IOError
|
101
|
+
assert_same(IOError, subject.connect_timeout_error)
|
102
|
+
assert_same(IOError, subject.read_timeout_error)
|
103
|
+
assert_same(IOError, subject.write_timeout_error)
|
104
|
+
end
|
105
|
+
|
91
106
|
def test_compare
|
92
107
|
a = TCPClient::Configuration.new
|
93
108
|
b = TCPClient::Configuration.new
|
@@ -95,4 +110,32 @@ class ConfigurationTest < MiniTest::Test
|
|
95
110
|
assert(a == b)
|
96
111
|
assert(a === b)
|
97
112
|
end
|
113
|
+
|
114
|
+
def test_dup
|
115
|
+
source =
|
116
|
+
TCPClient::Configuration.new(
|
117
|
+
buffered: false,
|
118
|
+
keep_alive: false,
|
119
|
+
reverse_lookup: false,
|
120
|
+
connect_timeout: 1,
|
121
|
+
read_timeout: 2,
|
122
|
+
write_timeout: 3,
|
123
|
+
ssl: {
|
124
|
+
ssl_version: :TLSv1_2
|
125
|
+
}
|
126
|
+
)
|
127
|
+
shadow = source.dup.freeze
|
128
|
+
|
129
|
+
# some changes
|
130
|
+
source.buffered = true
|
131
|
+
source.write_timeout = 5
|
132
|
+
source.ssl_params[:err] = true
|
133
|
+
source.timeout_error = IOError
|
134
|
+
|
135
|
+
refute_equal(source.__id__, shadow.__id__)
|
136
|
+
refute(shadow.buffered)
|
137
|
+
assert_equal(3, shadow.write_timeout)
|
138
|
+
assert_equal({ ssl_version: :TLSv1_2 }, shadow.ssl_params)
|
139
|
+
assert_same(TCPClient::ReadTimeoutError, shadow.read_timeout_error)
|
140
|
+
end
|
98
141
|
end
|
data/test/tcp_client_test.rb
CHANGED
@@ -11,6 +11,10 @@ class TCPClientTest < MiniTest::Test
|
|
11
11
|
@config = TCPClient::Configuration.create(buffered: false)
|
12
12
|
end
|
13
13
|
|
14
|
+
def port
|
15
|
+
PseudoServer.local_address.ip_port
|
16
|
+
end
|
17
|
+
|
14
18
|
def test_defaults
|
15
19
|
subject = TCPClient.new
|
16
20
|
assert(subject.closed?)
|
@@ -43,15 +47,15 @@ class TCPClientTest < MiniTest::Test
|
|
43
47
|
end
|
44
48
|
|
45
49
|
def test_connected_state
|
46
|
-
TCPClient.open(
|
50
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
47
51
|
refute(subject.closed?)
|
48
|
-
assert_equal(
|
52
|
+
assert_equal("localhost:#{port}", subject.to_s)
|
49
53
|
refute_nil(subject.address)
|
50
54
|
address_when_opened = subject.address
|
51
|
-
assert_equal(
|
55
|
+
assert_equal("localhost:#{port}", subject.address.to_s)
|
52
56
|
assert_equal('localhost', subject.address.hostname)
|
53
57
|
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
54
|
-
assert_same(
|
58
|
+
assert_same(port, subject.address.addrinfo.ip_port)
|
55
59
|
|
56
60
|
subject.close
|
57
61
|
assert(subject.closed?)
|
@@ -60,14 +64,14 @@ class TCPClientTest < MiniTest::Test
|
|
60
64
|
end
|
61
65
|
|
62
66
|
def check_read_timeout(timeout)
|
63
|
-
TCPClient.open(
|
67
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
64
68
|
refute(subject.closed?)
|
65
69
|
start_time = nil
|
66
70
|
assert_raises(TCPClient::ReadTimeoutError) do
|
67
71
|
start_time = Time.now
|
68
72
|
subject.read(42, timeout: timeout)
|
69
73
|
end
|
70
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
74
|
+
assert_in_delta(timeout, Time.now - start_time, 0.15)
|
71
75
|
end
|
72
76
|
end
|
73
77
|
|
@@ -78,14 +82,14 @@ class TCPClientTest < MiniTest::Test
|
|
78
82
|
end
|
79
83
|
|
80
84
|
def check_write_timeout(timeout)
|
81
|
-
TCPClient.open(
|
85
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
82
86
|
refute(subject.closed?)
|
83
87
|
start_time = nil
|
84
88
|
assert_raises(TCPClient::WriteTimeoutError) do
|
85
89
|
start_time = Time.now
|
86
90
|
subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
|
87
91
|
end
|
88
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
92
|
+
assert_in_delta(timeout, Time.now - start_time, 0.15)
|
89
93
|
end
|
90
94
|
end
|
91
95
|
|
@@ -95,7 +99,7 @@ class TCPClientTest < MiniTest::Test
|
|
95
99
|
end
|
96
100
|
|
97
101
|
def test_write_deadline
|
98
|
-
TCPClient.open(
|
102
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
99
103
|
refute(subject.closed?)
|
100
104
|
assert_raises(TCPClient::WriteTimeoutError) do
|
101
105
|
subject.with_deadline(0.25) do |*args|
|
@@ -107,7 +111,7 @@ class TCPClientTest < MiniTest::Test
|
|
107
111
|
end
|
108
112
|
|
109
113
|
def test_read_deadline
|
110
|
-
TCPClient.open(
|
114
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
111
115
|
refute(subject.closed?)
|
112
116
|
assert_raises(TCPClient::ReadTimeoutError) do
|
113
117
|
subject.with_deadline(0.25) do |*args|
|
@@ -119,7 +123,7 @@ class TCPClientTest < MiniTest::Test
|
|
119
123
|
end
|
120
124
|
|
121
125
|
def test_read_write_deadline
|
122
|
-
TCPClient.open(
|
126
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
123
127
|
refute(subject.closed?)
|
124
128
|
assert_raises(TCPClient::TimeoutError) do
|
125
129
|
subject.with_deadline(0.25) do |*args|
|
@@ -137,9 +141,9 @@ class TCPClientTest < MiniTest::Test
|
|
137
141
|
start_time = nil
|
138
142
|
assert_raises(TCPClient::ConnectTimeoutError) do
|
139
143
|
start_time = Time.now
|
140
|
-
TCPClient.new.connect(
|
144
|
+
TCPClient.new.connect("localhost:#{port}", ssl_config)
|
141
145
|
end
|
142
|
-
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.
|
146
|
+
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.25)
|
143
147
|
end
|
144
148
|
|
145
149
|
def test_connect_ssl_timeout
|
data/test/test_helper.rb
CHANGED
@@ -4,8 +4,6 @@ require 'minitest/autorun'
|
|
4
4
|
require 'minitest/parallel'
|
5
5
|
require_relative '../lib/tcp-client'
|
6
6
|
|
7
|
-
$stdout.sync = $stderr.sync = true
|
8
|
-
|
9
7
|
# this pseudo-server never reads or writes anything
|
10
|
-
|
11
|
-
Minitest.after_run {
|
8
|
+
PseudoServer = TCPServer.new('localhost', 0)
|
9
|
+
Minitest.after_run { PseudoServer.close }
|
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.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -71,7 +71,8 @@ files:
|
|
71
71
|
- lib/tcp-client/address.rb
|
72
72
|
- lib/tcp-client/configuration.rb
|
73
73
|
- lib/tcp-client/default_configuration.rb
|
74
|
-
- lib/tcp-client/
|
74
|
+
- lib/tcp-client/errors.rb
|
75
|
+
- lib/tcp-client/mixin/io_with_deadline.rb
|
75
76
|
- lib/tcp-client/ssl_socket.rb
|
76
77
|
- lib/tcp-client/tcp_socket.rb
|
77
78
|
- lib/tcp-client/version.rb
|
@@ -106,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
107
|
- !ruby/object:Gem::Version
|
107
108
|
version: '0'
|
108
109
|
requirements: []
|
109
|
-
rubygems_version: 3.2.
|
110
|
+
rubygems_version: 3.2.22
|
110
111
|
signing_key:
|
111
112
|
specification_version: 4
|
112
113
|
summary: A TCP client implementation with working timeout support.
|
@@ -1,96 +0,0 @@
|
|
1
|
-
IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
|
2
|
-
|
3
|
-
module IOTimeoutMixin
|
4
|
-
def self.included(mod)
|
5
|
-
im = mod.instance_methods
|
6
|
-
if im.index(:wait_writable) && im.index(:wait_readable)
|
7
|
-
mod.include(DeadlineMethods)
|
8
|
-
else
|
9
|
-
mod.include(DeadlineIO)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def read(nbytes, timeout: nil, exception: IOTimeoutError)
|
14
|
-
timeout = timeout.to_f
|
15
|
-
return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
|
16
|
-
deadline = Time.now + timeout
|
17
|
-
read_all(nbytes) do |junk_size|
|
18
|
-
with_deadline(deadline, exception) do
|
19
|
-
read_nonblock(junk_size, exception: false)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def write(*msgs, timeout: nil, exception: IOTimeoutError)
|
25
|
-
timeout = timeout.to_f
|
26
|
-
return write_all(msgs.join.b) { |junk| super(junk) } if timeout <= 0
|
27
|
-
deadline = Time.now + timeout
|
28
|
-
write_all(msgs.join.b) do |junk|
|
29
|
-
with_deadline(deadline, exception) do
|
30
|
-
write_nonblock(junk, exception: false)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def read_all(nbytes)
|
38
|
-
return '' if nbytes.zero?
|
39
|
-
result = ''
|
40
|
-
loop do
|
41
|
-
unless read = yield(nbytes - result.bytesize)
|
42
|
-
close
|
43
|
-
return result
|
44
|
-
end
|
45
|
-
result += read
|
46
|
-
return result if result.bytesize >= nbytes
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def write_all(data)
|
51
|
-
return 0 if (size = data.bytesize).zero?
|
52
|
-
result = 0
|
53
|
-
loop do
|
54
|
-
written = yield(data)
|
55
|
-
result += written
|
56
|
-
return result if result >= size
|
57
|
-
data = data.byteslice(written, data.bytesize - written)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
module DeadlineMethods
|
62
|
-
private def with_deadline(deadline, exclass)
|
63
|
-
loop do
|
64
|
-
case ret = yield
|
65
|
-
when :wait_writable
|
66
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
67
|
-
raise(exclass) if wait_writable(remaining_time).nil?
|
68
|
-
when :wait_readable
|
69
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
70
|
-
raise(exclass) if wait_readable(remaining_time).nil?
|
71
|
-
else
|
72
|
-
return ret
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
module DeadlineIO
|
79
|
-
private def with_deadline(deadline, exclass)
|
80
|
-
loop do
|
81
|
-
case ret = yield
|
82
|
-
when :wait_writable
|
83
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
84
|
-
raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
85
|
-
when :wait_readable
|
86
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
87
|
-
raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
|
88
|
-
else
|
89
|
-
return ret
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
private_constant(:DeadlineMethods, :DeadlineIO)
|
96
|
-
end
|