| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  | # typed: strict | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "downloadable" | 
					
						
							| 
									
										
										
										
											2024-07-16 10:18:26 -04:00
										 |  |  | require "concurrent/promises" | 
					
						
							| 
									
										
										
										
											2024-07-15 10:25:25 -04:00
										 |  |  | require "concurrent/executors" | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  | require "retryable_download" | 
					
						
							| 
									
										
										
										
											2025-08-08 08:33:26 +01:00
										 |  |  | require "resource" | 
					
						
							| 
									
										
										
										
											2025-08-20 19:20:19 +01:00
										 |  |  | require "utils/output" | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | module Homebrew | 
					
						
							|  |  |  |   class DownloadQueue | 
					
						
							| 
									
										
										
										
											2025-08-20 19:20:19 +01:00
										 |  |  |     include Utils::Output::Mixin | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 13:39:35 -04:00
										 |  |  |     sig { params(retries: Integer, force: T::Boolean, pour: T::Boolean).returns(T.nilable(DownloadQueue)) } | 
					
						
							|  |  |  |     def self.new_if_concurrency_enabled(retries: 1, force: false, pour: false) | 
					
						
							|  |  |  |       return if Homebrew::EnvConfig.download_concurrency <= 1
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       new(retries:, force:, pour:) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-17 17:55:50 +01:00
										 |  |  |     sig { params(retries: Integer, force: T::Boolean, pour: T::Boolean).void } | 
					
						
							| 
									
										
										
										
											2025-07-21 08:36:36 +01:00
										 |  |  |     def initialize(retries: 1, force: false, pour: false) | 
					
						
							| 
									
										
										
										
											2025-07-17 17:49:53 +01:00
										 |  |  |       @concurrency = T.let(EnvConfig.download_concurrency, Integer) | 
					
						
							|  |  |  |       @quiet = T.let(@concurrency > 1, T::Boolean) | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |       @tries = T.let(retries + 1, Integer) | 
					
						
							|  |  |  |       @force = force | 
					
						
							| 
									
										
										
										
											2025-07-17 17:55:50 +01:00
										 |  |  |       @pour = pour | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |       @pool = T.let(Concurrent::FixedThreadPool.new(concurrency), Concurrent::FixedThreadPool) | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-17 17:49:53 +01:00
										 |  |  |     sig { params(downloadable: Downloadable).void } | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |     def enqueue(downloadable) | 
					
						
							|  |  |  |       downloads[downloadable] ||= Concurrent::Promises.future_on( | 
					
						
							| 
									
										
										
										
											2025-07-22 17:48:32 +01:00
										 |  |  |         pool, RetryableDownload.new(downloadable, tries:, pour:), force, quiet | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |       ) do |download, force, quiet| | 
					
						
							|  |  |  |         download.clear_cache if force | 
					
						
							|  |  |  |         download.fetch(quiet:) | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-07 14:45:30 +02:00
										 |  |  |     sig { void } | 
					
						
							| 
									
										
										
										
											2025-07-17 17:49:53 +01:00
										 |  |  |     def fetch | 
					
						
							| 
									
										
										
										
											2025-07-20 17:12:43 -04:00
										 |  |  |       return if downloads.empty? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-04 15:51:02 +01:00
										 |  |  |       if concurrency == 1
 | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |         downloads.each do |downloadable, promise| | 
					
						
							|  |  |  |           promise.wait! | 
					
						
							|  |  |  |         rescue ChecksumMismatchError => e | 
					
						
							| 
									
										
										
										
											2025-08-04 15:51:02 +01:00
										 |  |  |           opoo "#{downloadable.download_queue_type} reports different checksum: #{e.expected}" | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |           Homebrew.failed = true if downloadable.is_a?(Resource::Patch) | 
					
						
							| 
									
										
										
										
											2025-07-29 12:42:13 +01:00
										 |  |  |         rescue => e | 
					
						
							|  |  |  |           raise e unless bottle_manifest_error?(downloadable, e) | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |         end | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         spinner = Spinner.new | 
					
						
							|  |  |  |         remaining_downloads = downloads.dup.to_a | 
					
						
							|  |  |  |         previous_pending_line_count = 0
 | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |         tty = $stdout.tty? | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         begin | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |           stdout_print_and_flush_if_tty Tty.hide_cursor | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |           output_message = lambda do |downloadable, future, last| | 
					
						
							|  |  |  |             status = case future.state | 
					
						
							|  |  |  |             when :fulfilled | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |               if tty | 
					
						
							|  |  |  |                 "#{Tty.green}✔︎#{Tty.reset}" | 
					
						
							|  |  |  |               else | 
					
						
							|  |  |  |                 "✔︎" | 
					
						
							|  |  |  |               end | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |             when :rejected | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |               if tty | 
					
						
							|  |  |  |                 "#{Tty.red}✘#{Tty.reset}" | 
					
						
							|  |  |  |               else | 
					
						
							|  |  |  |                 "✘" | 
					
						
							|  |  |  |               end | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |             when :pending, :processing | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |               "#{Tty.blue}#{spinner}#{Tty.reset}" if tty | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |             else | 
					
						
							|  |  |  |               raise future.state.to_s | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 12:42:13 +01:00
										 |  |  |             exception = future.reason if future.rejected? | 
					
						
							|  |  |  |             next 1 if bottle_manifest_error?(downloadable, exception) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-04 15:51:02 +01:00
										 |  |  |             message = "#{downloadable.download_queue_type} #{downloadable.download_queue_name}" | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |             if tty | 
					
						
							|  |  |  |               stdout_print_and_flush "#{status} #{message}#{"\n" unless last}" | 
					
						
							|  |  |  |             elsif status | 
					
						
							|  |  |  |               puts "#{status} #{message}" | 
					
						
							|  |  |  |             end | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             if future.rejected? | 
					
						
							| 
									
										
										
										
											2025-07-29 12:42:13 +01:00
										 |  |  |               if exception.is_a?(ChecksumMismatchError) | 
					
						
							| 
									
										
										
										
											2025-08-06 12:07:10 -04:00
										 |  |  |                 actual = Digest::SHA256.file(downloadable.cached_download).hexdigest | 
					
						
							| 
									
										
										
										
											2025-08-04 15:51:02 +01:00
										 |  |  |                 opoo "#{downloadable.download_queue_type} reports different checksum: #{exception.expected}" | 
					
						
							| 
									
										
										
										
											2025-08-06 12:07:10 -04:00
										 |  |  |                 puts (" " * downloadable.download_queue_type.size) + " SHA-256 checksum of downloaded file: #{actual}" | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |                 Homebrew.failed = true if downloadable.is_a?(Resource::Patch) | 
					
						
							|  |  |  |                 next 2
 | 
					
						
							|  |  |  |               else | 
					
						
							|  |  |  |                 message = future.reason.to_s | 
					
						
							| 
									
										
										
										
											2025-07-29 12:42:13 +01:00
										 |  |  |                 ofail message | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |                 next message.count("\n") | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             1
 | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           until remaining_downloads.empty? | 
					
						
							|  |  |  |             begin | 
					
						
							|  |  |  |               finished_states = [:fulfilled, :rejected] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               finished_downloads, remaining_downloads = remaining_downloads.partition do |_, future| | 
					
						
							|  |  |  |                 finished_states.include?(future.state) | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               finished_downloads.each do |downloadable, future| | 
					
						
							|  |  |  |                 previous_pending_line_count -= 1
 | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |                 stdout_print_and_flush_if_tty Tty.clear_to_end | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |                 output_message.call(downloadable, future, false) | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               previous_pending_line_count = 0
 | 
					
						
							|  |  |  |               max_lines = [concurrency, Tty.height].min | 
					
						
							|  |  |  |               remaining_downloads.each_with_index do |(downloadable, future), i| | 
					
						
							|  |  |  |                 break if previous_pending_line_count >= max_lines | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |                 stdout_print_and_flush_if_tty Tty.clear_to_end | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |                 last = i == max_lines - 1 || i == remaining_downloads.count - 1
 | 
					
						
							|  |  |  |                 previous_pending_line_count += output_message.call(downloadable, future, last) | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               if previous_pending_line_count.positive? | 
					
						
							|  |  |  |                 if (previous_pending_line_count - 1).zero? | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |                   stdout_print_and_flush_if_tty Tty.move_cursor_beginning | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |                 else | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |                   stdout_print_and_flush_if_tty Tty.move_cursor_up_beginning(previous_pending_line_count - 1) | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |                 end | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               sleep 0.05
 | 
					
						
							|  |  |  |             rescue Interrupt | 
					
						
							|  |  |  |               remaining_downloads.each do |_, future| | 
					
						
							|  |  |  |                 # FIXME: Implement cancellation of running downloads. | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               cancel | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               if previous_pending_line_count.positive? | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |                 stdout_print_and_flush_if_tty Tty.move_cursor_down(previous_pending_line_count - 1) | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |               end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               raise | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         ensure | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |           stdout_print_and_flush_if_tty Tty.show_cursor | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2025-07-17 17:49:53 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |       downloads.clear | 
					
						
							| 
									
										
										
										
											2024-09-07 14:45:30 +02:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-25 16:00:45 +00:00
										 |  |  |     sig { params(message: String).void } | 
					
						
							|  |  |  |     def stdout_print_and_flush_if_tty(message) | 
					
						
							|  |  |  |       stdout_print_and_flush(message) if $stdout.tty? | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { params(message: String).void } | 
					
						
							|  |  |  |     def stdout_print_and_flush(message) | 
					
						
							|  |  |  |       $stdout.print(message) | 
					
						
							|  |  |  |       $stdout.flush | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  |     sig { void } | 
					
						
							|  |  |  |     def shutdown | 
					
						
							|  |  |  |       pool.shutdown | 
					
						
							| 
									
										
										
										
											2024-09-07 14:45:30 +02:00
										 |  |  |       pool.wait_for_termination | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 12:42:13 +01:00
										 |  |  |     sig { params(downloadable: Downloadable, exception: T.nilable(Exception)).returns(T::Boolean) } | 
					
						
							|  |  |  |     def bottle_manifest_error?(downloadable, exception) | 
					
						
							|  |  |  |       return false if exception.nil? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       downloadable.is_a?(Resource::BottleManifest) || exception.is_a?(Resource::BottleManifest::Error) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |     sig { void } | 
					
						
							|  |  |  |     def cancel | 
					
						
							|  |  |  |       # FIXME: Implement graceful cancellation of running downloads based on | 
					
						
							|  |  |  |       #        https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Cancellation.html | 
					
						
							|  |  |  |       #        instead of killing the whole thread pool. | 
					
						
							|  |  |  |       pool.kill | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(Concurrent::FixedThreadPool) } | 
					
						
							|  |  |  |     attr_reader :pool | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(Integer) } | 
					
						
							|  |  |  |     attr_reader :concurrency | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(Integer) } | 
					
						
							|  |  |  |     attr_reader :tries | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     attr_reader :force | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     attr_reader :quiet | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-17 17:55:50 +01:00
										 |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     attr_reader :pour | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-17 17:49:53 +01:00
										 |  |  |     sig { returns(T::Hash[Downloadable, Concurrent::Promises::Future]) } | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |     def downloads | 
					
						
							| 
									
										
										
										
											2025-07-17 17:49:53 +01:00
										 |  |  |       @downloads ||= T.let({}, T.nilable(T::Hash[Downloadable, Concurrent::Promises::Future])) | 
					
						
							| 
									
										
										
										
											2025-07-11 15:54:49 +01:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     class Spinner | 
					
						
							|  |  |  |       FRAMES = [ | 
					
						
							|  |  |  |         "⠋", | 
					
						
							|  |  |  |         "⠙", | 
					
						
							|  |  |  |         "⠚", | 
					
						
							|  |  |  |         "⠞", | 
					
						
							|  |  |  |         "⠖", | 
					
						
							|  |  |  |         "⠦", | 
					
						
							|  |  |  |         "⠴", | 
					
						
							|  |  |  |         "⠲", | 
					
						
							|  |  |  |         "⠳", | 
					
						
							|  |  |  |         "⠓", | 
					
						
							|  |  |  |       ].freeze | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { void } | 
					
						
							|  |  |  |       def initialize | 
					
						
							|  |  |  |         @start = T.let(Time.now, Time) | 
					
						
							|  |  |  |         @i = T.let(0, Integer) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sig { returns(String) } | 
					
						
							|  |  |  |       def to_s | 
					
						
							|  |  |  |         now = Time.now | 
					
						
							|  |  |  |         if @start + 0.1 < now | 
					
						
							|  |  |  |           @start = now | 
					
						
							|  |  |  |           @i = (@i + 1) % FRAMES.count | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         FRAMES.fetch(@i) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2024-07-14 11:42:22 -04:00
										 |  |  |   end | 
					
						
							|  |  |  | end |