elrpc 0.0.1
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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +135 -0
- data/Rakefile +2 -0
- data/elrpc.gemspec +25 -0
- data/lib/elrpc.rb +697 -0
- data/lib/elrpc/version.rb +3 -0
- data/sample/echo-client.el +28 -0
- data/sample/echo-client.rb +22 -0
- data/sample/echo.rb +15 -0
- data/test/_add.rb +9 -0
- data/test/_call_echo.rb +10 -0
- data/test/_echo.rb +9 -0
- data/test/_errors.rb +15 -0
- data/test/_methods.rb +11 -0
- data/test/_process.rb +6 -0
- data/test/echo-bench.rb +40 -0
- data/test/test-epc.rb +164 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b19cb4f1857c727202dfee06bce97cad08cf54ad
|
4
|
+
data.tar.gz: 1038805533721cde69d0a9e1a9b9a7991cd3c61d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 53444d2a46a9fc54d3eed9004b6e23df4553d6b57aa39c42e46f9c8088ac5a523bce1e2f7b54e40e51bc9f50a35f24ead175c042dec6aa7329da8f2febe5c63a
|
7
|
+
data.tar.gz: 01a5342c11b311b802d895d88e694e3b5fb848c0d5b1200d1be4f265ff6fefaba4ab1d7c94f4f26f33be0bc5f869c521ce55df44c92cb4f2e64b51630d0b1d56
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 SAKURAI Masashi
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# Elrpc : EPC (RPC Stack for Emacs Lisp) for Ruby
|
2
|
+
|
3
|
+
EPC is an RPC stack for Emacs Lisp and Elrpc is an implementation of EPC in Ruby.
|
4
|
+
Using elrpc, you can develop an emacs extension with Ruby code.
|
5
|
+
|
6
|
+
- https://github.com/kiwanami/emacs-epc
|
7
|
+
|
8
|
+
## Sample Code
|
9
|
+
|
10
|
+
### Ruby code (child process)
|
11
|
+
|
12
|
+
This code will be started by the parent process.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'elrpc'
|
16
|
+
|
17
|
+
# start server
|
18
|
+
server = Elrpc.start_server()
|
19
|
+
|
20
|
+
# define a method
|
21
|
+
server.def_method "echo" do |arg|
|
22
|
+
# just return the given argument value
|
23
|
+
arg
|
24
|
+
end
|
25
|
+
|
26
|
+
# sleep the main thread and wait for closing connection
|
27
|
+
server.wait
|
28
|
+
```
|
29
|
+
|
30
|
+
### Emacs Lisp code (parent process)
|
31
|
+
|
32
|
+
This elisp code calls the child process.
|
33
|
+
The package `epc` is required.
|
34
|
+
|
35
|
+
```el
|
36
|
+
(require 'epc)
|
37
|
+
|
38
|
+
(let (epc)
|
39
|
+
;; start a child process (using bundle exec)
|
40
|
+
(setq epc (epc:start-epc "bundle" '("exec" "ruby" "echo.rb")))
|
41
|
+
|
42
|
+
(deferred:$
|
43
|
+
(epc:call-deferred epc 'echo '("hello"))
|
44
|
+
(deferred:nextc it
|
45
|
+
(lambda (x) (message "Return : %S" x))))
|
46
|
+
|
47
|
+
(message "%S" (epc:call-sync epc 'echo '(world)))
|
48
|
+
|
49
|
+
(epc:stop-epc epc)) ; just `eval-last-sexp' here
|
50
|
+
```
|
51
|
+
|
52
|
+
### Ruby code (parent process)
|
53
|
+
|
54
|
+
You can also write the parent process code in Ruby.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'elrpc'
|
58
|
+
|
59
|
+
# start a child process
|
60
|
+
cl = Elrpc.start_process(["ruby","echo.rb"])
|
61
|
+
|
62
|
+
# synchronous calling
|
63
|
+
puts cl.call_method("echo", "1 hello")
|
64
|
+
|
65
|
+
# asynchronous calling
|
66
|
+
cl.call_method_async("echo", "3 world") do |err, value|
|
67
|
+
puts value
|
68
|
+
end
|
69
|
+
|
70
|
+
puts "2 wait"
|
71
|
+
sleep 0.2
|
72
|
+
|
73
|
+
puts "4 ok"
|
74
|
+
# kill the child process
|
75
|
+
cl.stop
|
76
|
+
```
|
77
|
+
|
78
|
+
Here is the result.
|
79
|
+
|
80
|
+
```
|
81
|
+
$ bundle exec ruby echo-client.rb
|
82
|
+
1 hello
|
83
|
+
2 wait
|
84
|
+
3 world
|
85
|
+
4 ok
|
86
|
+
```
|
87
|
+
|
88
|
+
## Installation
|
89
|
+
|
90
|
+
Add this line to your application's Gemfile:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
gem 'elrpc'
|
94
|
+
```
|
95
|
+
|
96
|
+
And then execute:
|
97
|
+
|
98
|
+
$ bundle
|
99
|
+
|
100
|
+
Or install it yourself as:
|
101
|
+
|
102
|
+
$ gem install elrpc
|
103
|
+
|
104
|
+
## API Document
|
105
|
+
|
106
|
+
Please see the EPC document for the overview of EPC stack and protocol details.
|
107
|
+
|
108
|
+
- [EPC Readme](https://github.com/kiwanami/emacs-epc)
|
109
|
+
|
110
|
+
Please see the `elparser` document for object serialization.
|
111
|
+
|
112
|
+
- [elparser Readme](https://github.com/kiwanami/ruby-elparser)
|
113
|
+
|
114
|
+
### Start EPC
|
115
|
+
|
116
|
+
### Stop EPC
|
117
|
+
|
118
|
+
### Define Remote Method
|
119
|
+
|
120
|
+
### Call Remote Method
|
121
|
+
|
122
|
+
### Error Handling
|
123
|
+
|
124
|
+
### Utilities
|
125
|
+
|
126
|
+
### Define Server
|
127
|
+
|
128
|
+
### Debug
|
129
|
+
|
130
|
+
## License
|
131
|
+
|
132
|
+
Elrpc is licensed under MIT.
|
133
|
+
|
134
|
+
----
|
135
|
+
(C) 2015 SAKURAI Masashi. m.sakurai at kiwanami.net
|
data/Rakefile
ADDED
data/elrpc.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elrpc/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "elrpc"
|
8
|
+
spec.version = Elrpc::VERSION
|
9
|
+
spec.authors = ["SAKURAI Masashi"]
|
10
|
+
spec.email = ["m.sakurai@kiwanami.net"]
|
11
|
+
spec.summary = %q{EPC (RPC stack for the Emacs Lisp) for Ruby.}
|
12
|
+
spec.description = %q{EPC (RPC stack for the Emacs Lisp) for Ruby.}
|
13
|
+
spec.homepage = "https://github.com/kiwanami/ruby-elrpc"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "elparser", "~> 0.0"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "test-unit", "~> 3.0"
|
25
|
+
end
|
data/lib/elrpc.rb
ADDED
@@ -0,0 +1,697 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "elrpc/version"
|
3
|
+
|
4
|
+
require "socket"
|
5
|
+
require "thread"
|
6
|
+
require "monitor"
|
7
|
+
require "logger"
|
8
|
+
|
9
|
+
require "elparser"
|
10
|
+
|
11
|
+
|
12
|
+
module Elrpc
|
13
|
+
|
14
|
+
@@default_log_level = Logger::WARN
|
15
|
+
|
16
|
+
# Logger::WARN, Logger::INFO, Logger::DEBUG
|
17
|
+
def self.set_default_log_level(level)
|
18
|
+
@@default_log_level = level
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.default_log_level
|
22
|
+
@@default_log_level
|
23
|
+
end
|
24
|
+
|
25
|
+
@@count = 1
|
26
|
+
|
27
|
+
def self.gen_uid
|
28
|
+
@@count += 1
|
29
|
+
@@count
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.get_logger_format(comid)
|
33
|
+
return "%Y/%m/%d %H:%M:%S #{comid}"
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
def self.start_server(methods = [], port = 0)
|
39
|
+
server_socket = TCPServer.open(port)
|
40
|
+
port_number = server_socket.local_address.ip_port
|
41
|
+
STDOUT.puts "#{port_number}"
|
42
|
+
STDOUT.flush
|
43
|
+
socket = server_socket.accept
|
44
|
+
server = RPCService.new("SV", socket, methods)
|
45
|
+
server.add_close_hook do
|
46
|
+
server_socket.close
|
47
|
+
end
|
48
|
+
return server
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.start_client(port, methods=[], host = "127.0.0.1")
|
52
|
+
socket = TCPSocket.open(host, port)
|
53
|
+
client = RPCService.new("CL", socket, methods)
|
54
|
+
return client
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.start_process(cmd)
|
58
|
+
svr = Service.new(cmd)
|
59
|
+
svr.start
|
60
|
+
return svr
|
61
|
+
end
|
62
|
+
|
63
|
+
class Service
|
64
|
+
|
65
|
+
attr_reader :cmd, :port, :client
|
66
|
+
attr :output
|
67
|
+
|
68
|
+
# cmd = ["ruby", "_call.rb"]
|
69
|
+
def initialize(cmd)
|
70
|
+
@cmd = cmd
|
71
|
+
end
|
72
|
+
|
73
|
+
def _start_logger
|
74
|
+
return Thread.start do
|
75
|
+
loop do
|
76
|
+
ret = @io.readline
|
77
|
+
break if ret.nil?
|
78
|
+
@logger.puts(ret) if @logger
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def start
|
84
|
+
@io = IO.popen(@cmd)
|
85
|
+
@port = @io.readline.to_i
|
86
|
+
@output = nil
|
87
|
+
@thread = _start_logger
|
88
|
+
@client = Elrpc.start_client(@port)
|
89
|
+
return self
|
90
|
+
end
|
91
|
+
|
92
|
+
def register_method(method)
|
93
|
+
@client.register_method(method)
|
94
|
+
end
|
95
|
+
|
96
|
+
def def_method(name, argdoc=nil, docstring=nil, &block)
|
97
|
+
@client.def_method(name, argdoc, docstring, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def call_method_async(name, *args, &block)
|
101
|
+
@client.call_method_async(name, *args, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
def call_method(name, *args)
|
105
|
+
@client.call_method(name, *args)
|
106
|
+
end
|
107
|
+
|
108
|
+
def query_methods_async(&block)
|
109
|
+
@client.query_methods(&block)
|
110
|
+
end
|
111
|
+
|
112
|
+
def query_methods
|
113
|
+
@client.query_methods
|
114
|
+
end
|
115
|
+
|
116
|
+
def stop
|
117
|
+
@client.stop
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
class Method
|
125
|
+
|
126
|
+
attr_reader :name, :proc
|
127
|
+
attr :argdoc, :docstring
|
128
|
+
|
129
|
+
def initialize(name, argdoc=nil, docstring=nil, &proc)
|
130
|
+
@name = name.to_sym
|
131
|
+
@proc = proc
|
132
|
+
@argdoc = argdoc
|
133
|
+
@docstring = docstring
|
134
|
+
end
|
135
|
+
|
136
|
+
def call(args)
|
137
|
+
@proc.call(*args)
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
class EPCRuntimeError < StandardError
|
143
|
+
def initialize(_classname, _message, _backtrace)
|
144
|
+
@_classname = _classname
|
145
|
+
@_message = _message
|
146
|
+
@_backtrace = _backtrace
|
147
|
+
end
|
148
|
+
|
149
|
+
def message
|
150
|
+
"#{@_classname} : #{@_message}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def remote_classname
|
154
|
+
@_classname
|
155
|
+
end
|
156
|
+
def remote_message
|
157
|
+
@_message
|
158
|
+
end
|
159
|
+
def remote_backtrace
|
160
|
+
@_backtrace
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class EPCStackError < StandardError
|
165
|
+
def initialize(_classname, _message, _backtrace)
|
166
|
+
@_classname = _classname
|
167
|
+
@_message = _message
|
168
|
+
@_backtrace = _backtrace
|
169
|
+
end
|
170
|
+
|
171
|
+
def message
|
172
|
+
"#{@_classname} : #{@_message}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def remote_classname
|
176
|
+
@_classname
|
177
|
+
end
|
178
|
+
def remote_message
|
179
|
+
@_message
|
180
|
+
end
|
181
|
+
def remote_backtrace
|
182
|
+
@_backtrace
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
## 送信用データクラス
|
188
|
+
## キューに入れるために使う
|
189
|
+
|
190
|
+
class CallMessage
|
191
|
+
attr_reader :uid, :method, :args, :block
|
192
|
+
|
193
|
+
def initialize(uid, method, args, block)
|
194
|
+
@uid = uid
|
195
|
+
@method = method
|
196
|
+
@args = args
|
197
|
+
@block = block
|
198
|
+
end
|
199
|
+
|
200
|
+
def to_ast
|
201
|
+
[:call, @uid, @method, @args]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class MethodsMessage
|
206
|
+
attr_reader :uid, :block
|
207
|
+
|
208
|
+
def initialize(uid, block)
|
209
|
+
@uid = uid
|
210
|
+
@block = block
|
211
|
+
end
|
212
|
+
|
213
|
+
def to_ast
|
214
|
+
[:methods, @uid]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class ReturnMessage
|
219
|
+
attr_reader :uid, :value
|
220
|
+
|
221
|
+
def initialize(uid, value)
|
222
|
+
@uid = uid
|
223
|
+
@value = value
|
224
|
+
end
|
225
|
+
|
226
|
+
def to_ast
|
227
|
+
[:return, @uid, @value]
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class ErrorMessage
|
232
|
+
attr_reader :uid, :error_msg
|
233
|
+
|
234
|
+
def initialize(uid, error_msg)
|
235
|
+
@uid = uid
|
236
|
+
@error_msg = error_msg
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_ast
|
240
|
+
[:'return-error', @uid, @error_msg]
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class EPCErrorMessage
|
245
|
+
attr_reader :uid, :error_msg
|
246
|
+
|
247
|
+
def initialize(uid, error_msg)
|
248
|
+
@uid = uid
|
249
|
+
@error_msg = error_msg
|
250
|
+
end
|
251
|
+
|
252
|
+
def to_ast
|
253
|
+
[:'epc-error', @uid, @error_msg]
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
class RPCService
|
259
|
+
|
260
|
+
attr_reader :socket_state
|
261
|
+
attr_accessor :logger
|
262
|
+
|
263
|
+
def initialize(name, socket, methods = nil)
|
264
|
+
@logger = Logger.new(STDOUT)
|
265
|
+
@logger.level = Elrpc::default_log_level
|
266
|
+
@logger.datetime_format = Elrpc.get_logger_format(name)
|
267
|
+
|
268
|
+
@methods = Hash.new # name -> Method
|
269
|
+
@session = Hash.new # uid -> proc
|
270
|
+
@session_lock = Monitor.new
|
271
|
+
|
272
|
+
@sending_queue = Queue.new # CallMessage
|
273
|
+
|
274
|
+
@socket = socket
|
275
|
+
@socket_state_lock = Monitor.new
|
276
|
+
@socket_state = :socket_opened
|
277
|
+
|
278
|
+
@wait_lock = nil
|
279
|
+
@wait_cv = nil
|
280
|
+
@close_hooks = []
|
281
|
+
|
282
|
+
if methods then
|
283
|
+
methods.each do |m|
|
284
|
+
register_method(m)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
@sender_thread = Thread.start { sender_loop }
|
289
|
+
@receiver_thread = Thread.start { receiver_loop }
|
290
|
+
@worker_pool = WorkerPool.new(1, @logger)
|
291
|
+
|
292
|
+
@logger.debug ":ready for I/O stream."
|
293
|
+
end
|
294
|
+
|
295
|
+
# 自分にメソッドを登録する
|
296
|
+
def register_method(method)
|
297
|
+
@methods[method.name] = method
|
298
|
+
end
|
299
|
+
|
300
|
+
# register_method の簡易版
|
301
|
+
def def_method(name, argdoc=nil, docstring=nil, &block)
|
302
|
+
register_method(Method.new(name, argdoc, docstring, &block))
|
303
|
+
end
|
304
|
+
|
305
|
+
# 相手のメソッドを呼ぶ
|
306
|
+
# block(err, value)
|
307
|
+
def call_method_async(name, *args, &block)
|
308
|
+
uid = Elrpc.gen_uid
|
309
|
+
msg = CallMessage.new(uid, name, args, block)
|
310
|
+
# ここは競合しないのでロックしない
|
311
|
+
@session[uid] = msg
|
312
|
+
@sending_queue.push(msg)
|
313
|
+
uid
|
314
|
+
end
|
315
|
+
|
316
|
+
# 相手のメソッドを呼ぶ(同期版)
|
317
|
+
def call_method(name, *args)
|
318
|
+
mutex = Mutex.new
|
319
|
+
cv = ConditionVariable.new
|
320
|
+
ret = nil
|
321
|
+
ex = nil
|
322
|
+
call_method_async(name, *args) do |err, value|
|
323
|
+
mutex.synchronize do
|
324
|
+
ex = err
|
325
|
+
ret = value
|
326
|
+
cv.signal
|
327
|
+
end
|
328
|
+
end
|
329
|
+
mutex.synchronize do
|
330
|
+
cv.wait(mutex)
|
331
|
+
end
|
332
|
+
if !ex.nil?
|
333
|
+
raise ex
|
334
|
+
end
|
335
|
+
return ret
|
336
|
+
end
|
337
|
+
|
338
|
+
# 接続相手のメソッド一覧を返す
|
339
|
+
# [[name, argdoc, docstring], ...]
|
340
|
+
def query_methods_async(&block)
|
341
|
+
uid = Elrpc.gen_uid
|
342
|
+
msg = MethodsMessage.new(uid, block)
|
343
|
+
@session[uid] = msg
|
344
|
+
@sending_queue.push(msg)
|
345
|
+
uid
|
346
|
+
end
|
347
|
+
|
348
|
+
# 接続相手のメソッド一覧を返す(同期版)
|
349
|
+
# [[name, argdoc, docstring], ...]
|
350
|
+
def query_methods
|
351
|
+
mutex = Mutex.new
|
352
|
+
cv = ConditionVariable.new
|
353
|
+
ret = nil
|
354
|
+
ex = nil
|
355
|
+
query_methods_async do |err, value|
|
356
|
+
mutex.synchronize do
|
357
|
+
ex = err
|
358
|
+
ret = value
|
359
|
+
cv.signal
|
360
|
+
end
|
361
|
+
end
|
362
|
+
mutex.synchronize do
|
363
|
+
cv.wait(mutex)
|
364
|
+
end
|
365
|
+
if !ex.nil?
|
366
|
+
raise ex
|
367
|
+
end
|
368
|
+
return ret
|
369
|
+
end
|
370
|
+
|
371
|
+
def stop
|
372
|
+
if @socket_state == :socket_opened then
|
373
|
+
@logger.debug "RPCService.stop: received!"
|
374
|
+
@worker_pool.kill
|
375
|
+
@socket_state = :socket_closing
|
376
|
+
@socket.close
|
377
|
+
@sending_queue << nil # stop message
|
378
|
+
@sender_thread.join(4) unless Thread.current == @sender_thread
|
379
|
+
@receiver_thread.join(4) unless Thread.current == @receiver_thread
|
380
|
+
_clear_waiting_sessions
|
381
|
+
@socket_state = :scoket_not_connected
|
382
|
+
end
|
383
|
+
_wakeup
|
384
|
+
@logger.debug "RPCService.stop: completed"
|
385
|
+
end
|
386
|
+
|
387
|
+
# ソケットが相手から切断されるまでメインスレッドを止める
|
388
|
+
def wait
|
389
|
+
@wait_lock = Mutex.new
|
390
|
+
@wait_cv = ConditionVariable.new
|
391
|
+
@wait_lock.synchronize do
|
392
|
+
@wait_cv.wait(@wait_lock)
|
393
|
+
end
|
394
|
+
stop
|
395
|
+
end
|
396
|
+
|
397
|
+
def add_close_hook(&block)
|
398
|
+
@close_hooks << block
|
399
|
+
end
|
400
|
+
|
401
|
+
|
402
|
+
private
|
403
|
+
|
404
|
+
# RPC呼び出しで待ってるスレッドを全てエラーにして終了させる
|
405
|
+
def _clear_waiting_sessions
|
406
|
+
@session_lock.synchronize do
|
407
|
+
@session.keys.each do |uid|
|
408
|
+
_session_return(uid, "EPC Connection closed", nil)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def _socket_state_lock
|
414
|
+
@socket_state_lock.synchronize do
|
415
|
+
yield
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# もし、メインスレッドが停止していれば再開させて終了させる
|
420
|
+
def _wakeup
|
421
|
+
if @wait_lock
|
422
|
+
@wait_lock.synchronize do
|
423
|
+
@wait_cv.signal
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# 相手にシリアライズされたデータを送る
|
429
|
+
def _send_message(msg)
|
430
|
+
msg = msg.encode("UTF-8") + "\n"
|
431
|
+
len = msg.bytesize
|
432
|
+
body = sprintf("%06x%s",len,msg)
|
433
|
+
@socket.write(body)
|
434
|
+
@socket.flush
|
435
|
+
end
|
436
|
+
|
437
|
+
# 呼び出し元に値を返して、セッションをクリアする
|
438
|
+
def _session_return(uid, error, value)
|
439
|
+
m = nil
|
440
|
+
@session_lock.synchronize do
|
441
|
+
m = @session[uid]
|
442
|
+
@session.delete(uid)
|
443
|
+
end
|
444
|
+
if m then
|
445
|
+
m.block.call(error, value)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def sender_loop
|
450
|
+
loop do
|
451
|
+
begin
|
452
|
+
entry = @sending_queue.shift
|
453
|
+
if entry.nil? then
|
454
|
+
@logger.debug "Queue.shift received stop message."
|
455
|
+
break
|
456
|
+
end
|
457
|
+
@logger.debug "Queue.shift [#{@sending_queue.size}] : #{entry.uid}"
|
458
|
+
body = Elparser.encode( entry.to_ast )
|
459
|
+
@logger.debug "Encode : #{body}"
|
460
|
+
_send_message( body )
|
461
|
+
@logger.debug " Queue -> sent #{entry.uid}"
|
462
|
+
rescue Elparser::EncodingError => evar
|
463
|
+
@logger.warn "[sendloop] #{evar.to_s} "
|
464
|
+
err = EPCStackError.new(evar.class.name, evar.message, evar.backtrace)
|
465
|
+
_session_return(entry.uid, err, nil) if entry
|
466
|
+
rescue => evar
|
467
|
+
mes = evar.message
|
468
|
+
@logger.warn "[sendloop] #{evar.to_s} "
|
469
|
+
if mes["abort"] then
|
470
|
+
@logger.warn " [sendloop] disconnected by the peer."
|
471
|
+
@socket_state = :socket_not_connected
|
472
|
+
elsif evar.class == IOError then
|
473
|
+
@logger.warn evar.backtrace.join("\n")
|
474
|
+
@socket_state = :socket_closing
|
475
|
+
end
|
476
|
+
_session_return(entry.uid, evar, nil) if entry
|
477
|
+
end # begin
|
478
|
+
if @socket_state == :socket_closing ||
|
479
|
+
@socket_state == :socket_not_connected then
|
480
|
+
@logger.debug "[sender-thread] terminating..."
|
481
|
+
break
|
482
|
+
end
|
483
|
+
end # loop
|
484
|
+
@logger.debug "[sender-thread] loop exit : #{@socket_state}"
|
485
|
+
_wakeup
|
486
|
+
@logger.debug "[sender-thread] exit--------------"
|
487
|
+
end
|
488
|
+
|
489
|
+
def receiver_loop
|
490
|
+
parser = Elparser::Parser.new
|
491
|
+
loop do
|
492
|
+
ast = nil # for error message and recovery
|
493
|
+
uid = nil
|
494
|
+
begin
|
495
|
+
lenstr = @socket.read(6)
|
496
|
+
if lenstr.nil? then
|
497
|
+
@logger.debug "[rcvloop] Socket closed!"
|
498
|
+
break
|
499
|
+
end
|
500
|
+
len = lenstr.to_i(16)
|
501
|
+
@logger.debug "Receiving a message : len=#{len}"
|
502
|
+
body = @socket.read(len) # 1 means LF
|
503
|
+
if body.nil? then
|
504
|
+
@logger.debug "[rcvloop] Socket closed!"
|
505
|
+
break
|
506
|
+
end
|
507
|
+
@logger.debug "Parse : #{body}"
|
508
|
+
ast = parser.parse(body)
|
509
|
+
raise "Unexpected multiple s-expression : #{body}" if ast.size != 1
|
510
|
+
ast = ast[0].to_ruby
|
511
|
+
uid = ast[1]
|
512
|
+
case ast[0]
|
513
|
+
when :call
|
514
|
+
@logger.debug " received: CALL : #{uid}"
|
515
|
+
_call(ast)
|
516
|
+
when :return
|
517
|
+
@logger.debug " received: RETURN: #{uid}"
|
518
|
+
_return(ast)
|
519
|
+
when :'return-error'
|
520
|
+
@logger.debug " received: ERROR: #{uid}"
|
521
|
+
_return_error(ast)
|
522
|
+
when :'epc-error'
|
523
|
+
@logger.debug " received: EPC_ERROR: #{uid}"
|
524
|
+
_epc_error(ast)
|
525
|
+
when :'methods'
|
526
|
+
@logger.debug " received: METHODS: #{uid}"
|
527
|
+
_query_methods(ast)
|
528
|
+
else
|
529
|
+
@logger.debug " Unknown message code. try to reset the connection. >> #{body}"
|
530
|
+
@socket_state = :socket_closing
|
531
|
+
@sending_queue.push nil # wakeup sender thread
|
532
|
+
return
|
533
|
+
end # case
|
534
|
+
if @socket_state == :socket_closing then
|
535
|
+
@logger.debug "[receiver-thread] terminating..."
|
536
|
+
break
|
537
|
+
end
|
538
|
+
rescue Exception => evar
|
539
|
+
@logger.debug "[rcvloop] Exception! #{evar}"
|
540
|
+
mes = evar.message
|
541
|
+
if uid && @session[uid] then
|
542
|
+
_session_return(uid, evar, nil)
|
543
|
+
end
|
544
|
+
if mes["close"] || mes["reset"] then
|
545
|
+
@logger.debug " [rcvloop] disconnected by the peer."
|
546
|
+
break
|
547
|
+
elsif evar.kind_of?(IOError) then
|
548
|
+
@logger.debug " [rcvloop] IOError."
|
549
|
+
@socket_state = :socket_closing
|
550
|
+
break
|
551
|
+
else
|
552
|
+
@logger.warn " [rcvloop] going to recover the communication."
|
553
|
+
bt = evar.backtrace.join("\n")
|
554
|
+
@logger.warn " [rcvloop] #{bt}"
|
555
|
+
end
|
556
|
+
end # begin rescue
|
557
|
+
ast = nil
|
558
|
+
uid = nil
|
559
|
+
end # loop
|
560
|
+
@logger.debug "[receiver-thread] loop exit : #{@socket_state}"
|
561
|
+
_wakeup
|
562
|
+
@logger.debug "[receiver-thread exit]--------------"
|
563
|
+
end
|
564
|
+
|
565
|
+
# 相手からメソッドを呼ばれた
|
566
|
+
def _call(ast)
|
567
|
+
_, uid, name, args = ast
|
568
|
+
@logger.debug ": called: Enter: #{name} : #{uid}"
|
569
|
+
method = @methods[name.to_sym]
|
570
|
+
if method then
|
571
|
+
task = -> do
|
572
|
+
msg = nil
|
573
|
+
begin
|
574
|
+
ret = method.call(args)
|
575
|
+
msg = ReturnMessage.new(uid, ret)
|
576
|
+
rescue => e
|
577
|
+
@logger.debug ": called: Error!: #{name} : #{uid} : #{e}"
|
578
|
+
@logger.debug e
|
579
|
+
msg = ErrorMessage.new(uid, [e.class.name, e.message, e.backtrace.join("\n")])
|
580
|
+
end
|
581
|
+
@sending_queue.push(msg)
|
582
|
+
end
|
583
|
+
@worker_pool.invoke(task)
|
584
|
+
else
|
585
|
+
# method not found
|
586
|
+
@logger.debug ": called: Method not found: #{name} : #{uid} "
|
587
|
+
@sending_queue.push(EPCErrorMessage.new(uid, "Not found the name: #{name}"))
|
588
|
+
end # if
|
589
|
+
@logger.debug ": called: Leave: #{name} : #{uid}"
|
590
|
+
end
|
591
|
+
|
592
|
+
# 相手から返り値が返ってきた
|
593
|
+
def _return(ast)
|
594
|
+
_, uid, value = ast
|
595
|
+
@logger.debug ": return: Start: #{uid} : value = #{value}"
|
596
|
+
if @session[uid] then
|
597
|
+
_session_return(uid, nil, value)
|
598
|
+
else
|
599
|
+
@logger.error "Not found a session for #{uid}"
|
600
|
+
end
|
601
|
+
@logger.debug ": return: End: #{uid}"
|
602
|
+
end
|
603
|
+
|
604
|
+
# 相手からアプリケーションエラーが返ってきた
|
605
|
+
def _return_error(ast)
|
606
|
+
_, uid, error = ast
|
607
|
+
@logger.debug ": return-error: Start: #{uid} : error = #{error}"
|
608
|
+
if @session[uid] then
|
609
|
+
# error : [classname, message, backtrace]
|
610
|
+
_session_return(uid, EPCRuntimeError.new(error[0], error[1], error[2]), nil)
|
611
|
+
else
|
612
|
+
@logger.error "Not found a session for #{uid}"
|
613
|
+
end
|
614
|
+
@logger.debug ": return-error: End: #{uid}"
|
615
|
+
end
|
616
|
+
|
617
|
+
# 相手からEPCエラーが返ってきた
|
618
|
+
def _epc_error(ast)
|
619
|
+
_, uid, error = ast
|
620
|
+
@logger.debug ": epc-error: Start: #{uid} : error = #{error}"
|
621
|
+
if @session[uid] then
|
622
|
+
# error : [classname, message, backtrace]
|
623
|
+
_session_return(uid, EPCStackError.new(error[0], error[1], error[2]), nil)
|
624
|
+
else
|
625
|
+
@logger.error "Not found a session for #{uid}"
|
626
|
+
end
|
627
|
+
@logger.debug ": epc-error: End: #{uid}"
|
628
|
+
end
|
629
|
+
|
630
|
+
# 相手から一覧要求があった
|
631
|
+
def _query_methods(ast)
|
632
|
+
_, uid = ast
|
633
|
+
@logger.debug ": query-methods: Start: #{uid}"
|
634
|
+
begin
|
635
|
+
list = @methods.map do |k,m|
|
636
|
+
[m.name, m.argdoc, m.docstring]
|
637
|
+
end
|
638
|
+
msg = ReturnMessage.new(uid, list)
|
639
|
+
@sending_queue.push(msg)
|
640
|
+
rescue => e
|
641
|
+
@logger.warn ": query-method: Exception #{e.message}"
|
642
|
+
@logger.warn e.backtrace.join("\n")
|
643
|
+
msg = ErrorMessage.new(uid, [e.class.name, e.message, e.backtrace.join("\n")])
|
644
|
+
@sending_queue.push(msg)
|
645
|
+
end
|
646
|
+
@logger.debug ": query-methods: End: #{uid}"
|
647
|
+
end
|
648
|
+
|
649
|
+
end # class RPCService
|
650
|
+
|
651
|
+
|
652
|
+
## タスク処理用のスレッドプール
|
653
|
+
|
654
|
+
class WorkerPool
|
655
|
+
|
656
|
+
def initialize(num, logger)
|
657
|
+
@logger = logger
|
658
|
+
@job_queue = Queue.new
|
659
|
+
@worker_threads = []
|
660
|
+
num.times {
|
661
|
+
@worker_threads << Thread.start(@job_queue, @logger) { |queue, logger|
|
662
|
+
logger.debug("Worker Start")
|
663
|
+
loop {
|
664
|
+
begin
|
665
|
+
job = queue.shift
|
666
|
+
logger.debug "Worker Thread : Job #{job}"
|
667
|
+
break if job.nil?
|
668
|
+
job.call
|
669
|
+
rescue => e
|
670
|
+
logger.error "Worker Error >>"
|
671
|
+
logger.error e
|
672
|
+
end
|
673
|
+
}
|
674
|
+
logger.debug("Worker Exit")
|
675
|
+
}
|
676
|
+
}
|
677
|
+
end
|
678
|
+
|
679
|
+
def invoke(job)
|
680
|
+
if @worker_threads.size == 0 then
|
681
|
+
@logger.debug "Worker : Ignore #{job}"
|
682
|
+
return
|
683
|
+
end
|
684
|
+
@job_queue << job
|
685
|
+
end
|
686
|
+
|
687
|
+
def kill
|
688
|
+
@worker_threads.size.times {
|
689
|
+
invoke(nil)
|
690
|
+
}
|
691
|
+
@worker_threads.each {|t| t.join }
|
692
|
+
@worker_threads.clear
|
693
|
+
end
|
694
|
+
|
695
|
+
end
|
696
|
+
|
697
|
+
end
|