RubyProcess 0.0.12
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/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +20 -0
- data/README.md +98 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/cmds/blocks.rb +68 -0
- data/cmds/marshal.rb +8 -0
- data/cmds/new.rb +44 -0
- data/cmds/numeric.rb +14 -0
- data/cmds/static.rb +36 -0
- data/cmds/str_eval.rb +16 -0
- data/cmds/system.rb +47 -0
- data/examples/example_csv.rb +13 -0
- data/examples/example_file_write.rb +15 -0
- data/examples/example_knj_db_dump.rb +52 -0
- data/examples/example_strscan.rb +14 -0
- data/include/args_handeling.rb +88 -0
- data/lib/ruby_process.rb +458 -0
- data/lib/ruby_process/class_proxy.rb +106 -0
- data/lib/ruby_process/proxy_object.rb +55 -0
- data/ruby_process.gemspec +85 -0
- data/scripts/ruby_process_script.rb +37 -0
- data/shippable.yml +7 -0
- data/spec/class_proxy_spec.rb +95 -0
- data/spec/hard_load_spec.rb +35 -0
- data/spec/leaks_spec.rb +24 -0
- data/spec/ruby_process_spec.rb +174 -0
- data/spec/spec_helper.rb +14 -0
- metadata +173 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "ruby_process"
|
5
|
+
|
6
|
+
RubyProcess.new.spawn_process do |rp|
|
7
|
+
rp.static(:Object, :require, "csv")
|
8
|
+
|
9
|
+
rp.static(:CSV, :open, "test.csv", "w") do |csv|
|
10
|
+
csv << ["ID", "Name"]
|
11
|
+
csv << [1, "Kasper"]
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "ruby_process"
|
5
|
+
|
6
|
+
fpath = "/tmp/somefile"
|
7
|
+
RubyProcess.new.spawn_process do |rp|
|
8
|
+
#Opens file in subprocess.
|
9
|
+
rp.static(:File, :open, fpath, "w") do |fp|
|
10
|
+
#Writes to file in subprocess.
|
11
|
+
fp.write("Test!")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
print "Content of '#{fpath}': #{File.read(fpath)}\n"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#This example shows how to dump a database using 10 processes to do so (and effectivly use 10 cores).
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "knjrbfw"
|
7
|
+
require "ruby_process"
|
8
|
+
|
9
|
+
#Holds the 'db_settings'-global-variable.
|
10
|
+
require "#{Knj::Os.homedir}/example_knj_db_dump_settings.rb"
|
11
|
+
|
12
|
+
#Create database-connection.
|
13
|
+
db = Knj::Db.new($db_settings)
|
14
|
+
|
15
|
+
#Get list of databases.
|
16
|
+
tables = db.tables.list.values
|
17
|
+
|
18
|
+
tables_per_thread = (tables.length.to_f / 10.0).ceil
|
19
|
+
print "Tables per thread: #{tables_per_thread}\n"
|
20
|
+
|
21
|
+
threads = []
|
22
|
+
1.upto(1) do |i|
|
23
|
+
threads << Thread.new do
|
24
|
+
begin
|
25
|
+
thread_tables = tables.shift(tables_per_thread)
|
26
|
+
|
27
|
+
RubyProcess.new(debug: true).spawn_process do |rp|
|
28
|
+
rp.static(:Object, :require, "rubygems")
|
29
|
+
rp.static(:Object, :require, "knjrbfw")
|
30
|
+
|
31
|
+
fpath = "/tmp/dbdump_#{i}.sql"
|
32
|
+
|
33
|
+
thread_tables.each do |thread_db|
|
34
|
+
rp_db = rp.new("Knj::Db", $db_settings)
|
35
|
+
rp_dump = rp.new("Knj::Db::Dump", db: rp_db, tables: thread_tables)
|
36
|
+
|
37
|
+
rp.static(:File, :open, fpath, "w") do |rp_fp|
|
38
|
+
print "#{i} dumping #{thread_db}\n"
|
39
|
+
rp_dump.dump(rp_fp)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
puts e.inspect
|
45
|
+
puts e.backtrace
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
threads.each do |thread|
|
51
|
+
thread.join
|
52
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "ruby_process"
|
5
|
+
|
6
|
+
RubyProcess.new.spawn_process do |rp|
|
7
|
+
#Spawns string in the subprocess.
|
8
|
+
str = rp.new(:String, "Kasper is 26 years old")
|
9
|
+
|
10
|
+
#Scans with regex in subprocess, but yields proxy-objects in the current process.
|
11
|
+
str.scan(/is (\d+) years old/) do |match|
|
12
|
+
puts match.__rp_marshal
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class RubyProcess
|
2
|
+
#Recursivly parses arrays and hashes into proxy-object-hashes.
|
3
|
+
def parse_args(args)
|
4
|
+
if args.is_a?(Array)
|
5
|
+
newarr = []
|
6
|
+
args.each do |val|
|
7
|
+
newarr << parse_args(val)
|
8
|
+
end
|
9
|
+
|
10
|
+
return newarr
|
11
|
+
elsif args.is_a?(Hash)
|
12
|
+
newh = {}
|
13
|
+
args.each do |key, val|
|
14
|
+
newh[parse_args(key)] = parse_args(val)
|
15
|
+
end
|
16
|
+
|
17
|
+
return newh
|
18
|
+
elsif @args_allowed.index(args.class) != nil
|
19
|
+
debug "Allowing type '#{args.class}' as an argument: '#{args}'.\n" if @debug
|
20
|
+
return args
|
21
|
+
else
|
22
|
+
debug "Not allowing type '#{args.class}' as an argument - proxy object will be used.\n" if @debug
|
23
|
+
return handle_return_object(args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
#Returns a special hash instead of an actual object. Some objects will be returned in their normal form (true, false and nil).
|
30
|
+
def handle_return_object(obj, pid = @my_pid)
|
31
|
+
#Dont proxy these objects.
|
32
|
+
return obj if obj.is_a?(TrueClass) or obj.is_a?(FalseClass) or obj.is_a?(NilClass)
|
33
|
+
|
34
|
+
#The object is a proxy-obj - just return its arguments that contains the true 'my_pid'.
|
35
|
+
if obj.is_a?(RubyProcess::ProxyObject)
|
36
|
+
debug "Returning from proxy-obj: (ID: #{obj.args[:id]}, PID: #{obj.__rp_pid}).\n" if @debug
|
37
|
+
return {type: :proxy_obj, id: obj.__rp_id, pid: obj.__rp_pid}
|
38
|
+
end
|
39
|
+
|
40
|
+
#Check if object has already been spawned. If not: spawn id. Then returns hash for it.
|
41
|
+
id = obj.__id__
|
42
|
+
@objects[id] = obj if !@objects.key?(id)
|
43
|
+
|
44
|
+
debug "Proxy-object spawned (ID: #{id}, PID: #{pid}).\n" if @debug
|
45
|
+
return {type: :proxy_obj, id: id, pid: pid}
|
46
|
+
end
|
47
|
+
|
48
|
+
#Parses an argument array to proxy-object-hashes.
|
49
|
+
def handle_return_args(arr)
|
50
|
+
newa = []
|
51
|
+
arr.each do |obj|
|
52
|
+
newa << handle_return_object(obj)
|
53
|
+
end
|
54
|
+
|
55
|
+
return newa
|
56
|
+
end
|
57
|
+
|
58
|
+
#Recursivly scans arrays and hashes for proxy-object-hashes and replaces them with actual proxy-objects.
|
59
|
+
def read_args(args)
|
60
|
+
if args.is_a?(Array)
|
61
|
+
newarr = []
|
62
|
+
args.each do |val|
|
63
|
+
newarr << read_args(val)
|
64
|
+
end
|
65
|
+
|
66
|
+
return newarr
|
67
|
+
elsif args.is_a?(Hash) && args.length == 3 && args[:type] == :proxy_obj && args.key?(:id) && args.key?(:pid)
|
68
|
+
debug "Comparing PID (#{args[:pid]}, #{@my_pid}).\n" if @debug
|
69
|
+
|
70
|
+
if args[:pid] == @my_pid
|
71
|
+
debug "Same!\n" if @debug
|
72
|
+
return proxyobj_object(args[:id])
|
73
|
+
else
|
74
|
+
debug "Not same!\n" if @debug
|
75
|
+
return proxyobj_get(args[:id], args[:pid])
|
76
|
+
end
|
77
|
+
elsif args.is_a?(Hash)
|
78
|
+
newh = {}
|
79
|
+
args.each do |key, val|
|
80
|
+
newh[read_args(key)] = read_args(val)
|
81
|
+
end
|
82
|
+
|
83
|
+
return newh
|
84
|
+
end
|
85
|
+
|
86
|
+
return args
|
87
|
+
end
|
88
|
+
end
|
data/lib/ruby_process.rb
ADDED
@@ -0,0 +1,458 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "wref" unless Kernel.const_defined?(:Wref)
|
3
|
+
require "tsafe" unless Kernel.const_defined?(:Tsafe)
|
4
|
+
require "base64"
|
5
|
+
require "string-cases"
|
6
|
+
require "thread"
|
7
|
+
require "timeout"
|
8
|
+
|
9
|
+
#This class can communicate with another Ruby-process. It tries to integrate the work in the other process as seamless as possible by using proxy-objects.
|
10
|
+
class RubyProcess
|
11
|
+
attr_reader :finalize_count, :pid
|
12
|
+
|
13
|
+
#Require all the different commands.
|
14
|
+
dir = "#{File.dirname(__FILE__)}/../cmds"
|
15
|
+
Dir.foreach(dir) do |file|
|
16
|
+
require "#{dir}/#{file}" if file =~ /\.rb$/
|
17
|
+
end
|
18
|
+
|
19
|
+
#Methods for handeling arguments and proxy-objects in arguments.
|
20
|
+
require "#{File.dirname(__FILE__)}/../include/args_handeling.rb"
|
21
|
+
|
22
|
+
#Autoloader for subclasses.
|
23
|
+
def self.const_missing(name)
|
24
|
+
file_path = "#{::File.realpath(::File.dirname(__FILE__))}/ruby_process/#{::StringCases.camel_to_snake(name)}.rb"
|
25
|
+
|
26
|
+
if File.exists?(file_path)
|
27
|
+
require file_path
|
28
|
+
return const_get(name) if const_defined?(name)
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
#Constructor.
|
35
|
+
#===Examples
|
36
|
+
# RubyProcess.new.spawn_process do |rp|
|
37
|
+
# str = rp.new(:String, "Kasper")
|
38
|
+
# end
|
39
|
+
def initialize(args = {})
|
40
|
+
@args = args
|
41
|
+
@debug = @args[:debug]
|
42
|
+
@pid = @args[:pid]
|
43
|
+
|
44
|
+
#These classes are allowed in call-arguments. They can be marshalled without any errors.
|
45
|
+
@args_allowed = [FalseClass, Fixnum, Integer, NilClass, String, Symbol, TrueClass]
|
46
|
+
|
47
|
+
#Set IO variables if given.
|
48
|
+
@io_out = Tsafe::Proxy.new(obj: @args[:out]) if @args[:out]
|
49
|
+
@io_in = @args[:in] if @args[:in]
|
50
|
+
@io_err = @args[:err] if @args[:err]
|
51
|
+
|
52
|
+
#This hash holds answers coming from the subprocess.
|
53
|
+
@answers = Tsafe::MonHash.new
|
54
|
+
|
55
|
+
#This hash holds objects that are referenced in the process.
|
56
|
+
@objects = Tsafe::MonHash.new
|
57
|
+
|
58
|
+
#This weak-map holds all proxy objects.
|
59
|
+
@proxy_objs = Wref_map.new
|
60
|
+
@proxy_objs_ids = Tsafe::MonHash.new
|
61
|
+
@proxy_objs_unsets = Tsafe::MonArray.new
|
62
|
+
@flush_mutex = Mutex.new
|
63
|
+
@finalize_count = 0
|
64
|
+
|
65
|
+
#Send ID is used to identify the correct answers.
|
66
|
+
@send_mutex = Mutex.new
|
67
|
+
@send_count = 0
|
68
|
+
|
69
|
+
#The PID is used to know which process proxy-objects belongs to.
|
70
|
+
@my_pid = Process.pid
|
71
|
+
end
|
72
|
+
|
73
|
+
#Spawns a new process in the same Ruby-inteterpeter as the current one.
|
74
|
+
#===Examples
|
75
|
+
# rp = RubyProcess.new.spawn_process
|
76
|
+
# rp.str_eval("return 10").__rp_marshal #=> 10
|
77
|
+
# rp.destroy
|
78
|
+
def spawn_process(args = nil)
|
79
|
+
#Used for printing debug-stuff.
|
80
|
+
@main = true
|
81
|
+
|
82
|
+
if args && args[:exec]
|
83
|
+
cmd = "#{args[:exec]}"
|
84
|
+
else
|
85
|
+
if !ENV["rvm_ruby_string"].to_s.empty?
|
86
|
+
cmd = "#{ENV["rvm_ruby_string"]}"
|
87
|
+
else
|
88
|
+
cmd = "ruby"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
cmd << " \"#{File.realpath(File.dirname(__FILE__))}/../scripts/ruby_process_script.rb\" --pid=#{@my_pid}"
|
93
|
+
cmd << " --debug" if @args[:debug]
|
94
|
+
cmd << " \"--title=#{@args[:title]}\"" if !@args[:title].to_s.strip.empty?
|
95
|
+
|
96
|
+
#Start process and set IO variables.
|
97
|
+
require "open3"
|
98
|
+
@io_out, @io_in, @io_err = Open3.popen3(cmd)
|
99
|
+
@io_out = Tsafe::Proxy.new(obj: @io_out)
|
100
|
+
@io_out.sync = true
|
101
|
+
@io_in.sync = true
|
102
|
+
@io_err.sync = true
|
103
|
+
|
104
|
+
started = false
|
105
|
+
@io_in.each_line do |str|
|
106
|
+
if str == "ruby_process_started\n"
|
107
|
+
started = true
|
108
|
+
break
|
109
|
+
end
|
110
|
+
|
111
|
+
debug "Ruby-process-debug from stdout before started: '#{str}'\n" if @debug
|
112
|
+
end
|
113
|
+
|
114
|
+
raise "Ruby-sub-process couldnt start: '#{@io_err.read}'." unless started
|
115
|
+
self.listen
|
116
|
+
|
117
|
+
#Start by getting the PID of the process.
|
118
|
+
begin
|
119
|
+
@pid = self.static(:Process, :pid).__rp_marshal
|
120
|
+
raise "Unexpected PID: '#{@pid}'." if !@pid.is_a?(Fixnum) && !@pid.is_a?(Integer)
|
121
|
+
rescue => e
|
122
|
+
self.destroy
|
123
|
+
raise e
|
124
|
+
end
|
125
|
+
|
126
|
+
if block_given?
|
127
|
+
begin
|
128
|
+
yield(self)
|
129
|
+
ensure
|
130
|
+
self.destroy
|
131
|
+
end
|
132
|
+
|
133
|
+
return self
|
134
|
+
else
|
135
|
+
return self
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
#Starts listening on the given IO's. It is useally not needed to call this method manually.
|
140
|
+
def listen
|
141
|
+
#Start listening for input.
|
142
|
+
start_listen
|
143
|
+
|
144
|
+
#Start listening for errors.
|
145
|
+
start_listen_errors
|
146
|
+
end
|
147
|
+
|
148
|
+
#First tries to make the sub-process exit gently. Then kills it with "TERM" and 9 afterwards to make sure its dead. If 'spawn_process' is given a block, this method is automatically ensured after the block is run.
|
149
|
+
def destroy
|
150
|
+
return nil if self.destroyed?
|
151
|
+
|
152
|
+
debug "Destroying Ruby-process (#{caller}).\n" if @debug
|
153
|
+
pid = @pid
|
154
|
+
tries = 0
|
155
|
+
|
156
|
+
#Make main kill it and make sure its dead...
|
157
|
+
begin
|
158
|
+
if @main && @pid
|
159
|
+
tries += 1
|
160
|
+
Process.kill("TERM", pid) rescue Errno::ESRCH
|
161
|
+
|
162
|
+
#Ensure subprocess is dead.
|
163
|
+
begin
|
164
|
+
Timeout.timeout(1) do
|
165
|
+
sleep 0.01
|
166
|
+
|
167
|
+
loop do
|
168
|
+
begin
|
169
|
+
Process.getpgid(pid)
|
170
|
+
alive = true
|
171
|
+
rescue Errno::ESRCH
|
172
|
+
alive = false
|
173
|
+
end
|
174
|
+
|
175
|
+
break if !alive
|
176
|
+
end
|
177
|
+
end
|
178
|
+
rescue Timeout::Error
|
179
|
+
Process.kill(9, pid) rescue Errno::ESRCH
|
180
|
+
retry
|
181
|
+
end
|
182
|
+
end
|
183
|
+
rescue Errno::ESRCH
|
184
|
+
#Process is already dead - ignore.
|
185
|
+
ensure
|
186
|
+
@pid = nil
|
187
|
+
|
188
|
+
@io_out.close if @io_out && !@io_out.closed?
|
189
|
+
@io_out = nil
|
190
|
+
|
191
|
+
@io_in.close if @io_in && !@io_in.closed?
|
192
|
+
@io_in = nil
|
193
|
+
|
194
|
+
@io_err if @io_err && !@io_err.closed?
|
195
|
+
@io_err = nil
|
196
|
+
|
197
|
+
@main = nil
|
198
|
+
end
|
199
|
+
|
200
|
+
return nil
|
201
|
+
end
|
202
|
+
|
203
|
+
#Returns true if the Ruby process has been destroyed.
|
204
|
+
def destroyed?
|
205
|
+
return true if !@pid && !@io_out && !@io_in && !@io_err && @main == nil
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
|
209
|
+
#Joins the listen thread and error-thread. This is useually only called on the sub-process side, but can also be useful, if you are waiting for a delayed callback from the subprocess.
|
210
|
+
def join
|
211
|
+
debug "Joining listen-thread.\n" if @debug
|
212
|
+
@thr_listen.join if @thr_listen
|
213
|
+
raise @listen_err if @listen_err
|
214
|
+
|
215
|
+
debug "Joining error-thread.\n" if @debug
|
216
|
+
@thr_err.join if @thr_join
|
217
|
+
raise @listen_err_err if @listen_err_err
|
218
|
+
end
|
219
|
+
|
220
|
+
#Sends a command to the other process. This should not be called manually, but is used by various other parts of the framework.
|
221
|
+
def send(obj, &block)
|
222
|
+
alive_check!
|
223
|
+
|
224
|
+
#Sync ID stuff so they dont get mixed up.
|
225
|
+
id = nil
|
226
|
+
@send_mutex.synchronize do
|
227
|
+
id = @send_count
|
228
|
+
@send_count += 1
|
229
|
+
end
|
230
|
+
|
231
|
+
#Parse block.
|
232
|
+
if block
|
233
|
+
block_proxy_res = self.send(cmd: :spawn_proxy_block, id: block.__id__, answer_id: id)
|
234
|
+
block_proxy_res_id = block_proxy_res[:id]
|
235
|
+
raise "No block ID was returned?" unless block_proxy_res_id
|
236
|
+
raise "Invalid block-ID: '#{block_proxy_res_id}'." if block_proxy_res_id.to_i <= 0
|
237
|
+
@proxy_objs[block_proxy_res_id] = block
|
238
|
+
@proxy_objs_ids[block.__id__] = block_proxy_res_id
|
239
|
+
ObjectSpace.define_finalizer(block, self.method(:proxyobj_finalizer))
|
240
|
+
obj[:block] = {
|
241
|
+
id: block_proxy_res_id,
|
242
|
+
arity: block.arity
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
246
|
+
flush_finalized unless obj[:cmd] == :flush_finalized
|
247
|
+
|
248
|
+
debug "Sending(#{id}): #{obj}\n" if @debug
|
249
|
+
line = Base64.strict_encode64(Marshal.dump(
|
250
|
+
id: id,
|
251
|
+
type: :send,
|
252
|
+
obj: obj
|
253
|
+
))
|
254
|
+
|
255
|
+
begin
|
256
|
+
@answers[id] = Queue.new
|
257
|
+
@io_out.puts(line)
|
258
|
+
return answer_read(id)
|
259
|
+
ensure
|
260
|
+
#Be sure that the answer is actually deleted to avoid memory-leaking.
|
261
|
+
@answers.delete(id)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
#Returns true if the child process is still running. Otherwise false.
|
266
|
+
def alive?
|
267
|
+
begin
|
268
|
+
alive_check!
|
269
|
+
return true
|
270
|
+
rescue
|
271
|
+
return false
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
private
|
276
|
+
|
277
|
+
#Raises an error if the subprocess is no longer alive.
|
278
|
+
def alive_check!
|
279
|
+
raise "Has been destroyed." if self.destroyed?
|
280
|
+
raise "No 'io_out'." if !@io_out
|
281
|
+
raise "No 'io_in'." if !@io_in
|
282
|
+
raise "'io_in' was closed." if @io_in.closed?
|
283
|
+
raise "No listen thread." if !@thr_listen
|
284
|
+
#raise "Listen thread wasnt alive?" if !@thr_listen.alive?
|
285
|
+
|
286
|
+
return nil
|
287
|
+
end
|
288
|
+
|
289
|
+
#Prints the given string to stderr. Raises error if debugging is not enabled.
|
290
|
+
def debug(str_full)
|
291
|
+
raise "Debug not enabled?" unless @debug
|
292
|
+
|
293
|
+
str_full.each_line do |str|
|
294
|
+
if @main
|
295
|
+
$stderr.print "(M#{@my_pid}) #{str}"
|
296
|
+
else
|
297
|
+
$stderr.print "(S#{@my_pid}) #{str}"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
#Registers an object ID as a proxy-object on the host-side.
|
303
|
+
def proxyobj_get(id, pid = @my_pid)
|
304
|
+
if proxy_obj = @proxy_objs.get!(id)
|
305
|
+
debug "Reuse proxy-obj (ID: #{id}, PID: #{pid}, fID: #{proxy_obj.args[:id]}, fPID: #{proxy_obj.args[:pid]})\n" if @debug
|
306
|
+
return proxy_obj
|
307
|
+
end
|
308
|
+
|
309
|
+
@proxy_objs_unsets.delete(id)
|
310
|
+
proxy_obj = RubyProcess::ProxyObject.new(self, id, pid)
|
311
|
+
@proxy_objs[id] = proxy_obj
|
312
|
+
@proxy_objs_ids[proxy_obj.__id__] = id
|
313
|
+
ObjectSpace.define_finalizer(proxy_obj, self.method(:proxyobj_finalizer))
|
314
|
+
|
315
|
+
return proxy_obj
|
316
|
+
end
|
317
|
+
|
318
|
+
#Returns the saved proxy-object by the given ID. Raises error if it doesnt exist.
|
319
|
+
def proxyobj_object(id)
|
320
|
+
if obj = @objects[id]
|
321
|
+
return obj
|
322
|
+
end
|
323
|
+
|
324
|
+
raise "No object by that ID: '#{id}' (#{@objects})."
|
325
|
+
end
|
326
|
+
|
327
|
+
#Method used for detecting garbage-collected proxy-objects. This way we can also free references to them in the other process, so it doesnt run out of memory.
|
328
|
+
def proxyobj_finalizer(id)
|
329
|
+
debug "Finalized #{id}\n" if @debug
|
330
|
+
proxy_id = @proxy_objs_ids[id]
|
331
|
+
|
332
|
+
if !proxy_id
|
333
|
+
debug "No such ID in proxy objects IDs hash: '#{id}'.\n" if @debug
|
334
|
+
else
|
335
|
+
@proxy_objs_unsets << proxy_id
|
336
|
+
debug "Done finalizing #{id}\n" if @debug
|
337
|
+
end
|
338
|
+
|
339
|
+
return nil
|
340
|
+
end
|
341
|
+
|
342
|
+
#Waits for an answer to appear in the answers-hash. Then deletes it from hash and returns it.
|
343
|
+
def answer_read(id)
|
344
|
+
loop do
|
345
|
+
debug "Waiting for answer #{id}\n" if @debug
|
346
|
+
answer = @answers[id].pop
|
347
|
+
debug "Returning answer #{id}\n" if @debug
|
348
|
+
|
349
|
+
if answer.is_a?(Hash) and type = answer[:type]
|
350
|
+
if type == :error and class_str = answer[:class] and msg = answer[:msg] and bt_orig = answer[:bt]
|
351
|
+
begin
|
352
|
+
raise "#{class_str}: #{msg}"
|
353
|
+
rescue => e
|
354
|
+
bt = []
|
355
|
+
bt_orig.each do |btline|
|
356
|
+
bt << "(#{@pid}): #{btline}"
|
357
|
+
end
|
358
|
+
|
359
|
+
bt += e.backtrace
|
360
|
+
e.set_backtrace(bt)
|
361
|
+
raise e
|
362
|
+
end
|
363
|
+
elsif type == :proxy_obj && id = answer[:id] and pid = answer[:pid]
|
364
|
+
return proxyobj_get(id, pid)
|
365
|
+
elsif type == :proxy_block_call and block = answer[:block] and args = answer[:args] and queue = answer[:queue]
|
366
|
+
#Calls the block. This is used to call the block from the same thread that the answer is being read from. This can cause problems in Hayabusa, that uses thread-variables to determine output and such.
|
367
|
+
block.call(*args)
|
368
|
+
|
369
|
+
#Tells the parent thread that the block has been executed and it should continue.
|
370
|
+
queue << true
|
371
|
+
else
|
372
|
+
return answer
|
373
|
+
end
|
374
|
+
else
|
375
|
+
return answer
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
raise "This should never be reached."
|
380
|
+
end
|
381
|
+
|
382
|
+
#Starts the listen-thread that listens for, and executes, commands. This is normally automatically called and should not be called manually.
|
383
|
+
def start_listen
|
384
|
+
@thr_listen = Thread.new do
|
385
|
+
begin
|
386
|
+
@io_in.each_line do |line|
|
387
|
+
raise "No line?" if !line || line.to_s.strip.empty?
|
388
|
+
alive_check!
|
389
|
+
debug "Received: #{line}" if @debug
|
390
|
+
|
391
|
+
begin
|
392
|
+
obj = Marshal.load(Base64.strict_decode64(line.strip))
|
393
|
+
debug "Object received: #{obj}\n" if @debug
|
394
|
+
rescue => e
|
395
|
+
$stderr.puts "Base64Str: #{line}" if @debug
|
396
|
+
$stderr.puts e.inspect if @debug
|
397
|
+
$stderr.puts e.backtrace if @debug
|
398
|
+
|
399
|
+
raise e
|
400
|
+
end
|
401
|
+
|
402
|
+
id = obj[:id]
|
403
|
+
obj_type = obj[:type]
|
404
|
+
|
405
|
+
if obj_type == :send
|
406
|
+
Thread.new do
|
407
|
+
#Hack to be able to do callbacks from same thread when using blocks. This should properly be cleaned a bit up in the future.
|
408
|
+
obj[:obj][:send_id] = id
|
409
|
+
|
410
|
+
begin
|
411
|
+
raise "Object was not a hash." unless obj.is_a?(Hash)
|
412
|
+
raise "No ID was given?" unless id
|
413
|
+
res = self.__send__("cmd_#{obj[:obj][:cmd]}", obj[:obj])
|
414
|
+
rescue Exception => e
|
415
|
+
raise e if e.is_a?(SystemExit) || e.is_a?(Interrupt)
|
416
|
+
res = {type: :error, class: e.class.name, msg: e.message, bt: e.backtrace}
|
417
|
+
end
|
418
|
+
|
419
|
+
data = Base64.strict_encode64(Marshal.dump(type: :answer, id: id, answer: res))
|
420
|
+
@io_out.puts(data)
|
421
|
+
end
|
422
|
+
elsif obj_type == :answer
|
423
|
+
if answer_queue = @answers[id]
|
424
|
+
debug "Answer #{id} saved.\n" if @debug
|
425
|
+
answer_queue << obj[:answer]
|
426
|
+
elsif @debug
|
427
|
+
debug "No answer-queue could be found for ID #{id}."
|
428
|
+
end
|
429
|
+
else
|
430
|
+
raise "Unknown object: '#{obj}'."
|
431
|
+
end
|
432
|
+
end
|
433
|
+
rescue => e
|
434
|
+
if @debug
|
435
|
+
debug "Error while listening: #{e.inspect}"
|
436
|
+
debug e.backtrace.join("\n") + "\n"
|
437
|
+
end
|
438
|
+
|
439
|
+
@listen_err = e
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
#Starts the listen thread that outputs the 'stderr' for the other process on this process's 'stderr'.
|
445
|
+
def start_listen_errors
|
446
|
+
return nil if !@io_err
|
447
|
+
|
448
|
+
@thr_err = Thread.new do
|
449
|
+
begin
|
450
|
+
@io_err.each_line do |str|
|
451
|
+
$stderr.print str if @debug
|
452
|
+
end
|
453
|
+
rescue => e
|
454
|
+
@listen_err_err = e
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|