Use SystemCommand for curl.

This commit is contained in:
Markus Reiter 2018-07-30 10:11:00 +02:00
parent 50ddcb8152
commit 8ae284e871
12 changed files with 95 additions and 61 deletions

View File

@ -106,10 +106,10 @@ module Hbc
LockFile.new(temporary_path.basename).with_lock do LockFile.new(temporary_path.basename).with_lock do
_fetch _fetch
end end
rescue ErrorDuringExecution rescue ErrorDuringExecution => e
# 33 == range not supported # 33 == range not supported
# try wiping the incomplete download and retrying once # try wiping the incomplete download and retrying once
if $CHILD_STATUS.exitstatus == 33 && had_incomplete_download if e.status.exitstatus == 33 && had_incomplete_download
ohai "Trying a full download" ohai "Trying a full download"
temporary_path.unlink temporary_path.unlink
had_incomplete_download = false had_incomplete_download = false

View File

@ -293,10 +293,7 @@ end
# Detect and download from Apache Mirror # Detect and download from Apache Mirror
class CurlApacheMirrorDownloadStrategy < CurlDownloadStrategy class CurlApacheMirrorDownloadStrategy < CurlDownloadStrategy
def apache_mirrors def apache_mirrors
mirrors, = Open3.capture3( mirrors, = curl_output(*_curl_opts, "--silent", "--location", "#{@url}&asjson=1")
*curl_args(*_curl_opts, "--silent", "--location", "#{@url}&asjson=1"),
)
JSON.parse(mirrors) JSON.parse(mirrors)
end end

View File

@ -526,7 +526,11 @@ end
# raised by safe_system in utils.rb # raised by safe_system in utils.rb
class ErrorDuringExecution < RuntimeError class ErrorDuringExecution < RuntimeError
attr_reader :status
def initialize(cmd, status:, output: nil) def initialize(cmd, status:, output: nil)
@status = status
s = "Failure while executing; `#{cmd.shelljoin.gsub(/\\=/, "=")}` exited with #{status.exitstatus}." s = "Failure while executing; `#{cmd.shelljoin.gsub(/\\=/, "=")}` exited with #{status.exitstatus}."
unless [*output].empty? unless [*output].empty?

View File

@ -34,10 +34,10 @@ class SystemCommand
each_output_line do |type, line| each_output_line do |type, line|
case type case type
when :stdout when :stdout
puts line.chomp if print_stdout? $stdout << line if print_stdout?
@merged_output << [:stdout, line] @merged_output << [:stdout, line]
when :stderr when :stderr
$stderr.puts Formatter.error(line.chomp) if print_stderr? $stderr << line if print_stderr?
@merged_output << [:stderr, line] @merged_output << [:stderr, line]
end end
end end
@ -74,15 +74,16 @@ class SystemCommand
attr_predicate :sudo?, :print_stdout?, :print_stderr?, :must_succeed? attr_predicate :sudo?, :print_stdout?, :print_stderr?, :must_succeed?
def env_args def env_args
return [] if env.empty? set_variables = env.reject { |_, value| value.nil? }
.map do |name, value|
sanitized_name = Shellwords.escape(name)
sanitized_value = Shellwords.escape(value)
"#{sanitized_name}=#{sanitized_value}"
end
variables = env.map do |name, value| return [] if set_variables.empty?
sanitized_name = Shellwords.escape(name)
sanitized_value = Shellwords.escape(value)
"#{sanitized_name}=#{sanitized_value}"
end
["env", *variables] ["env", *set_variables]
end end
def sudo_prefix def sudo_prefix
@ -102,7 +103,7 @@ class SystemCommand
@expanded_args ||= args.map do |arg| @expanded_args ||= args.map do |arg|
if arg.respond_to?(:to_path) if arg.respond_to?(:to_path)
File.absolute_path(arg) File.absolute_path(arg)
elsif arg.is_a?(Integer) || arg.is_a?(Float) elsif arg.is_a?(Integer) || arg.is_a?(Float) || arg.is_a?(URI)
arg.to_s arg.to_s
else else
arg.to_str arg.to_str
@ -114,7 +115,7 @@ class SystemCommand
executable, *args = command executable, *args = command
raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
Open3.popen3([executable, executable], *args, **options) Open3.popen3(env, executable, *args, **options)
write_input_to(raw_stdin) write_input_to(raw_stdin)
raw_stdin.close_write raw_stdin.close_write
@ -157,23 +158,28 @@ class SystemCommand
hash[type] << line hash[type] << line
end end
Result.new(command, output[:stdout], output[:stderr], @status.exitstatus) Result.new(command, output[:stdout], output[:stderr], @status)
end end
class Result class Result
attr_accessor :command, :stdout, :stderr, :exit_status attr_accessor :command, :stdout, :stderr, :status, :exit_status
def initialize(command, stdout, stderr, exit_status) def initialize(command, stdout, stderr, status)
@command = command @command = command
@stdout = stdout @stdout = stdout
@stderr = stderr @stderr = stderr
@exit_status = exit_status @status = status
@exit_status = status.exitstatus
end end
def success? def success?
@exit_status.zero? @exit_status.zero?
end end
def to_ary
[stdout, stderr, status]
end
def plist def plist
@plist ||= begin @plist ||= begin
output = stdout output = stdout

