class XcodeInstall::Curl

Constants

COOKIES_PATH

Public Instance Methods

fetch(url: nil, directory: nil, cookies: nil, output: nil, progress: nil, progress_block: nil, retry_download_count: 3) click to toggle source

@param url: The URL to download @param directory: The directory to download this file into @param cookies: Any cookies we should use for the download (used for auth with Apple) @param output: A PathName for where we want to store the file @param progress: parse and show the progress? @param progress_block: A block that’s called whenever we have an updated progress %

the parameter is a single number that's literally percent (e.g. 1, 50, 80 or 100)

@param retry_download_count: A count to retry the downloading Xcode dmg/xip

# File lib/xcode/install.rb, line 29
def fetch(url: nil,
          directory: nil,
          cookies: nil,
          output: nil,
          progress: nil,
          progress_block: nil,
          retry_download_count: 3)
  options = cookies.nil? ? [] : ['--cookie', cookies, '--cookie-jar', COOKIES_PATH]

  uri = URI.parse(url)
  output ||= File.basename(uri.path)
  output = (Pathname.new(directory) + Pathname.new(output)) if directory

  # Piping over all of stderr over to a temporary file
  # the file content looks like this:
  #  0 4766M    0 6835k    0     0   573k      0  2:21:58  0:00:11  2:21:47  902k
  # This way we can parse the current %
  # The header is
  #  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
  #
  # Discussion for this on GH: https://github.com/KrauseFx/xcode-install/issues/276
  # It was not easily possible to reimplement the same system using built-in methods
  # especially when it comes to resuming downloads
  # Piping over stderror to Ruby directly didn't work, due to the lack of flushing
  # from curl. The only reasonable way to trigger this, is to pipe things directly into a
  # local file, and parse that, and just poll that. We could get real time updates using
  # the `tail` command or similar, however the download task is not time sensitive enough
  # to make this worth the extra complexity, that's why we just poll and
  # wait for the process to be finished
  progress_log_file = File.join(CACHE_DIR, "progress.#{Time.now.to_i}.progress")
  FileUtils.rm_f(progress_log_file)

  retry_options = ['--retry', '3']
  command = [
    'curl',
    '--disable',
    *options,
    *retry_options,
    '--location',
    '--continue-at',
    '-',
    '--output',
    output,
    url
  ].map(&:to_s)

  command_string = command.collect(&:shellescape).join(' ')
  command_string += " 2> #{progress_log_file}" # to not run shellescape on the `2>`

  # Run the curl command in a loop, retry when curl exit status is 18
  # "Partial file. Only a part of the file was transferred."
  # https://curl.haxx.se/mail/archive-2008-07/0098.html
  # https://github.com/KrauseFx/xcode-install/issues/210
  retry_download_count.times do
    wait_thr = poll_file(command_string: command_string, progress_log_file: progress_log_file, progress: progress, progress_block: progress_block)
    return wait_thr.value.success? if wait_thr.value.success?
  end
  false
ensure
  FileUtils.rm_f(COOKIES_PATH)
  FileUtils.rm_f(progress_log_file)
end
poll_file(command_string:, progress_log_file:, progress: nil, progress_block: nil) click to toggle source
# File lib/xcode/install.rb, line 92
def poll_file(command_string:, progress_log_file:, progress: nil, progress_block: nil)
  # Non-blocking call of Open3
  # We're not using the block based syntax, as the bacon testing
  # library doesn't seem to support writing tests for it
  stdin, stdout, stderr, wait_thr = Open3.popen3(command_string)

  # Poll the file and see if we're done yet
  while wait_thr.alive?
    sleep(0.5) # it's not critical for this to be real-time
    next unless File.exist?(progress_log_file) # it might take longer for it to be created

    progress_content = File.read(progress_log_file).split("\r").last || ''

    # Print out the progress for the CLI
    if progress
      print "\r#{progress_content}%"
      $stdout.flush
    end

    # Call back the block for other processes that might be interested
    matched = progress_content.match(/^\s*(\d+)/)
    next unless matched && matched.length == 2
    percent = matched[1].to_i
    progress_block.call(percent) if progress_block
  end

  # as we're not making use of the block-based syntax
  # we need to manually close those
  stdin.close
  stdout.close
  stderr.close

  wait_thr
end