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.
@@ -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
@@ -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