ccrpc 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 93b1358631d8f55fb408daa3644ff3966d518fdcc0bac693468e679d8ea6a144
4
+ data.tar.gz: 18c6b183a8904c7ad65ea588ec557be7f063107e3d8c191bf54f1c52fdf16915
5
+ SHA512:
6
+ metadata.gz: 328200b7e1cfbd503abb5c6503d9efc8851f76c7f964bbeb9ee61986d2a7aa0f89ed318df434bcc14cff28702d1919267bf11516900a26022a2dc9744a95ac88
7
+ data.tar.gz: 22b0d11ce99abedc64c8e46216025ecd753966f7114ef5351484c0b3c0a2273517a82cc524b1923dcf3dd082b3b81572c388759813d476cde13c1fd24371d95e
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,36 @@
1
+ name: CI
2
+ on:
3
+ workflow_dispatch:
4
+ schedule:
5
+ - cron: "0 6 * * 3" # At 05:00 on Wednesday # https://crontab.guru/#0_5_*_*_3
6
+ push:
7
+ branches:
8
+ - master
9
+ tags:
10
+ - "*.*.*"
11
+ pull_request:
12
+ types: [opened, synchronize]
13
+ branches:
14
+ - "*"
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ specs:
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ os: [ ubuntu-latest, macos-latest, windows-latest ]
24
+ ruby: [ 2.7, 3.0, 3.1, 3.2, 3.3, ruby-head, truffleruby-head, jruby-head ]
25
+ exclude:
26
+ - os: windows-latest
27
+ ruby: truffleruby-head
28
+ runs-on: ${{ matrix.os }}
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: ruby/setup-ruby@v1
32
+ with:
33
+ ruby-version: ${{ matrix.ruby }}
34
+
35
+ - run: bundle install
36
+ - run: bundle exec rake test TESTOPTS=-v
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,15 @@
1
+ .job_template: &job_definition
2
+ before_script:
3
+ - ruby -v
4
+ - bundle install -j$(nproc)
5
+
6
+ script:
7
+ - bundle exec rake test
8
+
9
+ test-2.7:
10
+ <<: *job_definition
11
+ image: comcard/ruby:2.7
12
+
13
+ test-latest:
14
+ <<: *job_definition
15
+ image: comcard/ruby:latest
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.1
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ccrpc.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+ gem "minitest", "~> 5.0"
8
+ gem "yard"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Lars Kanis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Ccrpc - A minimalistic RPC library for Ruby
2
+
3
+ Features:
4
+ * Simple human readable wire protocol
5
+ * Works on arbitrary ruby IO objects (Pipes, Sockets, STDIN, STDOUT) even Windows CR/LF converting IOs
6
+ * No object definitions - only plain string transfers (so no issues with undefined classes or garbage collection like in DRb)
7
+ * Each call transfers a function name and a list of parameters in form of a Hash<String=>String>
8
+ * Each response equally transfers a list of parameters
9
+ * Similar to closures, it's possible to respond to a particular call as a call_back
10
+ * Fully asynchronous, either by use of multiple threads or by using lazy_answers, so that arbitrary calls in both directions can be mixed simultaneously without blocking each other
11
+ * Fully thread safe, but doesn't use additional internal threads
12
+ * Each call_back arrives in the thread of the caller
13
+ * Only dedicated functions can be called (not arbitrary as in DRb)
14
+ * No dependencies
15
+
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'ccrpc'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ $ bundle install
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install ccrpc
32
+
33
+ ## Usage
34
+
35
+ Fork a subprocess and communicate with it through pipes
36
+ ```ruby
37
+ require 'ccrpc'
38
+
39
+ ar, aw = IO.pipe # pipe to send data to the forking process
40
+ br, bw = IO.pipe # pipe to send data to the forked process
41
+ fork do
42
+ ar.close; bw.close
43
+ # Create the receiver side of the connection
44
+ rpc = Ccrpc::RpcConnection.new(br, aw)
45
+ # Wait for calls
46
+ rpc.call do |call|
47
+ # Print the received call data
48
+ pp func: call.func, params: call.params # => {:func=>:hello, :params=>{"who"=>"world"}}
49
+ # The answer of the subprocess
50
+ {my_answer: 'hello back'}
51
+ end
52
+ end
53
+ br.close; aw.close
54
+
55
+ # Create the caller side of the connection
56
+ rpc = Ccrpc::RpcConnection.new(ar, bw)
57
+ # Call function "hello" with param {"who" => "world"}
58
+ pp rpc.call(:hello, who: 'world') # => {"my_answer"=>"hello back"}
59
+ ```
60
+
61
+ Communicate with a subprocess through STDIN and STDOUT.
62
+ Since STDIN and STDOUT are used for the RPC connection, it's best zu redirect STDOUT to STDERR after the RPC object is created.
63
+ This avoids clashes between "p" calls and the RPC protocol.
64
+
65
+ The following example invokes the call in the opposite direction, from the subprocess to the main process.
66
+
67
+ ```ruby
68
+ require 'ccrpc'
69
+
70
+ # The code of the subprocess:
71
+ code = <<-EOT
72
+ require 'ccrpc'
73
+ # Create the receiver side of the connection
74
+ # Use a copy of STDOUT because...
75
+ rpc = Ccrpc::RpcConnection.new(STDIN, STDOUT.dup)
76
+ # .. STDOUT is now redirected to STDERR, so that pp prints to STDERR
77
+ STDOUT.reopen(STDERR)
78
+ # Call function "hello" with param {"who" => "world"}
79
+ pp rpc.call(:hello, who: 'world') # => {"my_answer"=>"hello back"}
80
+ EOT
81
+
82
+ # Write the code to a temp file
83
+ tf = Tempfile.new('rpc')
84
+ tf.write(code)
85
+ tf.flush
86
+ # Execute the temp file in a subprocess
87
+ io = IO.popen(['ruby', tf.path], "w+")
88
+
89
+ # Create the caller side of the connection
90
+ rpc = Ccrpc::RpcConnection.new(io, io)
91
+ # Wait for calls
92
+ rpc.call do |call|
93
+ # Print the received call data to STDERR
94
+ pp func: call.func, params: call.params # => {:func=>:hello, :params=>{"who"=>"world"}}
95
+ # The answer of the subprocess
96
+ {my_answer: 'hello back'}
97
+ end
98
+ # call returns when the IO is closed by the subprocess
99
+ ```
100
+
101
+
102
+ ## Development
103
+
104
+ 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.
105
+
106
+ 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).
107
+
108
+ ## Contributing
109
+
110
+ Bug reports and pull requests are welcome on GitHub at https://github.com/larskanis/ccrpc.
111
+
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,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
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ccrpc"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/ccrpc.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ require_relative 'lib/ccrpc/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "ccrpc"
5
+ spec.version = Ccrpc::VERSION
6
+ spec.authors = ["Lars Kanis"]
7
+ spec.email = ["lars@greiz-reinsdorf.de"]
8
+
9
+ spec.summary = %q{Simple bidirectional RPC protocol}
10
+ spec.description = %q{Simple bidirectional and thread safe RPC protocol. Works on arbitrary Ruby IO objects.}
11
+ spec.homepage = "https://github.com/larskanis/ccrpc"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+
4
+ module Ccrpc
5
+ module Escape
6
+ def self.escape(data)
7
+ data = data.b if data.frozen? || data.encoding != Encoding::BINARY
8
+ data.gsub(/([\a\r\n\t\\])/n){ "\\x" + $1.unpack("H2")[0] }
9
+ end
10
+
11
+ def self.unescape(data)
12
+ data.b.gsub(/\\x([0-9a-fA-F]{2,2})/n){ [$1].pack("H*") }.force_encoding(Encoding::UTF_8)
13
+ end
14
+ end
15
+ end
data/lib/ccrpc/lazy.rb ADDED
@@ -0,0 +1,135 @@
1
+ # -*- frozen_string_literal: true -*-
2
+ # = lazy.rb -- Lazy evaluation in Ruby
3
+ #
4
+ # Author:: MenTaLguY
5
+ #
6
+ # Copyright 2005-2006 MenTaLguY <mental@rydia.net>
7
+ #
8
+ # You may redistribute it and/or modify it under the same terms as Ruby.
9
+ #
10
+
11
+ require 'thread'
12
+
13
+ module Ccrpc
14
+ # Raised when a forced computation diverges (e.g. if it tries to directly
15
+ # use its own result)
16
+ #
17
+ class DivergenceError < RuntimeError
18
+ def initialize( message="Computation diverges" )
19
+ super( message )
20
+ end
21
+ end
22
+
23
+ # Wraps an exception raised by a lazy computation.
24
+ #
25
+ # The reason we wrap such exceptions in LazyException is that they need to
26
+ # be distinguishable from similar exceptions which might normally be raised
27
+ # by whatever strict code we happen to be in at the time.
28
+ #
29
+ class LazyException < DivergenceError
30
+ # the original exception
31
+ attr_reader :reason
32
+
33
+ def initialize( reason )
34
+ @reason = reason
35
+ super( "#{ reason } (#{ reason.class })" )
36
+ set_backtrace( reason.backtrace.dup ) if reason
37
+ end
38
+ end
39
+
40
+ # A promise is just a magic object that springs to life when it is actually
41
+ # used for the first time, running the provided block and assuming the
42
+ # identity of the resulting object.
43
+ #
44
+ # This impersonation isn't perfect -- a promise wrapping nil or false will
45
+ # still be considered true by Ruby -- but it's good enough for most purposes.
46
+ # If you do need to unwrap the result object for some reason (e.g. for truth
47
+ # testing or for simple efficiency), you may do so via Kernel.demand.
48
+ #
49
+ # Formally, a promise is a placeholder for the result of a deferred computation.
50
+ #
51
+ class Promise
52
+ alias __class__ class #:nodoc:
53
+ instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_variable|^frozen\?$/ }
54
+
55
+ def initialize( &computation ) #:nodoc:
56
+ @mutex = Mutex.new
57
+ @computation = computation
58
+ @exception = nil
59
+ end
60
+
61
+ # create this once here, rather than creating a proc object for
62
+ # every evaluation
63
+ DIVERGES = lambda {|promise| raise DivergenceError.new } #:nodoc:
64
+ def DIVERGES.inspect #:nodoc:
65
+ "DIVERGES"
66
+ end
67
+
68
+ def __result__ #:nodoc:
69
+ @mutex.synchronize do
70
+ if @computation
71
+ raise @exception if @exception
72
+
73
+ computation = @computation
74
+ @computation = DIVERGES # trap divergence due to over-eager recursion
75
+
76
+ begin
77
+ @result = Promise.demand( computation.call( self ) )
78
+ @computation = nil
79
+ rescue DivergenceError => exception
80
+ @exception = exception
81
+ raise
82
+ rescue Exception => exception
83
+ # handle exceptions
84
+ @exception = LazyException.new( exception )
85
+ raise @exception
86
+ end
87
+ end
88
+
89
+ @result
90
+ end
91
+ end
92
+
93
+ def inspect #:nodoc:
94
+ @mutex.synchronize do
95
+ if @computation
96
+ "#<#{ __class__ } computation=#{ @computation.inspect }>"
97
+ else
98
+ @result.inspect
99
+ end
100
+ end
101
+ end
102
+
103
+ def marshal_dump
104
+ __result__
105
+ Marshal.dump( [ @exception, @result ] )
106
+ end
107
+
108
+ def marshal_load( str )
109
+ @mutex = Mutex.new
110
+ ( @exception, @result ) = Marshal.load( str )
111
+ @computation = DIVERGES if @exception
112
+ end
113
+
114
+ def respond_to?( message, include_all=false ) #:nodoc:
115
+ message = message.to_sym
116
+ message == :__result__ or
117
+ message == :inspect or
118
+ message == :marshal_dump or
119
+ message == :marshal_load or
120
+ __result__.respond_to?(message, include_all)
121
+ end
122
+
123
+ ruby2_keywords def method_missing( *args, &block ) #:nodoc:
124
+ __result__.__send__( *args, &block )
125
+ end
126
+
127
+ def self.demand( promise )
128
+ if promise.respond_to? :__result__
129
+ promise.__result__
130
+ else # not really a promise
131
+ promise
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,327 @@
1
+ # -*- coding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ ###########################################################################
4
+ # Copyright (C) 2015 to 2024 by Lars Kanis
5
+ # <lars@greiz-reinsdorf.de>
6
+ #
7
+ # Copyright: See COPYING file that comes with this distribution
8
+ ###########################################################################
9
+ #
10
+ # RPC Connection
11
+ #
12
+
13
+ require 'thread'
14
+ require_relative 'escape'
15
+
16
+ module Ccrpc
17
+ class RpcConnection
18
+ class InvalidResponse < RuntimeError
19
+ end
20
+ class NoCallbackDefined < RuntimeError
21
+ end
22
+ class CallAlreadyReturned < NoCallbackDefined
23
+ end
24
+ class DoubleResultError < RuntimeError
25
+ end
26
+ class ConnectionDetached < RuntimeError
27
+ end
28
+ class ReceiverAlreadyDefined < RuntimeError
29
+ end
30
+
31
+ # The context of a received call.
32
+ class Call
33
+ # @return [RpcConnection] The used connection.
34
+ attr_reader :conn
35
+ # @return [String] Called function
36
+ attr_reader :func
37
+ # @return [Hash{String => String}] List of parameters passed with the call.
38
+ attr_reader :params
39
+ attr_reader :id
40
+ # @return [Hash{String => String}] List of parameters send back to the called.
41
+ attr_reader :answer
42
+
43
+ # @private
44
+ def initialize(conn, func, params={}, id)
45
+ @conn = conn
46
+ @func = func
47
+ @params = params
48
+ @id = id
49
+ @answer = nil
50
+ end
51
+
52
+ # Send the answer back to the caller.
53
+ #
54
+ # @param [Hash{String, Symbol => String, Symbol}] value The answer parameters to be sent to the caller.
55
+ def answer=(value)
56
+ raise DoubleAnswerError, "More than one answer to #{self.inspect}" if @answer
57
+ @answer = value
58
+ conn.send(:send_answer, value, id)
59
+ end
60
+
61
+ # Send a dedicated callback to the caller's block.
62
+ #
63
+ # If {RpcConnection#call} is called with both function name and block, then it's possible to call back to this dedicated block through {#call_back} .
64
+ # The library ensures, that the callback ends up in the corresponding call block and in the same thread as the caller, even if there are multiple simultaneous calls are running at the same time in different threads or by using +lazy_answers+ .
65
+ #
66
+ # @param func [String, Symbol] The RPC function to be called on the other side.
67
+ # The other side must wait for calls through {#call} with function name and with a block.
68
+ # @param params [Hash{Symbol, String => Symbol, String}] Optional parameters passed with the RPC call.
69
+ # They can be retrieved through {Call#params} on the receiving side.
70
+ #
71
+ # Yielded parameters and returned objects are described in {RpcConnection#call}.
72
+ def call_back(func, params={}, &block)
73
+ raise CallAlreadyReturned, "Callback is no longer possible since the call already returned #{self.inspect}" if @answer
74
+ conn.send(:call_intern, func, params, @id, &block)
75
+ end
76
+ end
77
+
78
+ CallbackReceiver = Struct.new :meth, :callbacks
79
+
80
+ attr_accessor :read_io
81
+ attr_accessor :write_io
82
+
83
+ # Create a RPC connection
84
+ #
85
+ # @param [IO] read_io readable IO object for reception of data
86
+ # @param [IO] write_io writable IO object for transmission of data
87
+ # @param [Boolean] lazy_answers Enable or disable lazy results. See {#call} for more description.
88
+ def initialize(read_io, write_io, lazy_answers: false)
89
+ super()
90
+
91
+ @read_io = read_io
92
+ @write_io = write_io
93
+ if lazy_answers
94
+ require 'ccrpc/lazy'
95
+ alias maybe_lazy do_lazy
96
+ else
97
+ alias maybe_lazy dont_lazy
98
+ end
99
+
100
+ if @write_io.respond_to?(:setsockopt)
101
+ @write_io.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
102
+ end
103
+
104
+ # A random number as start call ID is not technically required, but makes transferred data more readable.
105
+ @id = rand(1000)
106
+ @id_mutex = Mutex.new
107
+ @read_mutex = Mutex.new
108
+ @write_mutex = Mutex.new
109
+ @answers = {}
110
+ @receivers = {}
111
+ @answers_mutex = Mutex.new
112
+ @new_answer = ConditionVariable.new
113
+
114
+ @read_enum = Enumerator.new do |y|
115
+ begin
116
+ while @read_enum
117
+ l = @read_io.gets&.force_encoding(Encoding::BINARY)
118
+ break if l.nil?
119
+ y << l
120
+ end
121
+ rescue => err
122
+ y << err
123
+ end
124
+ end
125
+ end
126
+
127
+ private def do_lazy(&block)
128
+ Promise.new(&block)
129
+ end
130
+ private def dont_lazy
131
+ yield
132
+ end
133
+
134
+ # Disable reception of data from the read_io object.
135
+ #
136
+ # This function doesn't close the IO objects.
137
+ # A waiting reception is not aborted by this call.
138
+ # It can be aborted by calling IO#close on the underlying read_io and write_io objects.
139
+ def detach
140
+ @read_enum = nil
141
+ end
142
+
143
+ # Do a RPC call and/or wait for a RPC call from the other side.
144
+ #
145
+ # {#call} must be called with either a function name (and optional parameters) or with a block or with both.
146
+ # If {#call} is called with a function name, the block on the other side of the RPC connection is called with that function name.
147
+ # If {#call} is called with a block only, than it receives these kind of calls, which are called anonymous callbacks.
148
+ # If {#call} is called with a function name and a block, then the RPC function on the other side is called and it is possible to call back to this dedicated block by invoking {Call#call_back} .
149
+ #
150
+ # @param func [String, Symbol] The RPC function to be called on the other side.
151
+ # The other side must wait for calls through {#call} without arguments but with a block.
152
+ # @param params [Hash{Symbol, String => Symbol, String}] Optional parameters passed with the RPC call.
153
+ # They can be retrieved through {Call#params} on the receiving side.
154
+ #
155
+ # @yieldparam [Ccrpc::Call] block The context of the received call.
156
+ # @yieldreturn [Hash{String, Symbol => String, Symbol}] The answer parameters to be sent back to the caller.
157
+ # @yieldreturn [Array<Hash>] Two element array with the answer parameters as the first element and +true+ as the second.
158
+ # By this answer type the answer is sent to other side but the reception of further calls or callbacks is stopped subsequently and the local corresponding {#call} method returns with +nil+.
159
+ #
160
+ # @return [Hash] Received answer parameters.
161
+ # @return [Promise] Received answer parameters enveloped by a Promise.
162
+ # This type of answers can be enabled by +RpcConnection#new(lazy_answers: true)+
163
+ # The Promise object is returned as soon as the RPC call is sent, but before waiting for the corresponding answer.
164
+ # This way several calls can be send in parallel without using threads.
165
+ # As soon as a method is called on the Promise object, this method is blocked until the RPC answer was received.
166
+ # The Promise object then behaves like a Hash object.
167
+ # @return [NilClass] Waiting for further answers was stopped gracefully by either returning +[hash, true]+ from the block or because the connection was closed.
168
+ def call(func=nil, params={}, &block)
169
+ call_intern(func, params, &block)
170
+ end
171
+
172
+ protected
173
+
174
+ def call_intern(func, params={}, recv_id=nil, &block)
175
+ id = next_id if func
176
+
177
+ @answers_mutex.synchronize do
178
+ @receivers[id] = CallbackReceiver.new(block_given? ? nil : caller[3], [])
179
+ end
180
+
181
+ send_call(func, params, id, recv_id) if func
182
+
183
+ pr = proc do
184
+ @answers_mutex.synchronize do
185
+ res = loop do
186
+ # Is a callback pending for this thread?
187
+ if cb=@receivers[id].callbacks.shift
188
+ @answers_mutex.unlock
189
+ begin
190
+ rets, exit = yield(cb)
191
+ if rets
192
+ cb.answer = rets
193
+ end
194
+ break if exit
195
+ ensure
196
+ @answers_mutex.lock
197
+ end
198
+
199
+ # Is a call return pending for this thread?
200
+ elsif a=@answers.delete(id)
201
+ break a
202
+
203
+ # Unless some other thread is already reading from the read_io, do it now
204
+ elsif @read_mutex.try_lock
205
+ @answers_mutex.unlock
206
+ begin
207
+ break if receive_answers
208
+ ensure
209
+ @read_mutex.unlock
210
+ @answers_mutex.lock
211
+ # Send signal possibly again to prevent deadlock if another thread started waiting before we re-locked the @answers_mutex
212
+ @new_answer.signal
213
+ end
214
+
215
+ # Wait for signal from other thread about a new call return or callback was received
216
+ else
217
+ @new_answer.wait(@answers_mutex)
218
+ end
219
+ end
220
+
221
+ @receivers.delete(id)
222
+
223
+ res
224
+ end
225
+ end
226
+ func ? maybe_lazy(&pr) : pr.call
227
+ end
228
+
229
+ def next_id
230
+ @id_mutex.synchronize do
231
+ @id = (@id + 1) & 0xffffffff
232
+ end
233
+ end
234
+
235
+ def send_call(func, params, id, recv_id=nil)
236
+ to_send = String.new
237
+ @write_mutex.synchronize do
238
+ params.reject{|k,v| v.nil? }.each do |key, value|
239
+ to_send << Escape.escape(key.to_s) << "\t" <<
240
+ Escape.escape(value.to_s) << "\n"
241
+ if to_send.bytesize > 9999
242
+ @write_io.write to_send
243
+ to_send = String.new
244
+ end
245
+ end
246
+ to_send << Escape.escape(func.to_s) << "\a#{id}"
247
+ to_send << "\a#{recv_id}" if recv_id
248
+ @write_io.write(to_send << "\n")
249
+ end
250
+ @write_io.flush
251
+ after_write
252
+ end
253
+
254
+ def send_answer(answer, id)
255
+ to_send = String.new
256
+ @write_mutex.synchronize do
257
+ answer.reject{|k,v| v.nil? }.each do |key, value|
258
+ to_send << Escape.escape(key.to_s) << "\t" <<
259
+ Escape.escape(value.to_s) << "\n"
260
+ if to_send.bytesize > 9999
261
+ @write_io.write to_send
262
+ to_send = String.new
263
+ end
264
+ end
265
+ to_send << "\a#{id}" if id
266
+ @write_io.write(to_send << "\n")
267
+ end
268
+ @write_io.flush
269
+ after_write
270
+ end
271
+
272
+ def receive_answers
273
+ rets = {}
274
+ (@read_enum || raise(ConnectionDetached, "connection already detached")).each do |l|
275
+ case l
276
+ when Exception
277
+ raise l
278
+ when /\A([^\t\a\n]+)\t(.*?)\r?\n\z/mn
279
+ # received key/value pair used for either callback parameters or return values
280
+ rets[Escape.unescape($1).force_encoding(Encoding::UTF_8)] ||= Escape.unescape($2.force_encoding(Encoding::UTF_8))
281
+
282
+ when /\A([^\t\a\n]+)(?:\a(\d+))?(?:\a(\d+))?\r?\n\z/mn
283
+ # received callback
284
+ cbfunc, id, recv_id = $1, $2&.to_i, $3&.to_i
285
+
286
+ callback = Call.new(self, Escape.unescape(cbfunc.force_encoding(Encoding::UTF_8)).to_sym, rets, id)
287
+
288
+ @answers_mutex.synchronize do
289
+ receiver = @receivers[recv_id]
290
+
291
+ if !receiver
292
+ if recv_id
293
+ raise NoCallbackDefined, "call_back to #{cbfunc.inspect} was received, but corresponding call returned already"
294
+ else
295
+ raise NoCallbackDefined, "call to #{cbfunc.inspect} was received, but there is no #{self.class}#call running"
296
+ end
297
+ elsif meth=receiver.meth
298
+ raise NoCallbackDefined, "call_back to #{cbfunc.inspect} was received, but corresponding call was called without a block in #{meth}"
299
+ end
300
+
301
+ receiver.callbacks << callback
302
+ @new_answer.broadcast
303
+ end
304
+ return
305
+
306
+ when /\A\a(\d+)\r?\n\z/mn
307
+ # received return event
308
+ id = $1.to_i
309
+ @answers_mutex.synchronize do
310
+ @answers[id] = rets
311
+ @new_answer.broadcast
312
+ end
313
+ return
314
+
315
+ else
316
+ raise InvalidResponse, "invalid response #{l.inspect}"
317
+ end
318
+ end
319
+ detach
320
+ return true
321
+ end
322
+
323
+ # Can be overwritten by subclasses to make use of idle time while waiting for answers.
324
+ def after_write
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,3 @@
1
+ module Ccrpc
2
+ VERSION = "0.3.1"
3
+ end
data/lib/ccrpc.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Ccrpc
2
+ autoload :RpcConnection, "ccrpc/rpc_connection"
3
+ autoload :Escape, "ccrpc/escape"
4
+ autoload :VERSION, "ccrpc/version"
5
+ end
data.tar.gz.sig ADDED
@@ -0,0 +1 @@
1
+ �=EB!�|7^��S ��zB?���,�Men%쓐o���γ���l08B�ur�p���^���|s�B{.X�D��93�����t���Mb���J�/�ɂ�W/jI�<���#G�������M�Czn{�&�Cd��Z�m�E9 >I͉�Hl��h�uV{��䗧����PB��fet54;O��F��k����ڥwF�g�;��~���.U�lna5�u
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ccrpc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Lars Kanis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDLjCCAhagAwIBAgIBDDANBgkqhkiG9w0BAQsFADA9MQ4wDAYDVQQDDAVrYW5p
14
+ czEXMBUGCgmSJomT8ixkARkWB2NvbWNhcmQxEjAQBgoJkiaJk/IsZAEZFgJkZTAe
15
+ Fw0yNDA1MDIxMTAwNDVaFw0yNTA1MDIxMTAwNDVaMD0xDjAMBgNVBAMMBWthbmlz
16
+ MRcwFQYKCZImiZPyLGQBGRYHY29tY2FyZDESMBAGCgmSJomT8ixkARkWAmRlMIIB
17
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApop+rNmg35bzRugZ21VMGqI6
18
+ HGzPLO4VHYncWn/xmgPU/ZMcZdfj6MzIaZJ/czXyt4eHpBk1r8QOV3gBXnRXEjVW
19
+ 9xi+EdVOkTV2/AVFKThcbTAQGiF/bT1n2M+B1GTybRzMg6hyhOJeGPqIhLfJEpxn
20
+ lJi4+ENAVT4MpqHEAGB8yFoPC0GqiOHQsdHxQV3P3c2OZqG+yJey74QtwA2tLcLn
21
+ Q53c63+VLGsOjODl1yPn/2ejyq8qWu6ahfTxiIlSar2UbwtaQGBDFdb2CXgEufXT
22
+ L7oaPxlmj+Q2oLOfOnInd2Oxop59HoJCQPsg8f921J43NCQGA8VHK6paxIRDLQID
23
+ AQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQUvgTdT7fe
24
+ x17ugO3IOsjEJwW7KP4wDQYJKoZIhvcNAQELBQADggEBAH+LA+CcA9vbbqtuhK4J
25
+ lG1lQwA+hCKiueQgVsepNbXyDzx6PMC8ap/bFaKSaoUWABBA/bsh3jDNXT/eVZrN
26
+ lFP8cVGrznSYIBG8D/QQmJKpvDBJgnC4Zk01HkhYlqJC4qCTn9X+/uZNHLPLbAEL
27
+ xl3P43zyL3GQb1IP9bp0xV6oxwG9FO9Rk8bYDojky/69ylowFI5aODS39v01Siu2
28
+ FsEjM9tMSNb7lQRywQ/432KXi+8AAPTm+wdGnlt3wLE9w2TTpGUBsNKk+QiytTZO
29
+ zwbjAdVlRm0pg/vpDzzFmRf1GYVckMm8hpCRt8BP0akAbNyw1snYvZBwgLqxztru
30
+ bEo=
31
+ -----END CERTIFICATE-----
32
+ date: 2024-07-11 00:00:00.000000000 Z
33
+ dependencies: []
34
+ description: Simple bidirectional and thread safe RPC protocol. Works on arbitrary
35
+ Ruby IO objects.
36
+ email:
37
+ - lars@greiz-reinsdorf.de
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - ".github/workflows/ci.yml"
43
+ - ".gitignore"
44
+ - ".gitlab-ci.yml"
45
+ - ".travis.yml"
46
+ - Gemfile
47
+ - LICENSE.txt
48
+ - README.md
49
+ - Rakefile
50
+ - bin/console
51
+ - bin/setup
52
+ - ccrpc.gemspec
53
+ - lib/ccrpc.rb
54
+ - lib/ccrpc/escape.rb
55
+ - lib/ccrpc/lazy.rb
56
+ - lib/ccrpc/rpc_connection.rb
57
+ - lib/ccrpc/version.rb
58
+ homepage: https://github.com/larskanis/ccrpc
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ homepage_uri: https://github.com/larskanis/ccrpc
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 2.7.0
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.5.11
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Simple bidirectional RPC protocol
82
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1 @@
1
+ I�}�17��ˎ�F�ȯ~W�����4ٍn��^+0