From 4a8457e23319a9c94f95bb02c962ac5c1fadc471 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Wed, 28 Jan 2026 18:27:58 -0600 Subject: [PATCH] Initial work on reimplementing Task to be Ractor/sub-Process safe --- lib/application_manager.rb | 61 +- lib/application_manager/task.rb | 886 ++++----------------- lib/application_manager/tasks/installer.rb | 7 +- 3 files changed, 197 insertions(+), 757 deletions(-) diff --git a/lib/application_manager.rb b/lib/application_manager.rb index a769d0e..2b0968c 100644 --- a/lib/application_manager.rb +++ b/lib/application_manager.rb @@ -23,9 +23,7 @@ class W3DHub # unpack packages # install dependencies (e.g. visual C runtime) - installer = Installer.new(app_id, channel) - - @tasks.push(installer) + @tasks.push(Installer.new(context: task_context(app_id, channel, "version"))) end def update(app_id, channel) @@ -33,7 +31,7 @@ class W3DHub return false unless installed?(app_id, channel) - updater = Updater.new(app_id, channel) + updater = Updater.new(Installer.new(context: task_context(app_id, channel, "version"))) @tasks.push(updater) end @@ -98,6 +96,18 @@ class W3DHub Process.spawn(exe) end + def task_context(app_id, channel, version) + Task::Context.new( + SecureRandom.hex, + "games", + app_id, + channel, + version, + "", + "" + ) + end + def repair(app_id, channel) logger.info(LOG_TAG) { "Repair Installation Request: #{app_id}-#{channel}" } @@ -110,7 +120,7 @@ class W3DHub # unpack packages # install dependencies (e.g. visual C runtime) if appropriate - @tasks.push(Repairer.new(app_id, channel)) + @tasks.push(Repairer.new(context: task_context(app_id, channel, "version"))) end def uninstall(app_id, channel) @@ -125,7 +135,7 @@ class W3DHub title: "Uninstall #{game.name}?", message: "Are you sure you want to uninstall #{game.name} (#{channel})?", accept_callback: proc { - @tasks.push(Uninstaller.new(app_id, channel)) + @tasks.push(Uninstaller.new(context: task_context(app_id, channel, "version"))) } ) end @@ -598,6 +608,39 @@ class W3DHub app.channels.detect { |g| g.id.to_s == channel_id.to_s } end + def handle_task_event(event) + # ONLY CALL on MAIN Ractor + raise "Something has gone horribly wrong!" unless Ractor.current == Ractor.main + + task = @tasks.find { |t| t.context.task_id == event.task_id } + return unless task # FIXME: This is probably a fatal error + + case event.type + when Task::EVENT_FAILURE + window.push_state( + W3DHub::States::MessageDialog, + type: event.data[:type], + title: event.data[:title], + message: event.data[:message] + ) + + States::Interface.instance&.hide_application_taskbar + @tasks.delete(task) + when Task::EVENT_START + task.started! # mark ApplicationManager's version of Task as :running + States::Interface.instance&.show_application_taskbar + when Task::EVENT_SUCCESS + States::Interface.instance&.hide_application_taskbar + @tasks.delete(task) + when Task::EVENT_PROGRESS + :FIXME + when Task::EVENT_PACKAGE_LIST + :FIXME + when Task::EVENT_PACKAGE_STATUS + :FIXME + end + end + # No application tasks are being done def idle? !busy? @@ -624,9 +667,9 @@ class W3DHub def task?(type, app_id, channel) @tasks.find do |t| t.type == type && - t.app_id == app_id && - t.release_channel == channel && - [ :not_started, :running, :paused ].include?(t.state) + t.context.app_id == app_id && + t.context.channel_id == channel && + [ :not_started, :running, :paused ].include?(t.state) end end end diff --git a/lib/application_manager/task.rb b/lib/application_manager/task.rb index 36c066e..5c1a5b3 100644 --- a/lib/application_manager/task.rb +++ b/lib/application_manager/task.rb @@ -3,816 +3,218 @@ class W3DHub class Task LOG_TAG = "W3DHub::ApplicationManager::Task".freeze - class FailFast < RuntimeError - end + # Task failed + EVENT_FAILURE = -1 + # Task started, show application taskbar + EVENT_START = 0 + # Task completed successfully + EVENT_SUCCESS = 1 + # Task progress + EVENT_PROGRESS = 2 + # List of packages this task will be working over + EVENT_PACKAGE_LIST = 3 + # Update a package's status: verifying, downloading, unpacking, patching + EVENT_PACKAGE_STATUS = 4 - include CyberarmEngine::Common + Context = Data.define( + :task_id, + :app_type, + :app_id, + :channel_id, + :version, + :target_path, + :temp_path + ) - attr_reader :app_id, :release_channel, :application, :channel, :target_version, - :manifests, :packages, :files, :wine_prefix, :status + MessageEvent = Data.define( + :task_id, + :type, + :subtype, + :data # { message: "Complete", progress: 0.1 } + ) - def initialize(app_id, release_channel) - @app_id = app_id - @release_channel = release_channel + attr_reader :context, :state - @task_state = :not_started # :not_started, :running, :paused, :halted, :complete, :failed + def initialize(context:) + @context = context - @application = Store.applications.games.find { |g| g.id == app_id } - @channel = @application.channels.find { |c| c.id == release_channel } + @state = :not_started - @target_version = type == :repairer ? Store.settings[:games][:"#{app_id}_#{@channel.id}"][:installed_version] : @channel.current_version + # remember all case insensitive file paths + @cache_file_paths = {} - @packages_to_download = [] - @total_bytes_to_download = -1 - @bytes_downloaded = -1 - - @manifests = [] - @files = [] - @packages = [] - @deleted_files = [] # TODO: remove removed files - - @wine_prefix = nil - - @status = Status.new(application: @application, channel: channel) { update_interface_task_status } - - setup + @failure = false + @failure_reason = "" end def setup end + def fail!(reason: "") + @failure = true + + Ractor.current.send( + MessageEvent.new( + context.task_id, + + ) + ) + end + + def failed? + @failure + end + + def send_status_update(type) + end + + def send_package_list() + end + + def send_task_result + Ractor.current.send( + MessageEvent.new( + context.task_id, + @failure ? EVENT_FAILURE : EVENT_SUCCESS, + nil, + @failure_reason + ) + ) + end + def type raise NotImplementedError end - def state - @task_state - end - - # Start task, inside its own thread - # FIXME: Ruby 3 has parallelism now: Use a Ractor to do work on a seperate core to - # prevent the UI from locking up while doing computation heavy work, i.e building - # list of packages to download def start - @task_state = :running + execute_task - Thread.new do - # Sync do - begin - status = execute_task - rescue FailFast - # no-op - rescue StandardError, Errno::EACCES => e - status = false - @task_failure_reason = e.message[0..512] - - logger.error(LOG_TAG) { "Task #{type.inspect} failed for #{@application.name}" } - logger.error(LOG_TAG) { e } - end - - # Force free some bytes - GC.compact if GC.respond_to?(:compact) - GC.start - - @task_state = :failed unless status - @task_state = :complete unless @task_state == :failed - - hide_application_taskbar if @task_state == :failed - send_message_dialog(:failure, "#{type.to_s.capitalize} Task failed for #{@application.name}", @task_failure_reason) if @task_state == :failed && !@fail_silently - # end - end + send_task_result end + # returns true on success and false on failure def execute_task end - # Suspend operation, if possible - def pause - @task_state = :paused if pauseable? - end - - # Halt operation, if possible - def stop - @task_state = :halted if stoppable? - end - - def pauseable? - false - end - - def stoppable? - false - end - - def complete? - @task_state == :complete - end - - def failed? - @task_state == :failed - end - - def normalize_path(path, base_path) - path = path.to_s.gsub("\\", "/") - return "#{base_path}/#{path}" if W3DHub.windows? # Windows is easy, or annoying, depending how you look at it... - - constructed_path = base_path - lowercase_full_path = "#{base_path}/#{path}".downcase.strip.freeze - - accepted_parts = 0 - split_path = path.split("/") - split_path.each do |segment| - Dir.glob("#{constructed_path}/*").each do |part| - next unless "#{constructed_path}/#{segment}".downcase == part.downcase - - # Handle edge case where a file with the same name is in a higher directory - next if File.file?(part) && part.downcase.strip != lowercase_full_path - - constructed_path = part - accepted_parts += 1 - break - end - end - - # Find file if it exists else use provided path as cased - if constructed_path.downcase.strip == lowercase_full_path - constructed_path - elsif accepted_parts.positive? - "#{constructed_path}/#{split_path[accepted_parts..].join('/')}" - else - "#{base_path}/#{path}" # File doesn't exist, case doesn't matter. - end - end - - def failure_reason - @task_failure_reason || "" - end - - def fail!(reason = "") - @task_state = :failed - @task_failure_reason = reason.to_s - - hide_application_taskbar - - raise FailFast, @task_failure_reason - end - - def fail_silently! - @fail_silently = true - end + ########################### + ## High level task steps ## + ########################### # Quick checks before network and computational work starts - def fail_fast - # Have write permissions to target directory - path = Cache.install_path(@application, @channel) - path = Store.settings[:app_install_dir] unless File.exist?(path) + def fail_fast! + # can read/write to destination - begin - File.write("#{path}/___can_write.wlh", "") - File.delete("#{path}/___can_write.wlh") - - Dir.mkdir("#{path}/__can_write") - Dir.rmdir("#{path}/__can_write") - rescue Errno::EACCES - fail!("FAIL FAST: Cannot write to #{path}") - end - - # FIXME: Check that there is enough disk space - - # TODO: Is missing wine/proton really a failure condition? - # Wine present? - if W3DHub.unix? - wine_present = W3DHub.command("which #{Store.settings[:wine_command]}") - fail!("FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed.\n\n"\ - "Will be unable to launch game.\n\nCheck wine options in launcher's settings.") unless wine_present - end + fail!("FAIL FAST: Insufficient disk space available.") unless disk_space_available? end - def run_on_main_thread(block) - Store.main_thread_queue << block - end - - def send_message_dialog(type, title, message) - run_on_main_thread( - proc do - window.push_state(W3DHub::States::MessageDialog, type: type, title: title, message: message) - end - ) - end - - def update_interface_task_status - run_on_main_thread( - proc do - States::Interface.instance&.interface_task_update_pending = self - end - ) - end - - def show_application_taskbar - run_on_main_thread( - proc do - States::Interface.instance&.show_application_taskbar - end - ) - end - - def hide_application_taskbar - run_on_main_thread( - proc do - States::Interface.instance&.hide_application_taskbar - end - ) - end - - ############### - # Tasks/Steps # - ############### - def fetch_manifests - @status.operations.clear - @status.label = "Downloading #{@application.name}..." - @status.value = "Fetching manifests..." - @status.progress = 0.0 + result = CyberarmEngine::Result.new - @status.step = :fetching_manifests - - if fetch_manifest("games", app_id, "manifest.xml", @target_version) - manifest = load_manifest("games", app_id, "manifest.xml", @target_version) - @manifests << manifest - - until(manifest.full?) - if fetch_manifest("games", app_id, "manifest.xml", manifest.base_version) - manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version) - manifests << manifest - else - fail!("Failed to retrieve manifest: games:#{app_id}:manifest.xml-#{manifest.base_version}") - return [] - end - end - else - fail!("Failed to retrieve manifest: games:#{app_id}:manifest.xml-#{@target_version}") - return [] - end - - @manifests + result end def build_package_list - @status.operations.clear - @status.label = "Downloading #{@application.name}..." - @status.value = "Building package list..." - @status.progress = 0.0 + result = CyberarmEngine::Result.new - @status.step = :build_package_list - - # Process manifest game files in OLDEST to NEWEST order so we can simply remove preceeding files from the array that aren't needed - @manifests.reverse.each do |manifest| - logger.info(LOG_TAG) { "#{manifest.game}-#{manifest.type}: #{manifest.version} (#{manifest.base_version})" } - - manifest.files.each do |file| - if file.removed? # No package data - @files.delete_if { |f| f.name.casecmp?(file.name) } - @deleted_files.push(file) - next - end - - @files.delete_if { |f| f.name.casecmp?(file.name) } unless file.patch? - - # If file has been recreated in a newer patch, don't delete it; - # A full file package will exist for it so it will get completely replaced. - @deleted_files.delete_if { |f| f.name.casecmp?(file.name) } - - @files.push(file) - end - - # TODO: Dependencies - end - - @files.each do |file| - next if packages.detect do |pkg| - pkg.category == "games" && - pkg.subcategory == @app_id && - pkg.name.to_s.casecmp?(file.package.to_s) && - pkg.version == file.version - end - - package = Api::Package.new({ category: "games", subcategory: @app_id, name: file.package, version: file.version }) - - package.is_patch = file if file.patch? - - packages.push(package) - end - - @packages = packages - end - - def verify_files - @status.operations.clear - @status.label = "Downloading #{@application.name}..." - @status.value = "Verifying installed files..." - @status.progress = 0.0 - - @status.step = :verify_files - - path = Cache.install_path(@application, @channel) - accepted_files = {} - rejected_files = [] - - file_count = manifests.map { |m| m.files.count }.sum - processed_files = 0 - - folder_exists = File.directory?(path) - - # Process manifest game files in NEWEST to OLDEST order so that we don't erroneously flag - # valid files as invalid due to an OLDER version of the file being checked FIRST. - @files.reverse.each do |file| - break unless folder_exists - - file_path = normalize_path(file.name, path) - - processed_files += 1 - @status.progress = processed_files.to_f / file_count - - next if file.removed_since - next if accepted_files.key?(file_path) - - unless File.exist?(file_path) - rejected_files << { file: file, manifest_version: file.version } - logger.info(LOG_TAG) { "[#{file.version}] File missing: #{file_path}" } - next - end - - digest = Digest::SHA256.new - f = File.open(file_path) - - while (chunk = f.read(32_000_000)) - digest.update(chunk) - end - - f.close - - logger.info(LOG_TAG) { file.inspect } if file.checksum.nil? - - if digest.hexdigest.upcase == file.checksum.upcase - accepted_files[file_path] = file.version - logger.info(LOG_TAG) { "[#{file.version}] Verified file: #{file_path}" } - else - rejected_files << { file: file, manifest_version: file.version } - logger.info(LOG_TAG) { "[#{file.version}] File failed Verification: #{file_path}" } - end - end - - logger.info(LOG_TAG) { "#{rejected_files.count} missing or corrupt files" } - - selected_packages = [] - selected_packages_hash = {} - - rejected_files.each do |hash| - next if selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] - - package = @packages.find { |pkg| pkg.name.casecmp?(hash[:file].package) && pkg.version == hash[:manifest_version] } - - if package - selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] = true - selected_packages << package - else - raise "missing package: #{hash[:file].package}:#{hash[:manifest_version]} in fetched packages list!" - end - end - - # Removed packages that don't need to be fetched or processed - @packages.delete_if { |package| !selected_packages.find { |pkg| pkg == package } } if folder_exists - - @packages - end - - def fetch_packages - return if @packages.empty? - - hashes = @packages.map do |pkg| - { - category: pkg.category, - subcategory: pkg.subcategory, - name: "#{pkg.name}.zip", - version: pkg.version - } - end - - package_details = Api.package_details(hashes, @channel.source || :w3dhub) - - unless package_details - fail!("Failed to fetch package details") - return - end - - [package_details].flatten.each do |rich| - if rich.error? - fail!("Failed to retrieve package details! (#{rich.category}:#{rich.subcategory}:#{rich.name}:#{rich.version})\nError: #{rich.error.gsub("-", " ").capitalize}") - return - end - - package = @packages.find do |pkg| - pkg.category.to_s.casecmp?(rich.category.to_s) && - pkg.subcategory.to_s.casecmp?(rich.subcategory.to_s) && - "#{pkg.name}.zip".casecmp?(rich.name) && - pkg.version == rich.version - end - - package.instance_variable_set(:"@name", rich.name) - package.instance_variable_set(:"@size", rich.size) - package.instance_variable_set(:"@checksum", rich.checksum) - package.instance_variable_set(:"@checksum_chunk_size", rich.checksum_chunk_size) - package.instance_variable_set(:"@checksum_chunks", rich.checksum_chunks) - end - - @packages_to_download = [] - - @status.label = "Downloading #{@application.name}..." - @status.value = "Verifying local packages..." - @status.progress = 0.0 - - package_details.each do |pkg| - @status.operations[:"#{pkg.checksum}"] = Status::Operation.new( - label: pkg.name, - value: "Pending...", - progress: 0.0 - ) - end - - @status.step = :prefetch_verifying_packages - - package_details.each_with_index.each do |pkg, i| - operation = @status.operations[:"#{pkg.checksum}"] - - if verify_package(pkg) - operation.value = "Verified" - operation.progress = 1.0 - else - @packages_to_download << pkg - - operation.value = "#{W3DHub.format_size(pkg.custom_partially_valid_at_bytes)} / #{W3DHub.format_size(pkg.size)}" - operation.progress = pkg.custom_partially_valid_at_bytes.to_f / pkg.size - end - - @status.progress = i.to_f / package_details.count - - update_interface_task_status - end - - @status.operations.delete_if { |key, o| o.progress >= 1.0 } - - @status.step = :fetch_packages - - @total_bytes_to_download = @packages_to_download.sum { |pkg| pkg.size - pkg.custom_partially_valid_at_bytes } - @bytes_downloaded = 0 - - pool = Pool.new(workers: Store.settings[:parallel_downloads]) - - @packages_to_download.each do |pkg| - pool.add_job Pool::Job.new( proc { - package_bytes_downloaded = pkg.custom_partially_valid_at_bytes - - package_fetch(pkg) do |chunk, remaining_bytes, total_bytes| - @bytes_downloaded += chunk.to_s.length - package_bytes_downloaded += chunk.to_s.length - - @status.value = "#{W3DHub.format_size(@bytes_downloaded)} / #{W3DHub.format_size(@total_bytes_to_download)}" - @status.progress = @bytes_downloaded.to_f / @total_bytes_to_download - - operation = @status.operations[:"#{pkg.checksum}"] - operation.value = "#{W3DHub.format_size(package_bytes_downloaded)} / #{W3DHub.format_size(pkg.size)}" - operation.progress = package_bytes_downloaded.to_f / pkg.size # total_bytes - - update_interface_task_status - end - }) - end - - pool.manage_pool - end - - def verify_packages - end - - def unpack_packages - path = Cache.install_path(@application, @channel) - logger.info(LOG_TAG) { "Unpacking packages in '#{path}'..." } - Cache.create_directories(path, true) - - @status.operations.clear - @status.label = "Installing #{@application.name}..." - @status.value = "Unpacking..." - @status.progress = 0.0 - - @packages.each do |pkg| - # FIXME: can't add a new key into hash during iteration (RuntimeError) - @status.operations[:"#{pkg.checksum}"] = Status::Operation.new( - label: pkg.name, - value: "Pending...", - progress: 0.0 - ) - end - - @status.step = :unpacking - - i = -1 - @packages.each do |package| - i += 1 - - status = if package.custom_is_patch - @status.operations[:"#{package.checksum}"].value = "Patching..." - @status.operations[:"#{package.checksum}"].progress = Float::INFINITY - @status.progress = i.to_f / packages.count - update_interface_task_status - - apply_patch(package, path) - else - @status.operations[:"#{package.checksum}"].value = "Unpacking..." - @status.operations[:"#{package.checksum}"].progress = Float::INFINITY - @status.progress = i.to_f / packages.count - update_interface_task_status - - unpack_package(package, path) - end - - if status - @status.operations[:"#{package.checksum}"].value = package.custom_is_patch ? "Patched" : "Unpacked" - @status.operations[:"#{package.checksum}"].progress = 1.0 - - update_interface_task_status - else - logger.info(LOG_TAG) { "COMMAND FAILED!" } - fail!("Failed to unpack #{package.name}") - - break - end - end + result end def remove_deleted_files - return unless @deleted_files.size.positive? + result = CyberarmEngine::Result.new - logger.info(LOG_TAG) { "Removing dead files..." } + result + end - @deleted_files.each do |file| - logger.info(LOG_TAG) { " #{file.name}" } + def verify_files + result = CyberarmEngine::Result.new - path = Cache.install_path(@application, @channel) - file_path = normalize_path(file.name, path) + result + end - File.delete(file_path) if File.exist?(file_path) + def fetch_packages + result = CyberarmEngine::Result.new - logger.info(LOG_TAG) { " removed." } - end + result + end + + def verify_packages + result = CyberarmEngine::Result.new + + result + end + + def unpack_packages + result = CyberarmEngine::Result.new + + result end def create_wine_prefix - if W3DHub.unix? && @wine_prefix - # TODO: create a wine prefix if configured - @status.operations.clear - @status.label = "Installing #{@application.name}..." - @status.value = "Creating wine prefix..." - @status.progress = 0.0 + result = CyberarmEngine::Result.new - @status.step = :create_wine_prefix - end + result end def install_dependencies - # TODO: install dependencies - @status.operations.clear - @status.label = "Installing #{@application.name}..." - @status.value = "Installing dependencies..." - @status.progress = 0.0 + result = CyberarmEngine::Result.new - @status.step = :install_dependencies + result end def write_paths_ini - path = Cache.install_path(@application, @channel) + result = CyberarmEngine::Result.new - File.open(normalize_path("data/paths.ini", path), "w") do |file| - file.puts("[paths]") - file.puts("RegBase=W3D Hub") - file.puts("RegClient=#{@application.category}\\#{@application.id}-#{@channel.id}") - file.puts("RegFDS=#{@application.category}\\#{@application.id}-#{@channel.id}-server") - file.puts("FileBase=W3D Hub"); - file.puts("FileClient=#{@application.category}\\#{@application.id}-#{@channel.id}") - file.puts("FileFDS=#{@application.category}\\#{@application.id}-#{@channel.id}-server") - - file.puts("UseRenFolder=#{@application.uses_ren_folder?}") - end + result end def mark_application_installed - Store.application_manager.installed!(self) - - @status.operations.clear - @status.label = "Installed #{@application.name}" - @status.value = "" - @status.progress = 1.0 - - @status.step = :mark_application_installed - - logger.info(LOG_TAG) { "#{@app_id} has been installed." } end - ############# - # Functions # - ############# - - def fetch_manifest(category, subcategory, name, version, &block) - # Check for and integrity of local manifest - - package = nil - array = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }], @channel.source || :w3dhub) - if array.is_a?(Array) - package = array.first - else - fail!("Failed to fetch manifest package details! (#{category}:#{subcategory}:#{name}:#{version})") - return false - end - - if package.error? - fail!("Failed to retrieve manifest package details! (#{category}:#{subcategory}:#{name}:#{version})\nError: #{package.error.gsub("-", " ").capitalize}") - return false - end - - if File.exist?(Cache.package_path(category, subcategory, name, version)) - verified = verify_package(package) - - # download manifest if not valid - verified ? true : package_fetch(package) - else - # download manifest if not cached - package_fetch(package) - end - end - - def package_fetch(package, &block) - logger.info(LOG_TAG) { "Downloading: #{package.category}:#{package.subcategory}:#{package.name}-#{package.version}" } - - status_okay = Api.package(package) do |chunk, remaining_bytes, total_bytes| - block&.call(chunk, remaining_bytes, total_bytes) - end - - return status_okay if status_okay - - fail!("Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})") - false - end - - def verify_package(package, &block) - logger.info(LOG_TAG) { "Verifying: #{package.category}:#{package.subcategory}:#{package.name}-#{package.version}" } - - digest = Digest::SHA256.new - path = Cache.package_path(package.category, package.subcategory, package.name, package.version) - - return false unless File.exist?(path) - - operation = @status.operations[:"#{package.checksum}"] - operation&.value = "Verifying..." - - file_size = File.size(path) - logger.info(LOG_TAG) { " File size: #{file_size}" } - chunk_size = package.checksum_chunk_size - chunks = package.checksum_chunks.size - - File.open(path) do |f| - i = -1 - package.checksum_chunks.each do |chunk_start, checksum| - i += 1 - operation&.progress = i.to_f / chunks - update_interface_task_status - - chunk_start = Integer(chunk_start.to_s) - - read_length = chunk_size - read_length = file_size - chunk_start if chunk_start + chunk_size > file_size - - break if (file_size - chunk_start).negative? - - f.seek(chunk_start) - - chunk = f.read(read_length) - digest.update(chunk) - - if Digest::SHA256.new.hexdigest(chunk).upcase == checksum.upcase - valid_at = chunk_start + read_length - # logger.debug(LOG_TAG) { " Passed chunk: #{chunk_start}" } # Only enable when deep diving to find a bug (VERBOSE) - # package.partially_valid_at_bytes = valid_at - package.partially_valid_at_bytes = chunk_start - else - logger.info(LOG_TAG) { " FAILED chunk: #{chunk_start}" } - break - end - end - end - - digest.hexdigest.upcase == package.checksum.upcase - end - - def load_manifest(category, subcategory, name, version) - Manifest.new(category, subcategory, name, version) - end - - def unpack_package(package, path) - logger.info(LOG_TAG) { " #{package.name}:#{package.version}" } - package_path = Cache.package_path(package.category, package.subcategory, package.name, package.version) - - logger.info(LOG_TAG) { " Unpacking package \"#{package_path}\" in \"#{path}\"" } - - return unzip(package_path, path) - end - - def apply_patch(package, path) - logger.info(LOG_TAG) { " #{package.name}:#{package.version}" } - package_path = Cache.package_path(package.category, package.subcategory, package.name, package.version) - temp_path = "#{Store.settings[:package_cache_dir]}/temp" - manifest_file = package.custom_is_patch - - Cache.create_directories(temp_path, true) - - logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" } - unzip(package_path, temp_path) - - file_path = normalize_path(manifest_file.name, path) - temp_file_path = normalize_path(manifest_file.name, temp_path) - - logger.info(LOG_TAG) { " Loading #{temp_file_path}.patch..." } - patch_mix = W3DHub::WWMix.new(path: "#{temp_file_path}.patch") - unless patch_mix.load - raise patch_mix.error_reason - end - patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") } - patch_entry.read - - patch_info = JSON.parse(patch_entry.blob, symbolize_names: true) - - logger.info(LOG_TAG) { " Loading #{file_path}..." } - target_mix = W3DHub::WWMix.new(path: "#{file_path}") - unless target_mix.load - raise target_mix.error_reason - end - - logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive? - patch_info[:removedFiles].each do |file| - logger.debug(LOG_TAG) { " #{file}" } - target_mix.entries.delete_if { |e| e.name.casecmp?(file) } - end - - logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive? - patch_info[:updatedFiles].each do |file| - logger.debug(LOG_TAG) { " #{file}" } - - patch = patch_mix.entries.find { |e| e.name.casecmp?(file) } - target = target_mix.entries.find { |e| e.name.casecmp?(file) } - - if target - target_mix.entries[target_mix.entries.index(target)] = patch - else - target_mix.entries << patch - end - end - - logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive? - temp_mix_path = "#{temp_path}/#{File.basename(file_path)}" - temp_mix = W3DHub::WWMix.new(path: temp_mix_path) - target_mix.entries.each { |e| temp_mix.add_entry(entry: e) } - unless temp_mix.save - raise temp_mix.error_reason - end - - # Overwrite target mix with temp mix - FileUtils.mv(temp_mix_path, file_path) - - FileUtils.remove_dir(temp_path) + ########################## + ## Supporting functions ## + ########################## + # pestimistically estimate required disk space to: + # download, unpack/patch, and install. + def disk_space_available? true end - def unzip(package_path, path) - stream = Zip::InputStream.new(File.open(package_path)) + def fetch_package(version, name) + result = CyberarmEngine::Result.new - while (entry = stream.get_next_entry) - # Normalize the path to handle case-insensitivity consistently - file_path = normalize_path(entry.name, path) + result + end - dir_path = File.dirname(file_path) - unless dir_path.end_with?("/.") || Dir.exist?(dir_path) - FileUtils.mkdir_p(dir_path) - end + def verify_package(version, name) + result = CyberarmEngine::Result.new - File.open(file_path, "wb") do |f| - i = entry.get_input_stream + result + end - while (chunk = i.read(32_000_000)) # Read up to ~32 MB per chunk - f.write chunk - end - end - end + def unpack_package(version, name) + result = CyberarmEngine::Result.new - return true + result + end + + # Apply all patches for a particular MIX file at once + def apply_patches(package) + result = CyberarmEngine::Result.new + + result + end + + def apply_patch(target_mix, patch_mix) + result = CyberarmEngine::Result.new + + result + end + + def unzip(package_path) + result = CyberarmEngine::Result.new + + result end end end diff --git a/lib/application_manager/tasks/installer.rb b/lib/application_manager/tasks/installer.rb index dcee2e1..24050c6 100644 --- a/lib/application_manager/tasks/installer.rb +++ b/lib/application_manager/tasks/installer.rb @@ -8,9 +8,7 @@ class W3DHub end def execute_task - show_application_taskbar - - fail_fast + fail_fast! return false if failed? fetch_manifests @@ -46,9 +44,6 @@ class W3DHub mark_application_installed return false if failed? - sleep 1 - hide_application_taskbar - true end end