View File

@ -39,7 +39,7 @@ describe "download strategies", :cask do
let(:url_options) { { user_agent: "Mozilla/25.0.1" } } let(:url_options) { { user_agent: "Mozilla/25.0.1" } }
it "adds the appropriate curl args" do it "adds the appropriate curl args" do
expect(downloader).to receive(:safe_system) { |*args| expect(downloader).to receive(:system_command!) { |*, args:, **|
expect(args.each_cons(2)).to include(["--user-agent", "Mozilla/25.0.1"]) expect(args.each_cons(2)).to include(["--user-agent", "Mozilla/25.0.1"])
} }
@ -53,7 +53,7 @@ describe "download strategies", :cask do
let(:url_options) { { user_agent: :fake } } let(:url_options) { { user_agent: :fake } }
it "adds the appropriate curl args" do it "adds the appropriate curl args" do
expect(downloader).to receive(:safe_system) { |*args| expect(downloader).to receive(:system_command!) { |*, args:, **|
expect(args.each_cons(2).to_a).to include(["--user-agent", a_string_matching(/Mozilla.*Mac OS X 10.*AppleWebKit/)]) expect(args.each_cons(2).to_a).to include(["--user-agent", a_string_matching(/Mozilla.*Mac OS X 10.*AppleWebKit/)])
} }

View File

@ -148,7 +148,7 @@ describe Hbc::Pkg, :cask do
"/usr/sbin/pkgutil", "/usr/sbin/pkgutil",
args: ["--pkg-info-plist", pkg_id], args: ["--pkg-info-plist", pkg_id],
).and_return( ).and_return(
SystemCommand::Result.new(nil, pkg_info_plist, nil, 0), SystemCommand::Result.new(nil, pkg_info_plist, nil, instance_double(Process::Status, exitstatus: 0)),
) )
info = pkg.info info = pkg.info

View File

@ -52,7 +52,7 @@ class FakeSystemCommand
if response.respond_to?(:call) if response.respond_to?(:call)
response.call(command_string, options) response.call(command_string, options)
else else
SystemCommand::Result.new(command, response, "", 0) SystemCommand::Result.new(command, response, "", OpenStruct.new(exitstatus: 0))
end end
end end

View File

