ccrpc 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +9 -0
- data/.gitlab-ci.yml +15 -0
- data/.travis.yml +6 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ccrpc.gemspec +25 -0
- data/lib/ccrpc/escape.rb +15 -0
- data/lib/ccrpc/lazy.rb +135 -0
- data/lib/ccrpc/rpc_connection.rb +327 -0
- data/lib/ccrpc/version.rb +3 -0
- data/lib/ccrpc.rb +5 -0
- data.tar.gz.sig +1 -0
- metadata +82 -0
- metadata.gz.sig +1 -0
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
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
data/Gemfile
ADDED
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
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
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
|
data/lib/ccrpc/escape.rb
ADDED
@@ -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
|
data/lib/ccrpc.rb
ADDED
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
|