From 6e79c4639dab28a8c048a7a60e8d6dc70e015964 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Thu, 29 Jan 2026 21:44:54 -0600 Subject: [PATCH] WIP Ractor Task stuff --- lib/application_manager.rb | 53 ++++++--- lib/application_manager/status.rb | 13 +-- lib/application_manager/task.rb | 172 ++++++++++++++++++++++++++---- lib/background_worker.rb | 20 ++++ lib/common.rb | 10 +- 5 files changed, 218 insertions(+), 50 deletions(-) diff --git a/lib/application_manager.rb b/lib/application_manager.rb index 2b0968c..8c9d846 100644 --- a/lib/application_manager.rb +++ b/lib/application_manager.rb @@ -555,7 +555,7 @@ class W3DHub end def installing?(app_id, channel) - @tasks.find { |t| t.is_a?(Installer) && t.app_id == app_id && t.release_channel == channel } + @tasks.find { |t| t.is_a?(Installer) && t.context.app_id == app_id && t.context.channel_id == channel } end def updateable?(app_id, channel) @@ -610,34 +610,49 @@ class W3DHub def handle_task_event(event) # ONLY CALL on MAIN Ractor - raise "Something has gone horribly wrong!" unless Ractor.current == Ractor.main + raise "Something has gone horribly wrong!" unless Ractor.main? + pp event 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] - ) + Store.main_thread_queue << proc do + window.push_state( + W3DHub::States::MessageDialog, + type: event.data[:type], + title: event.data[:title], + message: event.data[:message] + ) + end + # FIXME: Send event to Games page to trigger refresh 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 + # FIXME: Send event to Games page to trigger refresh + when Task::EVENT_STATUS + task.status = event.data + States::Interface.instance&.update_interface_task_status(task) + + when Task::EVENT_STATUS_OPERATION + hash = event.data + operation = task.status.operations[operation[:id]] + + if operation + operation.label = hash[:label] + operation.value = hash[:value] + operation.progress = hash[:progress] + + States::Interface.instance&.update_interface_task_status(task) + end end end @@ -661,7 +676,13 @@ class W3DHub @tasks.delete_if { |t| t.state == :complete || t.state == :halted || t.state == :failed } task = @tasks.find { |t| t.state == :not_started } - task&.start + + return unless task + + # mark MAIN ractor's task as started before handing off to background ractor + # so that we don't start up multiple tasks at once. + task.start + BackgroundWorker.ractor_task(task) end def task?(type, app_id, channel) diff --git a/lib/application_manager/status.rb b/lib/application_manager/status.rb index 3bea864..2b746cb 100644 --- a/lib/application_manager/status.rb +++ b/lib/application_manager/status.rb @@ -1,10 +1,10 @@ class W3DHub class ApplicationManager class Status - attr_reader :application, :channel, :step, :operations, :data - attr_accessor :label, :value, :progress + attr_reader :application, :channel, :operations, :data + attr_accessor :label, :value, :progress, :step - def initialize(application:, channel:, label: "", value: "", progress: 0.0, step: :pending, operations: {}, &callback) + def initialize(application:, channel:, label: "", value: "", progress: 0.0, step: :pending, operations: {}) @application = application @channel = channel @@ -15,17 +15,10 @@ class W3DHub @step = step @operations = operations - @callback = callback @data = {} end - def step=(sym) - @step = sym - @callback&.call(self) - @step - end - class Operation attr_accessor :label, :value, :progress diff --git a/lib/application_manager/task.rb b/lib/application_manager/task.rb index 5c1a5b3..7c85fef 100644 --- a/lib/application_manager/task.rb +++ b/lib/application_manager/task.rb @@ -3,6 +3,9 @@ class W3DHub class Task LOG_TAG = "W3DHub::ApplicationManager::Task".freeze + class AbortTaskExecutionError < StandardError + end + # Task failed EVENT_FAILURE = -1 # Task started, show application taskbar @@ -10,17 +13,15 @@ class W3DHub # 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 + EVENT_STATUS = 2 + # Subtask progress + EVENT_STATUS_OPERATION = 3 Context = Data.define( :task_id, :app_type, - :app_id, - :channel_id, + :application, + :channel, :version, :target_path, :temp_path @@ -34,6 +35,7 @@ class W3DHub ) attr_reader :context, :state + attr_accessor :status def initialize(context:) @context = context @@ -43,6 +45,8 @@ class W3DHub # remember all case insensitive file paths @cache_file_paths = {} + @status = Status.new(application: context.application, channel: context.channel) + @failure = false @failure_reason = "" end @@ -51,14 +55,13 @@ class W3DHub end def fail!(reason: "") + @state = :failed @failure = true + @failure_reason = reason - Ractor.current.send( - MessageEvent.new( - context.task_id, + send_task_result - ) - ) + raise AbortTaskExecutionError, reason end def failed? @@ -72,12 +75,20 @@ class W3DHub end def send_task_result - Ractor.current.send( + internal_send_message_event( + failed? ? EVENT_FAILURE : EVENT_SUCCESS, + nil, + failed? ? { type: :failure, title: "Task Failed", message: @failure_reason } : nil + ) + end + + def internal_send_message_event(type, subtype = nil, data = nil) + Ractor.yield( MessageEvent.new( context.task_id, - @failure ? EVENT_FAILURE : EVENT_SUCCESS, - nil, - @failure_reason + type, + subtype, + data ) ) end @@ -87,9 +98,27 @@ class W3DHub end def start + @state = :running + + # only mark task as running then return unless we're NOT running on the + # main ractor. Task is deep copied when past to the ractor. + return if Ractor.main? + + internal_send_message_event(EVENT_START) + + sleep 1 + execute_task + sleep 1 + + @state = failed? ? :failed : :complete + send_task_result + + sleep 1 + rescue StandardError => e + fail!(reason: "Fatal Error\n#{e}") unless e.is_a?(AbortTaskExecutionError) end # returns true on success and false on failure @@ -102,14 +131,52 @@ class W3DHub # Quick checks before network and computational work starts def fail_fast! - # can read/write to destination + # is wine present? + if W3DHub.unix? + wine_present = W3DHub.command("which #{Store.settings[:wine_command]}") - fail!("FAIL FAST: Insufficient disk space available.") unless disk_space_available? + unless wine_present + fail!(reason: "FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed.\n\n"\ + "Will be unable to launch game.\n\n"\ + "Check wine options in launcher's settings.") + end + end + + # can read/write to destination + # TODO + + # have enough disk space + + fail!(reason: "FAIL FAST: Insufficient disk space available.") unless disk_space_available? end - def fetch_manifests - result = CyberarmEngine::Result.new + def fetch_manifests(version) + manifests = [] + result = Result.new + while (package_result = fetch_package(category, subcategory, version, "manifest.xml")) + break unless package_result.okay? + + path = package_file_path(category, subcategory, version, "manifest.xml") + unless File.exist?(path) && !File.directory?(path) + result.error = RuntimeError.new("File missing: #{path}") + return result + end + + manifest = LegacyManifest.new(path) + + manifests << manifest + + break unless manifest.patch? + + version = manifest.base_version + end + + # return in oldest to newest order + result.data = manifests.reverse + result + rescue StandardError => e # Async derives its errors from StandardError + result.error = e result end @@ -180,8 +247,63 @@ class W3DHub true end + # returns JSON hash on success, false or nil on failure + def fetch_package_details(packages) + endpoint = "/apis/launcher/1/get-package-details" + result = Result.new + + hash = { + packages: packages.map do |h| + { category: h[:category], subcategory: h[:subcategory], name: h[:name], version: h[:version] } + end + } + + body = URI.encode_www_form("data": JSON.dump(hash)) + + Sync do + Async::HTTP::Internet.post("#{UPSTREAM_ENDPOINT}#{endpoint}", CLIENT_FORM_ENCODED_HEADERS, body) do |response| + if response.success? + result.data = JSON.parse(response.read) + else + result.error = RuntimeError.new(response) # FIXME: have better error + end + rescue StandardError => e + result.error = e + end + end + + result + end + def fetch_package(version, name) - result = CyberarmEngine::Result.new + endpoint = "/apis/launcher/1/get-package" + result = Result.new + + path = package_file_path(category, subcategory, version, name) + headers = [ + ["user-agent", USER_AGENT], + ["content-type", "application/x-www-form-urlencoded"], + ["authorization", "Bearer #{FAKE_BEARER_TOKEN}"] + ].freeze + body = URI.encode_www_form("data": JSON.dump({ category: category, subcategory: subcategory, name: name, version: version })) + + Sync do + Async::HTTP::Internet.post("#{UPSTREAM_ENDPOINT}#{endpoint}", headers, body) do |response| + if response.success? + create_directories(path) + + File.open(path, "wb") do |file| + response.each do |chunk| + file.write(chunk) + end + end + + result.data = true + end + rescue StandardError => e + result.error = e + end + end result end @@ -216,6 +338,14 @@ class W3DHub result end + + def package_file_path(category, subcategory, version, name) + "#{PACKAGE_CACHE}/"\ + "#{category}/"\ + "#{subcategory.to_s.empty? ? "" : "#{subcategory}/"}"\ + "#{version.to_s.empty? ? "" : "#{version}/"}"\ + "#{name}" + end end end end diff --git a/lib/background_worker.rb b/lib/background_worker.rb index fee113c..b845a81 100644 --- a/lib/background_worker.rb +++ b/lib/background_worker.rb @@ -59,6 +59,26 @@ class W3DHub @@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true, data: data)) end + def self.ractor_task(task) + raise "Something has gone horribly wrong!!!" unless Ractor.main? + + ractor = Ractor.new do + t = Ractor.receive + + t.start + end + + ractor.send(task) + + Thread.new do + while (message_event = ractor.take) + break unless message_event.is_a?(W3DHub::ApplicationManager::Task::MessageEvent) + + Store.application_manager.handle_task_event(message_event) + end + end + end + def initialize @busy = false @jobs = [] diff --git a/lib/common.rb b/lib/common.rb index 07f551a..64c36b0 100644 --- a/lib/common.rb +++ b/lib/common.rb @@ -1,4 +1,8 @@ class W3DHub + PLATFORM_WINDOWS = RbConfig::CONFIG["host_os"] =~ /(mingw|mswin|windows)/i + PLATFORM_DARWIN = RbConfig::CONFIG["host_os"] =~ /(darwin|mac os)/i + PLATFORM_LINUX = RbConfig::CONFIG["host_os"] =~ /(linux|bsd|aix|solaris)/i + def self.format_size(bytes) case bytes when 0..1023 # Bytes @@ -17,15 +21,15 @@ class W3DHub end def self.windows? - RbConfig::CONFIG["host_os"] =~ /(mingw|mswin|windows)/i + PLATFORM_WINDOWS end def self.mac? - RbConfig::CONFIG["host_os"] =~ /(darwin|mac os)/i + PLATFORM_DARWIN end def self.linux? - RbConfig::CONFIG["host_os"] =~ /(linux|bsd|aix|solaris)/i + PLATFORM_LINUX end def self.unix?