io_request 2.2.0 → 2.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f13666edea5cbb28ef091256048953a91693bb015d089b497dfa2657b75e2ca5
4
- data.tar.gz: d05a984fe433d148c049f2156637fe7bf7ff6e19aa89f85d90f776e4337b8110
3
+ metadata.gz: 53fdb5fa823faf9d379e6c2180f0d47f79959caa144dd1c34295c81226a4566d
4
+ data.tar.gz: 8614bb17e707782adc8d13ea01d0328bac2432658d0dc91977ee728107dcfcb7
5
5
  SHA512:
6
- metadata.gz: a4abe0a1f30f5760ab110f9b8ad097d4eda24548418398d0b24102d4b2fd8bb652082414c993672aa81915a939e8f07130a87eb1f137543ca87a54a1e0879ead
7
- data.tar.gz: 5a541175e7b27d967c22333f700a2a08b4de10b4fa0804230a5ef90406d6be28eeaa62025e9dca32d8986dd553b846cbfd16c2cc5b66848eb81b0a0ec2a08099
6
+ metadata.gz: 234a5e6d21fe3b1214a719af4b52617d4f58a3cf205e17cd8fa2d045098ada47bae1a272b8ec1daadfe5909831408e754d6a84085cad3705b5e09c8d3f940ccf
7
+ data.tar.gz: ef33270ed7cbf3b2082f7845d1ca1bbab2b3e77a145cff65ec4de361fb9b728535b3bd7b6d63bfef28713a216daea9434a6b908459fc3d7be1002142e90e3386
@@ -1,7 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Main module.
4
- module IORequest; end
4
+ module IORequest
5
+ # Client received message of zero size.
6
+ class ZeroSizeMessageError < RuntimeError; end
7
+
8
+ # Authorization failed.
9
+ class AuthorizationFailureError < RuntimeError; end
10
+
11
+ # Request timed out.
12
+ class RequestTimeoutError < RuntimeError; end
13
+ end
5
14
 
6
15
  require_relative 'io_request/version'
7
16
  require_relative 'io_request/logging'
@@ -45,15 +45,17 @@ module IORequest
45
45
  # @param r [IO] object to read from.
46
46
  # @param w [IO] object to write to.
47
47
  # @param rw [IO] read-write object (replaces `r` and `w` arguments).
48
+ # @return [Object] data from {Authorizer}
48
49
  def open(read: nil, write: nil, read_write: nil)
49
50
  @io_r = read_write || read
50
51
  @io_w = read_write || write
51
52
 
52
53
  IORequest.logger.debug(prog_name) { 'Starting connection' }
53
54
 
54
- authorization
55
+ auth_data = authorization
55
56
  @open = true
56
57
  @data_transition_thread = in_thread(name: 'connection') { data_transition_loop }
58
+ auth_data
57
59
  end
58
60
 
59
61
  def open?
@@ -63,30 +65,38 @@ module IORequest
63
65
  # Close connection.
64
66
  def close
65
67
  close_internal
68
+
66
69
  join_threads
67
- @open = false
68
70
  end
69
71
 
70
72
  # @yieldparam [Hash]
71
73
  # @yieldreturn [Hash]
72
- def respond(&block)
73
- IORequest.logger.debug(prog_name) { 'Saved responder block' }
74
- @responder = block
74
+ def on_request(&block)
75
+ IORequest.logger.debug(prog_name) { 'Saved on_request block' }
76
+ @on_request = block
77
+ end
78
+ alias respond on_request
79
+
80
+ # Code to execute after connection is closed.
81
+ def on_close(&block)
82
+ IORequest.logger.debug(prog_name) { 'Saved on_close block' }
83
+ @on_close = block
75
84
  end
76
85
 
77
86
  # If callback block is provided, request will be sent asynchroniously.
78
87
  # @param data [Hash]
79
- def request(data = {}, &callback)
88
+ # @param timeout [Integer] timeout in seconds.
89
+ def request(data, timeout: nil, &callback)
80
90
  message = Message.new(data, type: :request)
81
91
 
82
92
  if block_given?
83
93
  # Async execution of request
84
94
  in_thread(callback, name: 'requesting') do |cb|
85
- cb.call(send_request_and_wait_for_response(message).data)
95
+ cb.call(send_request_and_wait_for_response(message, timeout).data)
86
96
  end
87
97
  nil
88
98
  else
89
- send_request_and_wait_for_response(message).data
99
+ send_request_and_wait_for_response(message, timeout).data
90
100
  end
91
101
  end
92
102
 
@@ -96,50 +106,52 @@ module IORequest
96
106
 
97
107
  def close_internal
98
108
  IORequest.logger.debug(prog_name) { 'Closing connection' }
99
- begin
100
- send_zero_size_request
101
- rescue StandardError
102
- IORequest.logger.debug(prog_name) { 'Failed to send zero-sized message. Closing anyway' }
103
- end
104
- stop_data_transition
109
+ send_zero_size_request
105
110
  close_io
