Skip to content

Instantly share code, notes, and snippets.

@pasela
Last active November 9, 2025 05:17
Show Gist options
  • Select an option

  • Save pasela/9392115 to your computer and use it in GitHub Desktop.

Select an option

Save pasela/9392115 to your computer and use it in GitHub Desktop.

Revisions

  1. pasela revised this gist Mar 13, 2014. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions capture3_with_timeout.rb
    Original file line number Diff line number Diff line change
    @@ -87,10 +87,11 @@ def capture3_with_timeout(*cmd)
    end
    rescue Timeout::Error
    result[:timeout] = true
    Process.kill(opts[:signal], result[:pid])
    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, result[:pid])
    Process.kill(:KILL, pid)
    end
    end
    ensure
  2. pasela revised this gist Mar 7, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion capture3_with_timeout.rb
    Original file line number Diff line number Diff line change
    @@ -70,7 +70,7 @@ def capture3_with_timeout(*cmd)
    wait_thr = nil

    begin
    timeout(opts[:timeout]) do
    Timeout.timeout(opts[:timeout]) do
    result[:pid] = spawn(*cmd, spawn_opts)
    wait_thr = Process.detach(result[:pid])
    in_r.close
  3. pasela created this gist Mar 6, 2014.
    105 changes: 105 additions & 0 deletions capture3_with_timeout.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    # encoding: utf-8

    require "timeout"

    # 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[:stdin_data], opts[:binmode], opts[:timeout], opts[:signal]
    # and opts[:kill_after]. See Process.spawn.
    #
    # If opts[:stdin_data] is specified, it is sent to the command's standard input.
    #
    # If opts[:binmode] is true, internal pipes are set to binary mode.
    #
    # If opts[:timeout] is specified, SIGTERM is sent to the command after specified seconds.
    #
    # If opts[:signal] is specified, it is used instead of SIGTERM on timeout.
    #
    # If opts[:kill_after] 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,
    # }
    def capture3_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(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
    Process.kill(opts[:signal], result[:pid])
    if opts[:kill_after]
    unless wait_thr.join(opts[:kill_after])
    Process.kill(:KILL, result[: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