@ -1,8 +1,22 @@
require "system_command" require "system_command"
describe SystemCommand::Result do describe SystemCommand::Result do
describe "#to_ary" do
subject(:result) {
described_class.new([], "output", "error", instance_double(Process::Status, exitstatus: 0, success?: true))
}
it "can be destructed like `Open3.capture3`" do
out, err, status = result
expect(out).to eq "output"
expect(err).to eq "error"
expect(status).to be_a_success
end
end
describe "#plist" do describe "#plist" do
subject { described_class.new(command, stdout, "", 0).plist } subject { described_class.new(command, stdout, "", instance_double(Process::Status, exitstatus: 0)).plist }
let(:command) { ["true"] } let(:command) { ["true"] }
let(:garbage) { let(:garbage) {

View File

@ -1,53 +1,58 @@
describe SystemCommand do describe SystemCommand do
describe "#initialize" do describe "#initialize" do
let(:env_args) { ["bash", "-c", 'printf "%s" "${A?}" "${B?}" "${C?}"'] } let(:env_args) { ["bash", "-c", 'printf "%s" "${A?}" "${B?}" "${C?}"'] }
let(:env) { { "A" => "1", "B" => "2", "C" => "3" } }
let(:sudo) { false }
subject(:command) {
described_class.new(
"env",
args: env_args,
env: env,
must_succeed: true,
sudo: sudo,
)
}
context "when given some environment variables" do context "when given some environment variables" do
subject {
described_class.new(
"env",
args: env_args,
env: { "A" => "1", "B" => "2", "C" => "3" },
must_succeed: true,
)
}
its("run!.stdout") { is_expected.to eq("123") } its("run!.stdout") { is_expected.to eq("123") }
describe "the resulting command line" do describe "the resulting command line" do
it "includes the given variables explicitly" do it "includes the given variables explicitly" do
expect(Open3) expect(Open3)
.to receive(:popen3) .to receive(:popen3)
.with(["env", "env"], "A=1", "B=2", "C=3", "env", *env_args, {}) .with(an_instance_of(Hash), "env", "A=1", "B=2", "C=3", "env", *env_args, {})
.and_call_original .and_call_original
subject.run! command.run!
end end
end end
end end
context "when given an environment variable which is set to nil" do
let(:env) { { "A" => "1", "B" => "2", "C" => nil } }
it "unsets them" do
expect {
command.run!
}.to raise_error(/C: parameter null or not set/)
end
end
context "when given some environment variables and sudo: true" do context "when given some environment variables and sudo: true" do
subject { let(:sudo) { true }
described_class.new(
"env",
args: env_args,
env: { "A" => "1", "B" => "2", "C" => "3" },
must_succeed: true,
sudo: true,
)
}
describe "the resulting command line" do describe "the resulting command line" do
it "includes the given variables explicitly" do it "includes the given variables explicitly" do
expect(Open3) expect(Open3)
.to receive(:popen3) .to receive(:popen3)
.with(["/usr/bin/sudo", "/usr/bin/sudo"], "-E", "--", .with(an_instance_of(Hash), "/usr/bin/sudo", "-E", "--",
"env", "A=1", "B=2", "C=3", "env", *env_args, {}) "env", "A=1", "B=2", "C=3", "env", *env_args, {})
.and_wrap_original do |original_popen3, *_, &block| .and_wrap_original do |original_popen3, *_, &block|
original_popen3.call("true", &block) original_popen3.call("true", &block)
end end
subject.run! command.run!
end end
end end
end end
@ -215,5 +220,12 @@ describe SystemCommand do
described_class.run("non_existent_executable") described_class.run("non_existent_executable")
}.not_to raise_error }.not_to raise_error
end end
it 'does not format `stderr` when it starts with \r' do
expect {
system_command "bash",
args: ["-c", 'printf "\r%s" "################### 27.6%" 1>&2']
}.to output("\r################### 27.6%").to_stderr
end
end end
end end

View File

@ -4,12 +4,12 @@ describe "curl" do
describe "curl_args" do describe "curl_args" do
it "returns -q as the first argument when HOMEBREW_CURLRC is not set" do it "returns -q as the first argument when HOMEBREW_CURLRC is not set" do
# -q must be the first argument according to "man curl" # -q must be the first argument according to "man curl"
expect(curl_args("foo")[1]).to eq("-q") expect(curl_args("foo").first).to eq("-q")
end end
it "doesn't return -q as the first argument when HOMEBREW_CURLRC is set" do it "doesn't return -q as the first argument when HOMEBREW_CURLRC is set" do
ENV["HOMEBREW_CURLRC"] = "1" ENV["HOMEBREW_CURLRC"] = "1"
expect(curl_args("foo")[1]).to_not eq("-q") expect(curl_args("foo").first).to_not eq("-q")
end end
end end
end end

View File

