io_request 1.2.0 → 2.0.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 +4 -4
- data/.gitignore +10 -8
- data/.rubocop.yml +37 -0
- data/.rubocop_todo.yml +7 -0
- data/Gemfile +13 -4
- data/README.md +31 -39
- data/Rakefile +31 -10
- data/bin/console +15 -14
- data/examples/simple_example.rb +12 -10
- data/io_request.gemspec +23 -21
- data/lib/io_request.rb +30 -8
- data/lib/io_request/authorizer.rb +42 -0
- data/lib/io_request/client.rb +171 -158
- data/lib/io_request/message.rb +67 -85
- data/lib/io_request/utility/multi_thread.rb +64 -0
- data/lib/io_request/utility/with_id.rb +60 -0
- data/lib/io_request/utility/with_prog_name.rb +14 -0
- data/lib/io_request/version.rb +3 -1
- metadata +25 -22
- data/Gemfile.lock +0 -29
- data/lib/io_request/logging.rb +0 -49
- data/lib/io_request/utility.rb +0 -104
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a78b709d9b064cd44737d76a24c27f0df37e9f0d5897829043e9d884287219d
|
4
|
+
data.tar.gz: a2350c480d0f9cc883526c47021505bfd511ffa12622436ac5bafbe4d050c6df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1cc3c89258eaa0d4511fdcd1dc17cae4b82061ebdb747732a8cc60467a9a39c3414a337f6f85acc4dd6768f16bc45f9fe037405e5e7aeb19f0723deeb01759a
|
7
|
+
data.tar.gz: eb6918fb66bbaec142dd88abf4c00b69d5204fb88b03f00532e52d12d8a03822275efb3cac4584177a8f9ab40bc549bc5dff09977410f3cf6f99435f594e41ad
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
require:
|
4
|
+
- rubocop-performance
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
DisplayCopNames: true
|
8
|
+
DisplayStyleGuide: true
|
9
|
+
ExtraDetails: false
|
10
|
+
TargetRubyVersion: 2.6
|
11
|
+
Exclude:
|
12
|
+
- config/**/*
|
13
|
+
- tmp/**/*
|
14
|
+
- Capfile
|
15
|
+
- Gemfile
|
16
|
+
- Rakefile
|
17
|
+
|
18
|
+
# EOL is handled by git settings
|
19
|
+
Layout/EndOfLine:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
# Just enough
|
23
|
+
Layout/LineLength:
|
24
|
+
Max: 100
|
25
|
+
|
26
|
+
# Disable some common cops for tests
|
27
|
+
Style/Documentation:
|
28
|
+
Exclude:
|
29
|
+
- test/**/*
|
30
|
+
Metrics/AbcSize:
|
31
|
+
Exclude:
|
32
|
+
- test/**/*
|
33
|
+
Metrics/MethodLength:
|
34
|
+
Max: 15 # Extended limit of lines in method
|
35
|
+
Exclude:
|
36
|
+
- test/**/*
|
37
|
+
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2020-07-15 18:13:41 UTC using RuboCop version 0.88.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
data/Gemfile
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
source
|
2
|
-
|
3
|
-
# Specify your gem's dependencies in io_request.gemspec
|
4
|
-
gemspec
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in io_request.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'rubocop', '~> 0.88.0', group: :development
|
7
|
+
gem 'rubocop-performance', '~> 1.6', group: :development, require: false
|
8
|
+
|
9
|
+
gem 'json', '~> 2.3'
|
10
|
+
|
11
|
+
gem 'timeout', '~> 0.1.0'
|
12
|
+
|
13
|
+
gem 'logger', '~> 1.4'
|
data/README.md
CHANGED
@@ -1,39 +1,31 @@
|
|
1
|
-
# IORequest
|
2
|
-
|
3
|
-
Small gem to create JSON request/response type of connection over IO
|
4
|
-
|
5
|
-
## Installation
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
```
|
10
|
-
|
11
|
-
```
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
## Contributing
|
34
|
-
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/fizvlad/io-request-rb.
|
36
|
-
|
37
|
-
## License
|
38
|
-
|
39
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
1
|
+
# IORequest
|
2
|
+
|
3
|
+
Small gem to create JSON request/response type of connection over different IO objects.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Using [bundler](https://bundler.io/):
|
8
|
+
|
9
|
+
```
|
10
|
+
$ bundle add io_request
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Documentation is available at https://www.rubydoc.info/gems/io_request
|
16
|
+
|
17
|
+
Some examples could be find in *examples* folder.
|
18
|
+
|
19
|
+
## Development
|
20
|
+
|
21
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
22
|
+
|
23
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fizvlad/io-request-rb.
|
28
|
+
|
29
|
+
## License
|
30
|
+
|
31
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,10 +1,31 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
4
|
-
Rake::TestTask.new(:test) do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.libs <<
|
7
|
-
t.test_files = FileList[
|
8
|
-
end
|
9
|
-
|
10
|
-
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << 'test'
|
6
|
+
t.libs << 'lib'
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rubocop/rake_task'
|
11
|
+
RuboCop::RakeTask.new(:rubocop) {}
|
12
|
+
namespace 'rubocop' do
|
13
|
+
desc 'Generate rubocop TODO file.'
|
14
|
+
task 'todo' do
|
15
|
+
puts `rubocop --auto-gen-config`
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
namespace 'yardoc' do
|
20
|
+
desc 'Generate documentation'
|
21
|
+
task 'generate' do
|
22
|
+
puts `yardoc lib/*`
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'List undocumented elements'
|
26
|
+
task 'undoc' do
|
27
|
+
puts `yardoc stats --list-undoc lib/*`
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
task :default => :test
|
data/bin/console
CHANGED
@@ -1,14 +1,15 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
|
4
|
-
require
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'io_request'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/examples/simple_example.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io_request'
|
2
4
|
|
3
5
|
r1, w1 = IO.pipe
|
4
6
|
r2, w2 = IO.pipe
|
@@ -9,25 +11,25 @@ client_2 = IORequest::Client.new read: r2, write: w1
|
|
9
11
|
# Use
|
10
12
|
# Set up responders
|
11
13
|
# Authorization
|
12
|
-
client_2.respond type:
|
14
|
+
client_2.respond type: 'auth' do |request|
|
13
15
|
puts "Client 2: Authorization attempt as #{request.data[:username].inspect}"
|
14
16
|
sleep 2 # Some processing
|
15
|
-
{ type:
|
17
|
+
{ type: 'auth_success' }
|
16
18
|
end
|
17
19
|
|
18
20
|
# Default
|
19
21
|
client_2.respond do |request|
|
20
22
|
puts "Client 2: #{request.data.inspect}"
|
21
|
-
{ type:
|
23
|
+
{ type: 'success' }
|
22
24
|
end
|
23
25
|
|
24
26
|
# Send requests
|
25
27
|
auth = false
|
26
28
|
auth_request = client_1.request(
|
27
|
-
data: { type:
|
29
|
+
data: { type: 'auth', username: 'mymail@example.com', password: "let's pretend password hash is here" },
|
28
30
|
sync: true
|
29
31
|
) do |response|
|
30
|
-
unless response.data[:type] ==
|
32
|
+
unless response.data[:type] == 'auth_success'
|
31
33
|
puts "Client 1: Authorization failed. Response: #{response.data.inspect}"
|
32
34
|
next
|
33
35
|
end
|
@@ -36,13 +38,13 @@ auth_request = client_1.request(
|
|
36
38
|
# Do something
|
37
39
|
end
|
38
40
|
exit unless auth
|
39
|
-
puts
|
41
|
+
puts 'Client 1: Authorized!'
|
40
42
|
|
41
43
|
message = client_1.request(
|
42
|
-
data: { type:
|
44
|
+
data: { type: 'message', message: 'Hello!' },
|
43
45
|
sync: true
|
44
|
-
) do |
|
45
|
-
puts
|
46
|
+
) do |_response|
|
47
|
+
puts 'Client 1: Message responded'
|
46
48
|
end
|
47
49
|
|
48
50
|
# Close
|
data/io_request.gemspec
CHANGED
@@ -1,37 +1,39 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require
|
5
|
+
require 'io_request/version'
|
4
6
|
|
5
7
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name =
|
8
|
+
spec.name = 'io_request'
|
7
9
|
spec.version = IORequest::VERSION
|
8
|
-
spec.authors = [
|
9
|
-
spec.email = [
|
10
|
+
spec.authors = ['Fizvlad']
|
11
|
+
spec.email = ['fizvlad@mail.ru']
|
10
12
|
|
11
|
-
spec.summary =
|
12
|
-
spec.homepage =
|
13
|
-
spec.license =
|
13
|
+
spec.summary = 'Small gem to create JSON request/response type of connection over IO object'
|
14
|
+
spec.homepage = 'https://github.com/fizvlad/io-request-rb'
|
15
|
+
spec.license = 'MIT'
|
14
16
|
|
15
|
-
spec.required_ruby_version =
|
17
|
+
spec.required_ruby_version = '>=2.6.5'
|
16
18
|
|
17
|
-
spec.metadata[
|
18
|
-
spec.metadata[
|
19
|
-
spec.metadata[
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/fizvlad/io-request-rb'
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/fizvlad/io-request-rb/releases'
|
20
22
|
|
21
23
|
# Specify which files should be added to the gem when it is released.
|
22
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
-
spec.files
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
26
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
27
|
end
|
26
|
-
spec.bindir =
|
28
|
+
spec.bindir = 'exe'
|
27
29
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
-
spec.require_paths = [
|
30
|
+
spec.require_paths = ['lib']
|
29
31
|
|
30
|
-
spec.add_development_dependency
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency
|
32
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
33
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
34
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
33
35
|
|
34
|
-
spec.add_runtime_dependency
|
35
|
-
spec.add_runtime_dependency
|
36
|
-
spec.add_runtime_dependency
|
36
|
+
spec.add_runtime_dependency 'json', '~>2.0'
|
37
|
+
spec.add_runtime_dependency 'logger', '~>1.4'
|
38
|
+
spec.add_runtime_dependency 'timeout-extensions', '~>0.1.1'
|
37
39
|
end
|
data/lib/io_request.rb
CHANGED
@@ -1,8 +1,30 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'io_request/version'
|
4
|
+
require_relative 'io_request/utility/multi_thread'
|
5
|
+
require_relative 'io_request/utility/with_id'
|
6
|
+
require_relative 'io_request/utility/with_prog_name'
|
7
|
+
|
8
|
+
require_relative 'io_request/authorizer'
|
9
|
+
require_relative 'io_request/message'
|
10
|
+
require_relative 'io_request/client'
|
11
|
+
|
12
|
+
require 'logger'
|
13
|
+
|
14
|
+
# Main module.
|
15
|
+
module IORequest
|
16
|
+
# @return [Logger]
|
17
|
+
def self.logger
|
18
|
+
@@logger ||= Logger.new( # rubocop:disable Style/ClassVars
|
19
|
+
STDOUT,
|
20
|
+
formatter: proc do |severity, datetime, progname, msg|
|
21
|
+
"[#{datetime}] #{severity} - #{progname}:\t #{msg}\n"
|
22
|
+
end
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param new_logger [Logger]
|
27
|
+
def self.logger=(new_logger)
|
28
|
+
@@logger = new_logger # rubocop:disable Style/ClassVars
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IORequest
|
4
|
+
# Class to authorize client connection.
|
5
|
+
class Authorizer
|
6
|
+
# @yieldparam io_r [IO] input stream.
|
7
|
+
# @yieldparam io_w [IO] output stream.
|
8
|
+
# @yieldreturn [Object, nil] if `nil` is returned, authorization will be
|
9
|
+
# considered as failed one. Otherwise data will be saved into `data`.
|
10
|
+
def initialize(&block)
|
11
|
+
@block = block
|
12
|
+
@data = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Object] literally any non-nil data from block.
|
16
|
+
attr_reader :data
|
17
|
+
|
18
|
+
# @return [Boolean] authorization status.
|
19
|
+
def authorize(io_r, io_w)
|
20
|
+
@data = nil
|
21
|
+
@data = @block.call(io_r, io_w)
|
22
|
+
!@data.nil?
|
23
|
+
rescue StandardError => e
|
24
|
+
IORequest.logger.error(e.full_message)
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# No authorization.
|
30
|
+
def Authorizer.empty
|
31
|
+
Authorizer.new { |_io_r, _io_w| true }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Secret key authorization.
|
35
|
+
def Authorizer.by_secret_key(key)
|
36
|
+
Authorizer.new do |io_r, io_w|
|
37
|
+
io_w.write(key)
|
38
|
+
other = io_r.read(key.size)
|
39
|
+
key == other ? other : nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/io_request/client.rb
CHANGED
@@ -1,193 +1,206 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
require 'json'
|
4
5
|
|
5
6
|
module IORequest
|
6
7
|
# Connection client.
|
8
|
+
#
|
9
|
+
# General scheme:
|
10
|
+
# Client 1 Client 2
|
11
|
+
# | |
|
12
|
+
# ( Authorization ) See `Authorizer` class. Error in authorization should close
|
13
|
+
# | | connection
|
14
|
+
# | |
|
15
|
+
# [ Data transition loop ] Loop runs until someone sends 0 sized data. Then everyone
|
16
|
+
# | | should close connection. Any R/W errors should also finish the
|
17
|
+
# | | loop
|
18
|
+
# | |
|
19
|
+
# |-> uint(2 bytes) ->| Specifies size of following JSON string
|
20
|
+
# |-> Mesage as JSON ->| Message itself. It should contain its `type`, `id` and some
|
21
|
+
# | | data hash
|
22
|
+
# | |
|
23
|
+
# | (Message handling) See `Handler` class
|
24
|
+
# | |
|
25
|
+
# |<- uint(2 bytes) <-|
|
26
|
+
# |<- Mesage as JSON <-|
|
7
27
|
class Client
|
8
28
|
include Utility::WithProgName
|
9
29
|
include Utility::MultiThread
|
10
30
|
|
11
|
-
# Initialize new client
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@
|
17
|
-
@
|
18
|
-
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
|
23
|
-
@receive_thread = Thread.new { receive_loop }
|
24
|
-
IORequest.debug("New IORequest client initialized", prog_name)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Send request.
|
28
|
-
#
|
29
|
-
# Optional block can be provided. It will be called when response received.
|
30
|
-
#
|
31
|
-
# @option options [Hash] data data to send.
|
32
|
-
# @option options [Boolean] sync whether to join request after sending.
|
33
|
-
# @option options [Integer, Float] timeout timeout for {Request#join}.
|
34
|
-
#
|
35
|
-
# @yieldparam request [Response] response for request.
|
36
|
-
#
|
37
|
-
# @return [Request]
|
38
|
-
def request(data: {}, sync: false, timeout: nil, &block)
|
39
|
-
req = Request.new(data)
|
40
|
-
@out_requests[req] = block
|
41
|
-
IORequest.debug("Sending request ##{req.id}", prog_name)
|
42
|
-
send(req.to_hash)
|
43
|
-
req.join(timeout) if sync
|
44
|
-
req
|
45
|
-
end
|
46
|
-
|
47
|
-
# Setup block for answering incoming requests.
|
48
|
-
#
|
49
|
-
# @param subdata [Hash] provided block will be called only if received data
|
50
|
-
# includes this hash.
|
51
|
-
#
|
52
|
-
# @yieldparam request [Request] incoming request.
|
53
|
-
# @yieldreturn [Hash] data to be sent in response.
|
54
|
-
#
|
55
|
-
# @return [nil]
|
56
|
-
def respond(subdata = {}, &block)
|
57
|
-
@responders << [subdata, block]
|
58
|
-
nil
|
31
|
+
# Initialize new client.
|
32
|
+
def initialize(authorizer: Authorizer.empty)
|
33
|
+
@open = false
|
34
|
+
@authorizer = authorizer
|
35
|
+
|
36
|
+
@mutex_r = Mutex.new
|
37
|
+
@mutex_w = Mutex.new
|
38
|
+
|
39
|
+
@responses = {}
|
40
|
+
@responses_access_mutex = Mutex.new
|
41
|
+
@responses_access_cv = ConditionVariable.new
|
59
42
|
end
|
60
43
|
|
61
|
-
|
44
|
+
# Start new client connection.
|
45
|
+
# @param r [IO] object to read from.
|
46
|
+
# @param w [IO] object to write to.
|
47
|
+
# @param rw [IO] read-write object (replaces `r` and `w` arguments).
|
48
|
+
def open(read: nil, write: nil, read_write: nil)
|
49
|
+
@io_r = read_write || read
|
50
|
+
@io_w = read_write || write
|
62
51
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
52
|
+
IORequest.logger.debug(prog_name) { 'Starting connection' }
|
53
|
+
|
54
|
+
authorization
|
55
|
+
@open = true
|
56
|
+
@data_transition_thread = in_thread(name: 'connection') { data_transition_loop }
|
57
|
+
end
|
58
|
+
|
59
|
+
def open?
|
60
|
+
@open
|
61
|
+
end
|
62
|
+
|
63
|
+
# Close connection.
|
64
|
+
def close
|
65
|
+
close_internal
|
66
|
+
join_threads
|
67
|
+
@open = false
|
68
|
+
end
|
69
|
+
|
70
|
+
# @yieldparam [Hash]
|
71
|
+
# @yieldreturn [Hash]
|
72
|
+
def respond(&block)
|
73
|
+
IORequest.logger.debug(prog_name) { 'Saved responder block' }
|
74
|
+
@responder = block
|
75
|
+
end
|
76
|
+
|
77
|
+
# If callback block is provided, request will be sent asynchroniously.
|
78
|
+
# @param data [Hash]
|
79
|
+
def request(data = {}, &callback)
|
80
|
+
message = Message.new(data, type: :request)
|
81
|
+
|
82
|
+
if block_given?
|
83
|
+
# Async execution of request
|
84
|
+
in_thread(callback, name: 'requesting') do |cb|
|
85
|
+
cb.call(send_request_and_wait_for_response(message).data)
|
95
86
|
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
100
|
-
nil
|
101
|
-
end
|
102
|
-
# Handle incoming response.
|
103
|
-
def handle_in_response(res)
|
104
|
-
req_id = res.request.to_i
|
105
|
-
req = @out_requests.keys.find { |r| r.id == req_id }
|
106
|
-
unless req
|
107
|
-
IORequest.warn("Request ##{req_id} not found", prog_name)
|
108
|
-
return
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
send_request_and_wait_for_response(message).data
|
109
90
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_reader :authorizer
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def close_internal
|
98
|
+
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' }
|
122
103
|
end
|
104
|
+
stop_data_transition
|
105
|
+
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
|
+
@data_transition_thread = nil
|
114
|
+
end
|
115
|
+
|
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 }
|
123
120
|
end
|
124
121
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
result = block
|
131
|
-
break
|
122
|
+
def authorization
|
123
|
+
auth_successful = @mutex_r.synchronize do
|
124
|
+
@mutex_w.synchronize do
|
125
|
+
IORequest.logger.debug(prog_name) { 'Authorizing new client' }
|
126
|
+
@authorizer.authorize(@io_r, @io_w)
|
132
127
|
end
|
133
128
|
end
|
129
|
+
unless auth_successful
|
130
|
+
IORequest.logger.debug(prog_name) { 'Authorization failed' }
|
131
|
+
raise 'Authorization failed'
|
132
|
+
end
|
134
133
|
|
135
|
-
|
134
|
+
IORequest.logger.debug(prog_name) { "New client authorized with data #{@authorizer.data}" }
|
136
135
|
end
|
137
136
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
137
|
+
def data_transition_loop
|
138
|
+
IORequest.logger.debug(prog_name) { 'Starting data transition loop' }
|
139
|
+
loop do
|
140
|
+
data_transition_iteration
|
141
|
+
rescue StandardError => e
|
142
|
+
IORequest.logger.debug(prog_name) { "Data transition iteration failed: #{e}" }
|
143
|
+
break
|
144
|
+
end
|
145
|
+
close_internal
|
144
146
|
end
|
145
147
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
receive_raw
|
148
|
+
def data_transition_iteration
|
149
|
+
message = @mutex_r.synchronize { Message.read_from(@io_r) }
|
150
|
+
IORequest.logger.debug(prog_name) { "Received message: #{message}" }
|
151
|
+
if message.request?
|
152
|
+
in_thread(name: 'responding') { handle_request(message) }
|
153
|
+
else
|
154
|
+
handle_response(message)
|
154
155
|
end
|
155
|
-
return nil if str.nil?
|
156
|
-
string_to_data(decode(str))
|
157
|
-
rescue Timeout::Error
|
158
|
-
nil
|
159
|
-
rescue IOError
|
160
|
-
nil
|
161
|
-
rescue Exception => e
|
162
|
-
IORequest.warn "Exception of #{e.class} encountered while trying to receive message. Suggesting IO was closed. Full trace: #{e.full_message}", prog_name
|
163
|
-
nil
|
164
156
|
end
|
165
157
|
|
166
|
-
|
167
|
-
|
168
|
-
|
158
|
+
def handle_request(message)
|
159
|
+
data = @responder&.call(message.data) || {}
|
160
|
+
response = Message.new(data, type: :response, to: message.id)
|
161
|
+
send_response(response)
|
169
162
|
end
|
170
|
-
|
171
|
-
def
|
172
|
-
@
|
163
|
+
|
164
|
+
def handle_response(message)
|
165
|
+
@responses_access_mutex.synchronize do
|
166
|
+
@responses[message.to.to_s] = message
|
167
|
+
@responses_access_cv.broadcast
|
168
|
+
end
|
173
169
|
end
|
174
170
|
|
175
|
-
|
176
|
-
|
177
|
-
|
171
|
+
def send_response(response)
|
172
|
+
@mutex_w.synchronize do
|
173
|
+
IORequest.logger.debug(prog_name) { "Sending response: #{response}" }
|
174
|
+
response.write_to(@io_w)
|
175
|
+
end
|
178
176
|
end
|
179
|
-
|
180
|
-
def
|
181
|
-
|
177
|
+
|
178
|
+
def send_zero_size_request
|
179
|
+
@mutex_w.synchronize do
|
180
|
+
IORequest.logger.debug(prog_name) { 'Sending zero size message' }
|
181
|
+
@io_w.write([0].pack('S'))
|
182
|
+
end
|
182
183
|
end
|
183
184
|
|
184
|
-
|
185
|
-
|
186
|
-
|
185
|
+
def send_request_and_wait_for_response(request)
|
186
|
+
@mutex_w.synchronize do
|
187
|
+
IORequest.logger.debug(prog_name) { "Sending message: #{request}" }
|
188
|
+
request.write_to(@io_w)
|
189
|
+
end
|
190
|
+
wait_for_response(request)
|
187
191
|
end
|
188
|
-
|
189
|
-
def
|
190
|
-
|
192
|
+
|
193
|
+
def wait_for_response(request)
|
194
|
+
IORequest.logger.debug(prog_name) { "Waiting for response for #{request}" }
|
195
|
+
@responses_access_mutex.synchronize do
|
196
|
+
response = nil
|
197
|
+
until response
|
198
|
+
@responses_access_cv.wait(@responses_access_mutex)
|
199
|
+
response = @responses[request.id.to_s]
|
200
|
+
end
|
201
|
+
IORequest.logger.debug(prog_name) { "Found response: #{response}" }
|
202
|
+
response
|
203
|
+
end
|
191
204
|
end
|
192
205
|
end
|
193
206
|
end
|