Module: Vectory::Capture

Defined in:
lib/vectory/capture.rb

Class Method Summary collapse

Class Method Details

.with_timeout(*cmd) ⇒ Object

rubocop:disable all

Originally from gist.github.com/pasela/9392115

Capture the standard output and the standard error of a command. Almost same as Open3.capture3 method except for timeout handling and return value. See Open3.capture3.

result = capture3_with_timeout([env,] cmd... [, opts])

The arguments env, cmd and opts are passed to Process.spawn except opts, opts, opts, opts and opts. See Process.spawn.

If opts is specified, it is sent to the command’s standard input.

If opts is true, internal pipes are set to binary mode.

If opts is specified, SIGTERM is sent to the command after specified seconds.

If opts is specified, it is used instead of SIGTERM on timeout.

If opts is specified, also send a SIGKILL after specified seconds. it is only sent if the command is still running after the initial signal was sent.

The return value is a Hash as shown below.

{
  :pid     => PID of the command,
  :status  => Process::Status of the command,
  :stdout  => the standard output of the command,
  :stderr  => the standard error of the command,
  :timeout => whether the command was timed out,
}


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
# File 'lib/vectory/capture.rb', line 40

def with_timeout(*cmd)
  spawn_opts = Hash === cmd.last ? cmd.pop.dup : {}
  opts = {
    :stdin_data => spawn_opts.delete(:stdin_data) || "",
    :binmode    => spawn_opts.delete(:binmode) || false,
    :timeout    => spawn_opts.delete(:timeout),
    :signal     => spawn_opts.delete(:signal) || :TERM,
    :kill_after => spawn_opts.delete(:kill_after),
  }

  in_r,  in_w  = IO.pipe
  out_r, out_w = IO.pipe
  err_r, err_w = IO.pipe
  in_w.sync = true

  if opts[:binmode]
    in_w.binmode
    out_r.binmode
    err_r.binmode
  end

  spawn_opts[:in]  = in_r
  spawn_opts[:out] = out_w
  spawn_opts[:err] = err_w

  result = {
    :pid     => nil,
    :status  => nil,
    :stdout  => nil,
    :stderr  => nil,
    :timeout => false,
  }

  out_reader = nil
  err_reader = nil
  wait_thr = nil

  begin
    Timeout.timeout(opts[:timeout]) do
      result[:pid] = spawn(*cmd, spawn_opts)
      wait_thr = Process.detach(result[:pid])
      in_r.close
      out_w.close
      err_w.close

      out_reader = Thread.new { out_r.read }
      err_reader = Thread.new { err_r.read }

      in_w.write opts[:stdin_data]
      in_w.close

      result[:status] = wait_thr.value
    end
  rescue Timeout::Error
    result[:timeout] = true
    pid = spawn_opts[:pgroup] ? -result[:pid] : result[:pid]
    Process.kill(opts[:signal], pid)
    if opts[:kill_after]
      unless wait_thr.join(opts[:kill_after])
        Process.kill(:KILL, pid)
      end
    end
  ensure
    result[:status] = wait_thr.value if wait_thr
    result[:stdout] = out_reader.value if out_reader
    result[:stderr] = err_reader.value if err_reader
    out_r.close unless out_r.closed?
    err_r.close unless err_r.closed?
  end

  result
end