WIP Ractor Task stuff

This commit is contained in:
2026-01-29 21:44:54 -06:00
parent 4a8457e233
commit 6e79c4639d
5 changed files with 218 additions and 50 deletions

View File

@@ -555,7 +555,7 @@ class W3DHub
end end
def installing?(app_id, channel) 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 end
def updateable?(app_id, channel) def updateable?(app_id, channel)
@@ -610,34 +610,49 @@ class W3DHub
def handle_task_event(event) def handle_task_event(event)
# ONLY CALL on MAIN Ractor # 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 } task = @tasks.find { |t| t.context.task_id == event.task_id }
return unless task # FIXME: This is probably a fatal error return unless task # FIXME: This is probably a fatal error
case event.type case event.type
when Task::EVENT_FAILURE when Task::EVENT_FAILURE
window.push_state( Store.main_thread_queue << proc do
W3DHub::States::MessageDialog, window.push_state(
type: event.data[:type], W3DHub::States::MessageDialog,
title: event.data[:title], type: event.data[:type],
message: event.data[:message] title: event.data[:title],
) message: event.data[:message]
)
end
# FIXME: Send event to Games page to trigger refresh
States::Interface.instance&.hide_application_taskbar States::Interface.instance&.hide_application_taskbar
@tasks.delete(task) @tasks.delete(task)
when Task::EVENT_START when Task::EVENT_START
task.started! # mark ApplicationManager's version of Task as :running
States::Interface.instance&.show_application_taskbar States::Interface.instance&.show_application_taskbar
when Task::EVENT_SUCCESS when Task::EVENT_SUCCESS
States::Interface.instance&.hide_application_taskbar States::Interface.instance&.hide_application_taskbar
@tasks.delete(task) @tasks.delete(task)
when Task::EVENT_PROGRESS # FIXME: Send event to Games page to trigger refresh
:FIXME when Task::EVENT_STATUS
when Task::EVENT_PACKAGE_LIST task.status = event.data
:FIXME States::Interface.instance&.update_interface_task_status(task)
when Task::EVENT_PACKAGE_STATUS
:FIXME 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
end end
@@ -661,7 +676,13 @@ class W3DHub
@tasks.delete_if { |t| t.state == :complete || t.state == :halted || t.state == :failed } @tasks.delete_if { |t| t.state == :complete || t.state == :halted || t.state == :failed }
task = @tasks.find { |t| t.state == :not_started } 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 end
def task?(type, app_id, channel) def task?(type, app_id, channel)

View File

@@ -1,10 +1,10 @@
class W3DHub class W3DHub
class ApplicationManager class ApplicationManager
class Status class Status
attr_reader :application, :channel, :step, :operations, :data attr_reader :application, :channel, :operations, :data
attr_accessor :label, :value, :progress 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 @application = application
@channel = channel @channel = channel
@@ -15,17 +15,10 @@ class W3DHub
@step = step @step = step
@operations = operations @operations = operations
@callback = callback
@data = {} @data = {}
end end
def step=(sym)
@step = sym
@callback&.call(self)
@step
end
class Operation class Operation
attr_accessor :label, :value, :progress attr_accessor :label, :value, :progress

View File

@@ -3,6 +3,9 @@ class W3DHub
class Task class Task
LOG_TAG = "W3DHub::ApplicationManager::Task".freeze LOG_TAG = "W3DHub::ApplicationManager::Task".freeze
class AbortTaskExecutionError < StandardError
end
# Task failed # Task failed
EVENT_FAILURE = -1 EVENT_FAILURE = -1
# Task started, show application taskbar # Task started, show application taskbar
@@ -10,17 +13,15 @@ class W3DHub
# Task completed successfully # Task completed successfully
EVENT_SUCCESS = 1 EVENT_SUCCESS = 1
# Task progress # Task progress
EVENT_PROGRESS = 2 EVENT_STATUS = 2
# List of packages this task will be working over # Subtask progress
EVENT_PACKAGE_LIST = 3 EVENT_STATUS_OPERATION = 3
# Update a package's status: verifying, downloading, unpacking, patching
EVENT_PACKAGE_STATUS = 4
Context = Data.define( Context = Data.define(
:task_id, :task_id,
:app_type, :app_type,
:app_id, :application,
:channel_id, :channel,
:version, :version,
:target_path, :target_path,
:temp_path :temp_path
@@ -34,6 +35,7 @@ class W3DHub
) )
attr_reader :context, :state attr_reader :context, :state
attr_accessor :status
def initialize(context:) def initialize(context:)
@context = context @context = context
@@ -43,6 +45,8 @@ class W3DHub
# remember all case insensitive file paths # remember all case insensitive file paths
@cache_file_paths = {} @cache_file_paths = {}
@status = Status.new(application: context.application, channel: context.channel)
@failure = false @failure = false
@failure_reason = "" @failure_reason = ""
end end
@@ -51,14 +55,13 @@ class W3DHub
end end
def fail!(reason: "") def fail!(reason: "")
@state = :failed
@failure = true @failure = true
@failure_reason = reason
Ractor.current.send( send_task_result
MessageEvent.new(
context.task_id,
) raise AbortTaskExecutionError, reason
)
end end
def failed? def failed?
@@ -72,12 +75,20 @@ class W3DHub
end end
def send_task_result 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( MessageEvent.new(
context.task_id, context.task_id,
@failure ? EVENT_FAILURE : EVENT_SUCCESS, type,
nil, subtype,
@failure_reason data
) )
) )
end end
@@ -87,9 +98,27 @@ class W3DHub
end end
def start 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 execute_task
sleep 1
@state = failed? ? :failed : :complete
send_task_result send_task_result
sleep 1
rescue StandardError => e
fail!(reason: "Fatal Error\n#{e}") unless e.is_a?(AbortTaskExecutionError)
end end
# returns true on success and false on failure # returns true on success and false on failure
@@ -102,14 +131,52 @@ class W3DHub
# Quick checks before network and computational work starts # Quick checks before network and computational work starts
def fail_fast! 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 end
def fetch_manifests def fetch_manifests(version)
result = CyberarmEngine::Result.new 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 result
end end
@@ -180,8 +247,63 @@ class W3DHub
true true
end 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) 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 result
end end
@@ -216,6 +338,14 @@ class W3DHub
result result
end 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 end
end end

View File

@@ -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)) @@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true, data: data))
end 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 def initialize
@busy = false @busy = false
@jobs = [] @jobs = []

View File

@@ -1,4 +1,8 @@
class W3DHub 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) def self.format_size(bytes)
case bytes case bytes
when 0..1023 # Bytes when 0..1023 # Bytes
@@ -17,15 +21,15 @@ class W3DHub
end end
def self.windows? def self.windows?
RbConfig::CONFIG["host_os"] =~ /(mingw|mswin|windows)/i PLATFORM_WINDOWS
end end
def self.mac? def self.mac?
RbConfig::CONFIG["host_os"] =~ /(darwin|mac os)/i PLATFORM_DARWIN
end end
def self.linux? def self.linux?
RbConfig::CONFIG["host_os"] =~ /(linux|bsd|aix|solaris)/i PLATFORM_LINUX
end end
def self.unix? def self.unix?