lib/vectory/utils.rb in vectory-0.2.0 vs lib/vectory/utils.rb in vectory-0.3.0

- old
+ new

@@ -1,7 +1,10 @@ # frozen_string_literal: true +require "marcel" +require "timeout" + module Vectory class Utils # Extracted from https://github.com/metanorma/metanorma-utils/blob/v1.5.2/lib/utils/image.rb class << self # sources/plantuml/plantuml20200524-90467-1iqek5i.png @@ -47,8 +50,116 @@ end def absolute_path?(uri) %r{^/}.match?(uri) || %r{^[A-Z]:/}.match?(uri) end + + # rubocop:disable all + # + # Originally from https://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[: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.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 + # rubocop:enable all end end end