@ -11,7 +11,7 @@ def curl_executable
end end
def curl_args(*extra_args, show_output: false, user_agent: :default) def curl_args(*extra_args, show_output: false, user_agent: :default)
args = [curl_executable.to_s] args = []
# do not load .curlrc unless requested (must be the first argument) # do not load .curlrc unless requested (must be the first argument)
args << "-q" unless ENV["HOMEBREW_CURLRC"] args << "-q" unless ENV["HOMEBREW_CURLRC"]
@ -40,18 +40,18 @@ end
def curl(*args) def curl(*args)
# SSL_CERT_FILE can be incorrectly set by users or portable-ruby and screw # SSL_CERT_FILE can be incorrectly set by users or portable-ruby and screw
# with SSL downloads so unset it here. # with SSL downloads so unset it here.
with_env SSL_CERT_FILE: nil do system_command! curl_executable,
safe_system(*curl_args(*args)) args: curl_args(*args),
end env: { "SSL_CERT_FILE" => nil }
end end
def curl_download(*args, to: nil, continue_at: "-", **options) def curl_download(*args, to: nil, continue_at: "-", **options)
had_incomplete_download ||= File.exist?(to) had_incomplete_download ||= File.exist?(to)
curl("--location", "--remote-time", "--continue-at", continue_at.to_s, "--output", to, *args, **options) curl("--location", "--remote-time", "--continue-at", continue_at.to_s, "--output", to, *args, **options)
rescue ErrorDuringExecution rescue ErrorDuringExecution => e
# `curl` error 33: HTTP server doesn't seem to support byte ranges. Cannot resume. # `curl` error 33: HTTP server doesn't seem to support byte ranges. Cannot resume.
# HTTP status 416: Requested range not satisfiable # HTTP status 416: Requested range not satisfiable
if ($CHILD_STATUS.exitstatus == 33 || had_incomplete_download) && continue_at == "-" if (e.status.exitstatus == 33 || had_incomplete_download) && continue_at == "-"
continue_at = 0 continue_at = 0
had_incomplete_download = false had_incomplete_download = false
retry retry
@ -61,7 +61,9 @@ rescue ErrorDuringExecution
end end
def curl_output(*args, **options) def curl_output(*args, **options)
Open3.capture3(*curl_args(*args, show_output: true, **options)) system_command(curl_executable,
args: curl_args(*args, show_output: true, **options),
print_stderr: false)
end end
def curl_check_http_content(url, user_agents: [:default], check_content: false, strict: false, require_http: false) def curl_check_http_content(url, user_agents: [:default], check_content: false, strict: false, require_http: false)

View File

@ -131,13 +131,12 @@ module GitHub
# This is a no-op if the user is opting out of using the GitHub API. # This is a no-op if the user is opting out of using the GitHub API.
return block_given? ? yield({}) : {} if ENV["HOMEBREW_NO_GITHUB_API"] return block_given? ? yield({}) : {} if ENV["HOMEBREW_NO_GITHUB_API"]
args = %W[--header application/vnd.github.v3+json --write-out \n%{http_code}] # rubocop:disable Lint/NestedPercentLiteral args = ["--header", "application/vnd.github.v3+json", "--write-out", "\n%{http_code}"]
args += curl_args
token, username = api_credentials token, username = api_credentials
case api_credentials_type case api_credentials_type
when :keychain when :keychain
args += %W[--user #{username}:#{token}] args += ["--user", "#{username}:#{token}"]
when :environment when :environment
args += ["--header", "Authorization: token #{token}"] args += ["--header", "Authorization: token #{token}"]
end end
@ -162,7 +161,7 @@ module GitHub
args += ["--dump-header", headers_tmpfile.path] args += ["--dump-header", headers_tmpfile.path]
output, errors, status = curl_output(url.to_s, "--location", *args) output, errors, status = curl_output("--location", url.to_s, *args)
output, _, http_code = output.rpartition("\n") output, _, http_code = output.rpartition("\n")
output, _, http_code = output.rpartition("\n") if http_code == "000" output, _, http_code = output.rpartition("\n") if http_code == "000"
headers = headers_tmpfile.read headers = headers_tmpfile.read