106
- end
107
-
108
- def stop_data_transition
109
- return unless defined?(@data_transition_thread) && !@data_transition_thread.nil?
110
-
111
- IORequest.logger.debug(prog_name) { 'Killing data transition thread' }
112
- @data_transition_thread.kill
113
111
  @data_transition_thread = nil
112
+ @open = false
113
+ @on_close&.call if defined?(@on_close)
114
114
  end
115
115
 
116
116
  def close_io
117
- IORequest.logger.debug(prog_name) { 'Closing IO' }
118
- @mutex_r.synchronize { @io_r&.close }
119
- @mutex_w.synchronize { @io_w&.close }
117
+ begin
118
+ @io_r&.close
119
+ rescue StandardError => e
120
+ IORequest.logger.debug "Failed to close read IO: #{e}"
121
+ end
122
+ begin
123
+ @io_w&.close
124
+ rescue StandardError => e
125
+ IORequest.logger.debug "Failed to close write IO: #{e}"
126
+ end
127
+ IORequest.logger.debug(prog_name) { 'Closed IO streams' }
120
128
  end
121
129
 
122
130
  def authorization
123
- auth_successful = @mutex_r.synchronize do
131
+ auth_successful = false
132
+ data = nil
133
+ @mutex_r.synchronize do
124
134
  @mutex_w.synchronize do
125
135
  IORequest.logger.debug(prog_name) { 'Authorizing new client' }
126
- @authorizer.authorize(@io_r, @io_w)
136
+ auth_successful = @authorizer.authorize(@io_r, @io_w)
137
+ data = @authorizer.data
127
138
  end
128
139
  end
129
- unless auth_successful
130
- IORequest.logger.debug(prog_name) { 'Authorization failed' }
131
- raise 'Authorization failed'
132
- end
140
+ raise AuthorizationFailureError unless auth_successful
133
141
 
134
- IORequest.logger.debug(prog_name) { "New client authorized with data #{@authorizer.data}" }
142
+ IORequest.logger.debug(prog_name) { "New client authorized with data #{data}" }
143
+ data
135
144
  end
136
145
 
137
146
  def data_transition_loop
138
147
  IORequest.logger.debug(prog_name) { 'Starting data transition loop' }
139
148
  loop do
140
149
  data_transition_iteration
150
+ rescue ZeroSizeMessageError
151
+ IORequest.logger.debug(prog_name) { 'Connection was closed from the other side' }
152
+ break
141
153
  rescue StandardError => e
142
- IORequest.logger.debug(prog_name) { "Data transition iteration failed: #{e}" }
154
+ IORequest.logger.debug(prog_name) { "Data transition unknown error: #{e}" }
143
155
  break
144
156
  end
145
157
  close_internal
@@ -156,7 +168,8 @@ module IORequest
156
168
  end
157
169
 
158
170
  def handle_request(message)
159
- data = @responder&.call(message.data) || {}
171
+ data = {}
172
+ data = @on_request&.call(message.data) if defined?(@on_request)
160
173
  response = Message.new(data, type: :response, to: message.id)
161
174
  send_response(response)
162
175
  end
@@ -171,7 +184,11 @@ module IORequest
171
184
  def send_response(response)
172
185
  @mutex_w.synchronize do
173
186
  IORequest.logger.debug(prog_name) { "Sending response: #{response}" }
174
- response.write_to(@io_w)
187
+ begin
188
+ response.write_to(@io_w)
189
+ rescue IOError => e
190
+ IORequest.logger.debug(prog_name) { "Failed to write response message: #{e}" }
191
+ end
175
192
  end
176
193
  end
177
194
 
@@ -180,23 +197,30 @@ module IORequest
180
197
  IORequest.logger.debug(prog_name) { 'Sending zero size message' }
181
198
  @io_w.write([0].pack('S'))
182
199
  end
200
+ rescue StandardError => e
201
+ IORequest.logger.debug(prog_name) { "Failed to send zero-sized message(#{e})" }
183
202
  end
184
203
 
185
- def send_request_and_wait_for_response(request)
204
+ def send_request_and_wait_for_response(request, timeout)
186
205
  @mutex_w.synchronize do
187
206
  IORequest.logger.debug(prog_name) { "Sending message: #{request}" }
188
207
  request.write_to(@io_w)
189
208
  end
190
- wait_for_response(request)
209
+ wait_for_response(request, timeout)
191
210
  end
192
211
 
193
- def wait_for_response(request)
212
+ def wait_for_response(request, timeout)
194
213
  IORequest.logger.debug(prog_name) { "Waiting for response for #{request}" }
214
+ waiting_start_time = Time.now
195
215
  @responses_access_mutex.synchronize do
196
216
  response = nil
197
217
  until response
