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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1c2652ec394a4f083349a0254f8eeb9432b18a8869b98d39db221cc76dbdd1b
4
- data.tar.gz: 0b26e622ae44a45f5e7b7c07fb9d81f6bf2a0f84bccb1ff7f0ab6563fe59b28e
3
+ metadata.gz: 4b265053761a87007b7de21c3a7a369b778f013ae726bb26220a4fd9aeeaf71c
4
+ data.tar.gz: 5a71b5dc86ed4ab9ef481e65c24aef0481c5e9d2c868f3f9975b801936d99691
5
5
  SHA512:
6
- metadata.gz: 7842b8e3d9f53287ed83e6ca9ebd139f33c2ddd6472b474d69db8c0481178b0f74a386dc62b8e0251cc0a2742624976efa22530582d3c3af14ae94231868fc4b
7
- data.tar.gz: af08470a96f7d2cf64086184dab3d39ac9d84741c19a848fbfb4ff0dab3e2499d72f17dce99d11967235e0d09b2b03a1c5c3116588884ca6656e6daf0f0015f8
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 = @write_timeout = @read_timeout = nil
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: ConnectTimeoutError)
30
+ def connect(addr, configuration, exception: nil)
52
31
  close
53
- NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
32
+ raise(NoOpenSSL) if configuration.ssl? && !defined?(SSLSocket)
54
33
  @address = Address.new(addr)
55
- @socket = TCPSocket.new(@address, configuration, exception)
56
- configuration.ssl? &&
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
- socket, @socket = @socket, nil
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
- raise('no block given') unless block_given?
79
- raise('deadline already used') if @deadline
56
+ previous_deadline = @deadline
57
+ raise(NoBlockGiven) unless block_given?
80
58
  tm = timeout&.to_f
81
- raise(ArgumentError, "invalid deadline - #{timeout}") unless tm&.positive?
59
+ raise(InvalidDeadLine) unless tm&.positive?
82
60
  @deadline = Time.now + tm
83
61
  yield(self)
84
62
  ensure
85
- @deadline = nil
63
+ @deadline = previous_deadline
86
64
  end
87
65
 
88
- def read(nbytes, timeout: nil, exception: ReadTimeoutError)
89
- NotConnected.raise!(self) if closed?
90
- time = timeout || remaining_time(exception) || @read_timeout
91
- @socket.read(nbytes, timeout: time, exception: exception)
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: WriteTimeoutError)
95
- NotConnected.raise!(self) if closed?
96
- time = timeout || remaining_time(exception) || @write_timeout
97
- @socket.write(*msg, timeout: time, exception: exception)
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 remaining_time(exception)
108
- return unless @deadline
109
- remaining_time = @deadline - Time.now
110
- 0 < remaining_time ? remaining_time : raise(exception)
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
@@ -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
- name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
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, :keep_alive, :reverse_lookup, :timeout
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 = seconds(seconds)
42
- @connect_timeout = @write_timeout = @read_timeout = nil
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 write_timeout
54
- @write_timeout || @timeout
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 read_timeout
62
- @read_timeout || @timeout
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 read_timeout=(seconds)
66
- @read_timeout = seconds(seconds)
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(ArgumentError, "unknown attribute - #{attribute}")
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
- cfg = Configuration.new(options)
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/io_timeout'
7
+ require_relative 'mixin/io_with_deadline'
8
8
 
9
9
  class TCPClient
10
10
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
11
- include IOTimeoutMixin
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
- ctx = OpenSSL::SSL::SSLContext.new
28
- ctx.set_params(ssl_params)
29
- ctx
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
- if timeout
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/io_timeout'
2
+ require_relative 'mixin/io_with_deadline'
3
3
 
4
4
  class TCPClient
5
5
  class TCPSocket < ::Socket
6
- include IOTimeoutMixin
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
- return connect(addr) unless timeout
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
@@ -1,3 +1,3 @@
1
1
  class TCPClient
2
- VERSION = '0.1.5'.freeze
2
+ VERSION = '0.3.2'.freeze
3
3
  end
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
- Rake::TestTask.new(:test) do |t|
14
- t.ruby_opts = %w[-w]
15
- t.verbose = true
16
- t.test_files = FileList['test/**/*_test.rb']
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('google.com', 42)
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
@@ -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('localhost:1234', config) do |subject|
50
+ TCPClient.open("localhost:#{port}", config) do |subject|
47
51
  refute(subject.closed?)
48
- assert_equal('localhost:1234', subject.to_s)
52
+ assert_equal("localhost:#{port}", subject.to_s)
49
53
  refute_nil(subject.address)
50
54
  address_when_opened = subject.address
51
- assert_equal('localhost:1234', subject.address.to_s)
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(1234, subject.address.addrinfo.ip_port)
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('localhost:1234', config) do |subject|
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.11)
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('localhost:1234', config) do |subject|
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.02)
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('localhost:1234', config) do |subject|
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('localhost:1234', config) do |subject|
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('localhost:1234', config) do |subject|
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('localhost:1234', ssl_config)
144
+ TCPClient.new.connect("localhost:#{port}", ssl_config)
141
145
  end
142
- assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
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
- DummyServer = TCPServer.new('localhost', 1234)
11
- Minitest.after_run { DummyServer.close }
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.1.5
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-02-12 00:00:00.000000000 Z
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/mixin/io_timeout.rb
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.9
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