tcp-client 0.1.5 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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