Class: RubyProcess

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_process.rb

Overview

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.

Defined Under Namespace

Classes: ClassProxy, ProxyObject

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ RubyProcess

Constructor.

Examples

RubyProcess.new.spawn_process do |rp|

str = rp.new(:String, "Kasper")

end



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/ruby_process.rb', line 39

def initialize(args = {})
  @args = args
  @debug = @args[:debug]
  @pid = @args[:pid]

  #These classes are allowed in call-arguments. They can be marshalled without any errors.
  @args_allowed = [FalseClass, Fixnum, Integer, NilClass, String, Symbol, TrueClass]

  #Set IO variables if given.
  @io_out = Tsafe::Proxy.new(obj: @args[:out]) if @args[:out]
  @io_in = @args[:in] if @args[:in]
  @io_err = @args[:err] if @args[:err]

  #This hash holds answers coming from the subprocess.
  @answers = Tsafe::MonHash.new

  #This hash holds objects that are referenced in the process.
  @objects = Tsafe::MonHash.new

  #This weak-map holds all proxy objects.
  @proxy_objs = Wref_map.new
  @proxy_objs_ids = Tsafe::MonHash.new
  @proxy_objs_unsets = Tsafe::MonArray.new
  @flush_mutex = Mutex.new
  @finalize_count = 0

  #Send ID is used to identify the correct answers.
  @send_mutex = Mutex.new
  @send_count = 0

  #The PID is used to know which process proxy-objects belongs to.
  @my_pid = Process.pid
end

Instance Attribute Details

#finalize_countObject (readonly)

Returns the value of attribute finalize_count.



11
12
13
# File 'lib/ruby_process.rb', line 11

def finalize_count
  @finalize_count
end

#pidObject (readonly)

Returns the value of attribute pid.



11
12
13
# File 'lib/ruby_process.rb', line 11

def pid
  @pid
end

Class Method Details

.const_missing(name) ⇒ Object

Autoloader for subclasses.



23
24
25
26
27
28
29
30
31
32
# File 'lib/ruby_process.rb', line 23

def self.const_missing(name)
  file_path = "#{::File.realpath(::File.dirname(__FILE__))}/ruby_process/#{::StringCases.camel_to_snake(name)}.rb"

  if File.exists?(file_path)
    require file_path
    return const_get(name) if const_defined?(name)
  end

  super
end

Instance Method Details

#alive?Boolean

Returns true if the child process is still running. Otherwise false.

Returns:

  • (Boolean)


266
267
268
269
270
271
272
273
# File 'lib/ruby_process.rb', line 266

def alive?
  begin
    alive_check!
    return true
  rescue
    return false
  end
end

#destroyObject

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/ruby_process.rb', line 149

def destroy
  return nil if self.destroyed?

  debug "Destroying Ruby-process (#{caller}).\n" if @debug
  pid = @pid
  tries = 0

  #Make main kill it and make sure its dead...
  begin
    if @main && @pid
      tries += 1
      Process.kill("TERM", pid) rescue Errno::ESRCH

      #Ensure subprocess is dead.
      begin
        Timeout.timeout(1) do
          sleep 0.01

          loop do
            begin
              Process.getpgid(pid)
              alive = true
            rescue Errno::ESRCH
              alive = false
            end

            break if !alive
          end
        end
      rescue Timeout::Error
        Process.kill(9, pid) rescue Errno::ESRCH
        retry
      end
    end
  rescue Errno::ESRCH
    #Process is already dead - ignore.
  ensure
    @pid = nil

    @io_out.close if @io_out && !@io_out.closed?
    @io_out = nil

    @io_in.close if @io_in && !@io_in.closed?
    @io_in = nil

    @io_err if @io_err && !@io_err.closed?
    @io_err = nil

    @main = nil
  end

  return nil
end

#destroyed?Boolean

Returns true if the Ruby process has been destroyed.

Returns:

  • (Boolean)


204
205
206
207
# File 'lib/ruby_process.rb', line 204

def destroyed?
  return true if !@pid && !@io_out && !@io_in && !@io_err && @main == nil
  return false
end

#joinObject

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.

Raises:

  • (@listen_err)


210
211
212
213
214
215
216
217
218
# File 'lib/ruby_process.rb', line 210

def join
  debug "Joining listen-thread.\n" if @debug
  @thr_listen.join if @thr_listen
  raise @listen_err if @listen_err

  debug "Joining error-thread.\n" if @debug
  @thr_err.join if @thr_join
  raise @listen_err_err if @listen_err_err
end

#listenObject

Starts listening on the given IO’s. It is useally not needed to call this method manually.



140
141
142
143
144
145
146
# File 'lib/ruby_process.rb', line 140

def listen
  #Start listening for input.
  start_listen

  #Start listening for errors.
  start_listen_errors
end

#send(obj, &block) ⇒ Object

Sends a command to the other process. This should not be called manually, but is used by various other parts of the framework.



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/ruby_process.rb', line 221

def send(obj, &block)
  alive_check!

  #Sync ID stuff so they dont get mixed up.
  id = nil
  @send_mutex.synchronize do
    id = @send_count
    @send_count += 1
  end

  #Parse block.
  if block
    block_proxy_res = self.send(cmd: :spawn_proxy_block, id: block.__id__, answer_id: id)
    block_proxy_res_id = block_proxy_res[:id]
    raise "No block ID was returned?" unless block_proxy_res_id
    raise "Invalid block-ID: '#{block_proxy_res_id}'." if block_proxy_res_id.to_i <= 0
    @proxy_objs[block_proxy_res_id] = block
    @proxy_objs_ids[block.__id__] = block_proxy_res_id
    ObjectSpace.define_finalizer(block, self.method(:proxyobj_finalizer))
    obj[:block] = {
      id: block_proxy_res_id,
      arity: block.arity
    }
  end

  flush_finalized unless obj[:cmd] == :flush_finalized

  debug "Sending(#{id}): #{obj}\n" if @debug
  line = Base64.strict_encode64(Marshal.dump(
    id: id,
    type: :send,
    obj: obj
  ))

  begin
    @answers[id] = Queue.new
    @io_out.puts(line)
    return answer_read(id)
  ensure
    #Be sure that the answer is actually deleted to avoid memory-leaking.
    @answers.delete(id)
  end
end

#spawn_process(args = nil) ⇒ Object

Spawns a new process in the same Ruby-inteterpeter as the current one.

Examples

rp = RubyProcess.new.spawn_process rp.str_eval(“return 10”).__rp_marshal #=> 10 rp.destroy



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ruby_process.rb', line 78

def spawn_process(args = nil)
  #Used for printing debug-stuff.
  @main = true

  if args && args[:exec]
    cmd = "#{args[:exec]}"
  else
    if !ENV["rvm_ruby_string"].to_s.empty?
      cmd = "#{ENV["rvm_ruby_string"]}"
    else
      cmd = "ruby"
    end
  end

  cmd << " \"#{File.realpath(File.dirname(__FILE__))}/../scripts/ruby_process_script.rb\" --pid=#{@my_pid}"
  cmd << " --debug" if @args[:debug]
  cmd << " \"--title=#{@args[:title]}\"" if !@args[:title].to_s.strip.empty?

  #Start process and set IO variables.
  require "open3"
  @io_out, @io_in, @io_err = Open3.popen3(cmd)
  @io_out = Tsafe::Proxy.new(obj: @io_out)
  @io_out.sync = true
  @io_in.sync = true
  @io_err.sync = true

  started = false
  @io_in.each_line do |str|
    if str == "ruby_process_started\n"
      started = true
      break
    end

    debug "Ruby-process-debug from stdout before started: '#{str}'\n" if @debug
  end

  raise "Ruby-sub-process couldnt start: '#{@io_err.read}'." unless started
  self.listen

  #Start by getting the PID of the process.
  begin
    @pid = self.static(:Process, :pid).__rp_marshal
    raise "Unexpected PID: '#{@pid}'." if !@pid.is_a?(Fixnum) && !@pid.is_a?(Integer)
  rescue => e
    self.destroy
    raise e
  end

  if block_given?
    begin
      yield(self)
    ensure
      self.destroy
    end

    return self
  else
    return self
  end
end