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, retry_download_count: 3) ⇒ Object

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)

  • retry_download_count: (defaults to: 3)

    A count to retry the downloading Xcode dmg/xip



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
# 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) ⇒ Object



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
118
119
120
121
122
123
124
125
# 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