io_request 2.2.0 → 2.5.0

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: 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