198
- @responses_access_cv.wait(@responses_access_mutex)
199
- response = @responses[request.id.to_s]
218
+ @responses_access_cv.wait(@responses_access_mutex, 1)
219
+ if @responses_access_mutex.owned?
220
+ # NOTE: Only accessing responses hash if thread owns access mutex
221
+ response = @responses.delete(request.id.to_s)
222
+ end
223
+ raise RequestTimeoutError if timeout && (Time.now - waiting_start_time < timeout)
200
224
  end
201
225
  IORequest.logger.debug(prog_name) { "Found response: #{response}" }
202
226
  response
@@ -32,11 +32,20 @@ module IORequest
32
32
  end
33
33
 
34
34
  # @return [Array<IORequest::Client>]
35
- attr_reader :clients
35
+ def clients
36
+ @clients_data.keys
37
+ end
38
+
39
+ # @param client [IORequest::Client]
40
+ # @return [Hash, nil] you are free to store anything you want in hash.
41
+ # Only field you will find in it is `auth` with authenticator data.
42
+ def data(client)
43
+ @clients_data[client]
44
+ end
36
45
 
37
46
  # Start server.
38
47
  def start
39
- @clients = []
48
+ @clients_data = {}
40
49
 
41
50
  @server = TCPServer.new(@port)
42
51
 
@@ -45,8 +54,8 @@ module IORequest
45
54
 
46
55
  # Fully stop server.
47
56
  def stop
48
- @clients.each(&:close)
49
- @clients = []
57
+ clients.each(&:close)
58
+ @clients_data.clear
50
59
 
51
60
  @server.close
52
61
  @server = nil
@@ -68,7 +77,7 @@ module IORequest
68
77
  while (socket = @server.accept)
69
78
  handle_socket(socket)
70
79
  end
71
- rescue
80
+ rescue StandardError
72
81
  stop
73
82
  end
74
83
 
@@ -76,17 +85,21 @@ module IORequest
76
85
  ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @ctx)
77
86
  ssl_socket.accept
78
87
 
79
- client = IORequest::Client.new authorizer: @authorizer
80
- begin
81
- client.open read_write: ssl_socket
82
- client.respond { |data| @requests_handler.call(data, client) }
83
- @clients << client
84
- rescue StandardError
85
- IORequest.debug "Failed to open client: #{e}"
86
- ssl_socket.close
88
+ handle_client(ssl_socket, IORequest::Client.new(authorizer: @authorizer))
89
+ rescue StandardError => e
90
+ IORequest.logger.warn "Unknown error while handling sockets: #{e}"
91
+ end
92
+
93
+ def handle_client(ssl_socket, client)
94
+ auth_data = client.open read_write: ssl_socket
95
+ client.on_request { |data| @requests_handler.call(data, client) }
96
+ @clients_data[client] = { auth: auth_data }
97
+ client.on_close do
98
+ @clients_data.select! { |c, _d| c.open? }
87
99
  end
88
100
  rescue StandardError => e
89
- IORequest.warn "Unknown error while handling sockets: #{e}"
101
+ IORequest.logger.debug "Failed to open client: #{e}"
102
+ ssl_socket.close
90
103
  end
91
104
  end
92
105
 
@@ -105,9 +118,15 @@ module IORequest
105
118
  @authorizer = authorizer
106
119
  @requests_handler = requests_handler
107
120
 
121
+ @client = nil
122
+
108
123
  initialize_ssl_context(certificate, key)
109
124
  end
110
125
 
126
+ def connected?
127
+ !@client.nil?
128
+ end
129
+
111
130
  # Connect to server.
112
131
  # @param host [String] host of server.
113
132
  # @param port [Integer] port of server.
@@ -121,9 +140,11 @@ module IORequest
121
140
  @client = IORequest::Client.new authorizer: @authorizer
122
141
  begin
123
142
  @client.open read_write: ssl_socket
124
- @client.respond(&@requests_handler)
143
+ @client.on_request(&@requests_handler)
144
+ @client.on_close do
145
+ @client = nil
146
+ end
125
147
  rescue StandardError
126
- IORequest.debug "Failed to open client: #{e}"
127
148
  ssl_socket.close
128
149
  @client = nil
129
150
  end
@@ -81,8 +81,8 @@ module IORequest
81
81
  # @param io_r [:read]
82
82
  # @return [Message]
83
83
  def self.read_from(io_r)
84
- size = io_r.read(2).unpack1('S')
85
- raise '0 size received' if size.zero?
84
+ size = io_r.read(2)&.unpack1('S') || 0
85
+ raise ZeroSizeMessageError if size.zero?
86
86
 
87
87
  json_string = io_r.read(size).unpack1("a#{size}")
88
88
  msg = JSON.parse(json_string, symbolize_names: true)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module IORequest
4
4
  # Gem version.
5
- VERSION = '2.2.0'
5
+ VERSION = '2.5.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io_request
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fizvlad
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-19 00:00:00.000000000 Z
11
+ date: 2020-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler