File: subexec.rb

package info (click to toggle)
ruby-subexec 0.2.3+gh-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, buster, jessie, jessie-kfreebsd, sid, stretch
  • size: 116 kB
  • ctags: 6
  • sloc: ruby: 156; sh: 3; makefile: 2
file content (125 lines) | stat: -rw-r--r-- 3,087 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
72
73
74
75
76
77
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
# # Subexec
# * by Peter Kieltyka
# * http://github/nulayer/subexec
#
# ## Description
#
# Subexec is a simple library that spawns an external command with
# an optional timeout parameter. It relies on Ruby 1.9's Process.spawn
# method. Also, it works with synchronous and asynchronous code.
#
# Useful for libraries that are Ruby wrappers for CLI's. For example,
# resizing images with ImageMagick's mogrify command sometimes stalls
# and never returns control back to the original process. Subexec
# executes mogrify and preempts if it gets lost.
#
# ## Usage
#
# # Print hello
# sub = Subexec.run "echo 'hello' && sleep 3", :timeout => 5
# puts sub.output     # returns: hello
# puts sub.exitstatus # returns: 0
#
# # Timeout process after a second
# sub = Subexec.run "echo 'hello' && sleep 3", :timeout => 1
# puts sub.output     # returns:
# puts sub.exitstatus # returns:

class Subexec
  VERSION = '0.2.3'

  attr_accessor :pid,
                :command,
                :lang,
                :output,
                :exitstatus,
                :timeout,
                :log_file

  def self.run(command, options={})
    sub = new(command, options)
    sub.run!
    sub
  end

  def initialize(command, options={})
    self.command    = command
    self.lang       = options[:lang]      || "C"
    self.timeout    = options[:timeout]   || -1     # default is to never timeout
    self.log_file   = options[:log_file]
    self.exitstatus = 0
  end

  def run!
    if RUBY_VERSION >= '1.9' && RUBY_ENGINE != 'jruby'
      spawn
    else
      exec
    end
  end


  private

    def spawn
      # TODO: weak implementation for log_file support.
      # Ideally, the data would be piped through to both descriptors
      r, w = IO.pipe

      log_to_file = !log_file.nil?
      log_opts = log_to_file ? {[:out, :err] => [log_file, 'a']} : {STDERR=>w, STDOUT=>w}
      self.pid = Process.spawn({'LANG' => self.lang}, command, log_opts)
      w.close

      @timer = Time.now + timeout
      timed_out = false

      self.output = ''

      append_to_output = Proc.new do
        self.output << r.readlines.join('')  unless log_to_file
      end

      loop do
        ret = begin
          Process.waitpid(pid, Process::WUNTRACED|Process::WNOHANG)
        rescue Errno::ECHILD
          break
        end

        break if ret == pid

        append_to_output.call

        if timeout > 0 && Time.now > @timer
          timed_out = true
          break
        end

        sleep 0.01
      end

      if timed_out
        # The subprocess timed out -- kill it
        Process.kill(9, pid) rescue Errno::ESRCH
        self.exitstatus = nil
      else
        # The subprocess exited on its own
        self.exitstatus = $?.exitstatus
        append_to_output.call
      end
      r.close

      self
    end

    def exec
      if !(RUBY_PLATFORM =~ /win32|mswin|mingw/).nil?
        self.output = `set LANG=#{lang} && #{command} 2>&1`
      else
        self.output = `LANG=#{lang} && export LANG && #{command} 2>&1`
      end
      self.exitstatus = $?.exitstatus
    end

end