Class: XcodeInstall::Curl

Inherits:
Object
  • Object
show all
Defined in:
lib/xcode/install.rb

Constant Summary collapse

COOKIES_PATH =
Pathname.new('/tmp/curl-cookies.txt')

Instance Method Summary collapse

Instance Method Details

#fetch(url: nil, directory: nil, cookies: nil, output: nil, progress: nil, progress_block: nil) ⇒ Object

rubocop:disable Metrics/AbcSize

Parameters:

  • url: (defaults to: nil)

    The URL to download

  • directory: (defaults to: nil)

    The directory to download this file into

  • cookies: (defaults to: nil)

    Any cookies we should use for the download (used for auth with Apple)

  • output: (defaults to: nil)

    A PathName for where we want to store the file

  • progress: (defaults to: nil)

    parse and show the progress?

  • progress_block: (defaults to: nil)

    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)



27
28
29
30
31
32
33
34
35
36
37
38
39
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
112
113
114
115
116
117
# File 'lib/xcode/install.rb', line 27

def fetch(url: nil,
          directory: nil,
          cookies: nil,
          output: nil,
          progress: nil,
          progress_block: nil)
  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
  3.times do
    # 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

    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