diff --git a/Gemfile.lock b/Gemfile.lock index 3e2ea84..465bbfc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,7 +43,7 @@ GEM fiber-storage (1.0.1) fiddle (1.1.8) gosu (1.4.6) - io-endpoint (0.17.1) + io-endpoint (0.17.2) io-event (1.14.2) io-stream (0.11.1) ircparser (1.0.0) @@ -97,4 +97,4 @@ DEPENDENCIES win32-security BUNDLED WITH - 2.6.8 + 4.0.3 diff --git a/lib/api.rb b/lib/api.rb index 1d33121..a7d4d6e 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -16,32 +16,7 @@ class W3DHub ].freeze def self.on_thread(method, *args, &callback) - raise "Renew." - BackgroundWorker.foreground_job(-> { Api.send(method, *args) }, callback) - end - - class Response - def initialize(error: nil, status: -1, body: "") - @status = status - @body = body - @error = error - end - - def success? - @status == 200 - end - - def status - @status - end - - def body - @body - end - - def error - @error - end + Api.send(method, *args, &callback) end #! === W3D Hub API === !# @@ -50,7 +25,9 @@ class W3DHub HTTP_CLIENTS = {} - def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback) + raise "NO CALLBACK DEFINED!" unless callback + case backend when :w3dhub endpoint = W3DHUB_API_ENDPOINT @@ -79,24 +56,7 @@ class W3DHub headers << ["authorization", "Bearer #{Store.account.access_token}"] end - Sync do - begin - response = provision_http_client(endpoint).send(method, path, headers, body) - - Response.new(status: response.status, body: response.read) - rescue Async::TimeoutError => e - logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" } - - Response.new(error: e) - rescue StandardError => e - logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" } - logger.error(LOG_TAG) { e } - - Response.new(error: e) - ensure - response&.close - end - end + Store.network_manager.request(method, url, headers, body, nil, &callback) end def self.provision_http_client(hostname) @@ -114,17 +74,17 @@ class W3DHub HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint) end - def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) - async_http(:post, path, headers, body, backend) + def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback) + async_http(:post, path, headers, body, backend, &callback) end - def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) - async_http(:get, path, headers, body, backend) + def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback) + async_http(:get, path, headers, body, backend, &callback) end # Api.get but handles any URL instead of known hosts - def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil) - async_http(:get, path, headers, body, backend) + def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil, &callback) + async_http(:get, path, headers, body, backend, &callback) end # Method: POST @@ -142,27 +102,27 @@ class W3DHub # # On a failed login the service responds with: # {"error":"login-failed"} - def self.refresh_user_login(refresh_token, backend = :w3dhub) + def self.refresh_user_login(refresh_token, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({refreshToken: refresh_token})) - response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) + post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) do |result| + if result.okay? + user_data = JSON.parse(result.data, symbolize_names: true) - if response.status == 200 - user_data = JSON.parse(response.body, symbolize_names: true) + return false if user_data[:error] - return false if user_data[:error] + user_details_data = user_details(user_data[:userid]) || {} - user_details_data = user_details(user_data[:userid]) || {} - - Account.new(user_data, user_details_data) - else - logger.error(LOG_TAG) { "Failed to fetch refresh user login:" } - logger.error(LOG_TAG) { response } - false + Account.new(user_data, user_details_data) + else + logger.error(LOG_TAG) { "Failed to fetch refresh user login:" } + logger.error(LOG_TAG) { response } + false + end end end # See #user_refresh_token - def self.user_login(username, password, backend = :w3dhub) + def self.user_login(username, password, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({username: username, password: password})) response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) @@ -184,7 +144,7 @@ class W3DHub # /apis/w3dhub/1/get-user-details # # Response: avatar-uri (Image download uri), id, username - def self.user_details(id, backend = :w3dhub) + def self.user_details(id, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({ id: id })) user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend) @@ -200,15 +160,15 @@ class W3DHub # /apis/w3dhub/1/get-service-status # Service response: # {"services":{"authentication":true,"packageDownload":true}} - def self.service_status(backend = :w3dhub) - response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend) - - if response.status == 200 - ServiceStatus.new(response.body) - else - logger.error(LOG_TAG) { "Failed to fetch service status:" } - logger.error(LOG_TAG) { response } - false + def self.service_status(backend = :w3dhub, &callback) + response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend) do |result| + if result.okay? + ServiceStatus.new(result.data) + else + logger.error(LOG_TAG) { "Failed to fetch service status:" } + logger.error(LOG_TAG) { response } + false + end end end @@ -216,7 +176,7 @@ class W3DHub # Client sends an Authorization header bearer token which is received from logging in (Optional) # Launcher sends an empty data request: data={} # Response is a list of applications/games - def self.applications(backend = :w3dhub) + def self.applications(backend = :w3dhub, &callback) response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend) if response.status == 200 @@ -301,7 +261,7 @@ class W3DHub # Client sends an Authorization header bearer token which is received from logging in (Optional) # Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates) # Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one - def self.news(category, backend = :w3dhub) + def self.news(category, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({category: category})) response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend) @@ -319,7 +279,7 @@ class W3DHub # /apis/launcher/1/get-package-details # client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]} - def self.package_details(packages, backend = :w3dhub) + def self.package_details(packages, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({ packages: packages })) response = post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend) @@ -339,14 +299,15 @@ class W3DHub # client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"} # # server responds with download bytes, probably supports chunked download and resume - def self.package(package, &block) - Cache.fetch_package(package, block) + # FIXME: REFACTOR Cache.fetch_package to use HttpClient + def self.package(package, &callback) + Cache.fetch_package(package, callback) end # /apis/w3dhub/1/get-events # # clients requests events: data={"serverPath":"apb"} - def self.events(app_id, backend = :w3dhub) + def self.events(app_id, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id })) response = post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend) @@ -381,15 +342,25 @@ class W3DHub # id, name, score, kills, deaths # ...players[]: # nick, team (index of teams array), score, kills, deaths - def self.server_list(level = 1, backend = :gsh) - response = get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend) + def self.server_list(level = 1, backend = :gsh, &callback) + handler = lambda do |result| + unless result.okay? + callback.call(false) + next + end - if response.status == 200 - data = JSON.parse(response.body, symbolize_names: true) - return data.map { |hash| ServerListServer.new(hash) } + data = JSON.parse(result.data, symbolize_names: true) + callback.call(data.map { |hash| ServerListServer.new(hash) }) end - false + get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend, &handler) + + # if response.status == 200 + # data = JSON.parse(response.body, symbolize_names: true) + # callback&.call(data.map { |hash| ServerListServer.new(hash) }) + # end + + # callback&.call(false) end # /listings/getStatus/v2/:id?statusLevel=#{0-2} @@ -403,7 +374,7 @@ class W3DHub # id, name, score, kills, deaths # ...players[]: # nick, team (index of teams array), score, kills, deaths - def self.server_details(id, level, backend = :gsh) + def self.server_details(id, level, backend = :gsh, &callback) return false unless id && level response = get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend) @@ -419,7 +390,7 @@ class W3DHub # /listings/push/v2/negotiate?negotiateVersion=1 ##? /listings/push/v2/?id=#{websocket token?} ## Websocket server list listener - def self.server_list_push(id) + def self.server_list_push(id, &callback) end end end diff --git a/lib/api/server_list_updater.rb b/lib/api/server_list_updater.rb index ddeffdf..6dbd0cf 100644 --- a/lib/api/server_list_updater.rb +++ b/lib/api/server_list_updater.rb @@ -23,7 +23,7 @@ class W3DHub @invocation_id = 0 logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." } - run + # run end def run @@ -32,7 +32,8 @@ class W3DHub begin @auto_reconnect = true - while W3DHub::BackgroundWorker.alive? + # FIXME + while true #W3DHub::BackgroundWorker.alive? connect if @auto_reconnect sleep @reconnection_delay end @@ -54,19 +55,31 @@ class W3DHub @auto_reconnect = false logger.debug(LOG_TAG) { "Requesting connection token..." } - response = Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh) - if response.status != 200 + result = nil + Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh) do |callback_result| + result = callback_result + end + + # FIXME: we've introduced ourselves to callback hell, yay! + while result.nil? + sleep 0.1 + end + + if result.error? @auto_reconnect = true - @reconnection_delay = @reconnection_delay * 2 + @reconnection_delay *= 2 @reconnection_delay = 60 if @reconnection_delay > 60 + return end @reconnection_delay = 1 - data = JSON.parse(response.body, symbolize_names: true) + connect_websocket(JSON.parse(result.data, symbolize_names: true)) + end + def connect_websocket(data) @invocation_id = 0 if @invocation_id > 9095 id = data[:connectionToken] endpoint = "#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2?id=#{id}" diff --git a/lib/network_manager.rb b/lib/network_manager.rb index d44596d..131a748 100644 --- a/lib/network_manager.rb +++ b/lib/network_manager.rb @@ -5,6 +5,7 @@ class W3DHub Request = Struct.new(:context, :callback) Context = Data.define( :request_id, + :method, :url, :headers, :body, @@ -12,52 +13,42 @@ class W3DHub ) def initialize - @requests = {} + @requests = [] + @running = true - @ractor = Ractor.new do - raise "Something has gone quite wrong!" if Ractor.main? + Thread.new do + http_client = HttpClient.new - queue = [] - api_client = ApiClient.new + Sync do + while @running + request = @requests.shift - # Ractor has no concept of non-blocking send/receive... :cry: - Thread.new do - while (context = Ractor.receive) # blocking - # we cannot (easily) ensure we always are receive expected data - next unless context.is_a?(Context) - - queue << context - end - end - - Async do - loop do - context = queue.shift - - # goto sleep for an instant if there is no work to be doing - unless context - sleep 0.1 + # goto sleep for an second if there is no work to be doing + unless request + sleep 1 next end - Sync do - result = api_client.handle(context) + Async do |task| + assigned_request = request + result = http_client.handle(task, assigned_request) - Ractor.yield(NetworkEvent.new(context, result)) + pp [assigned_request, result] + + Store.main_thread_queue << -> { assigned_request.callback.call(result) } end end end end - - monitor end - def add_request(url, headers, body, bearer_token, &block) + def request(method, url, headers, body, bearer_token, &block) request_id = SecureRandom.hex - @requests << Request.new( + request = Request.new( Context.new( request_id, + method, url, headers, body, @@ -66,31 +57,9 @@ class W3DHub block ) - @ractor.send(context) + @requests << request request_id end - - def monitor - raise "Something has gone quite wrong!!!" unless Ractor.main? - - # Thread that spends its days sleeping **yawn** - Thread.new do - while (event = @ractor.take) - pp event - - next unless event.is_a?(NetworkEvent) - - request = @request.find { |r| r.context.request_id == event.context.request_id } - - next if request - - @requests.delete(request) - result = event.result - - Store.main_thread_queue << ->(result) { request.callback(result) } - end - end - end end end diff --git a/lib/network_manager/api_client.rb b/lib/network_manager/api_client.rb deleted file mode 100644 index 917bfe1..0000000 --- a/lib/network_manager/api_client.rb +++ /dev/null @@ -1,9 +0,0 @@ -class W3DHub - class NetworkManager - # Api reimplemented in a Ractor friendly manner - class ApiClient - def initialize - end - end - end -end diff --git a/lib/network_manager/http_client.rb b/lib/network_manager/http_client.rb new file mode 100644 index 0000000..a891d1c --- /dev/null +++ b/lib/network_manager/http_client.rb @@ -0,0 +1,61 @@ +class W3DHub + class NetworkManager + # non-blocking, http requests. + class HttpClient + def initialize + @http_clients = {} + end + + def handle(task, request) + result = CyberarmEngine::Result.new + context = request.context + + task.with_timeout(30) do + uri = URI(context.url) + + pp uri + + response = provision_http_client(uri.origin).send( + context.method, + uri.path, + context.headers, + context.body + ) + + pp response + + if response.success? + result.data = response.body.read + else + result.error = response + end + rescue Async::TimeoutError => e + result.error = e + rescue StandardError => e + result.error = e + ensure + response&.close + end + + result + end + + def provision_http_client(hostname) + return @http_clients[hostname.downcase] if @http_clients[hostname.downcase] + + ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil + ssl_context&.set_params( + ca_file: W3DHub.ca_bundle_path, + verify_mode: OpenSSL::SSL::VERIFY_PEER + ) + + endpoint = Async::HTTP::Endpoint.parse(hostname, ssl_context: ssl_context) + @http_clients[hostname.downcase] = Async::HTTP::Client.new(endpoint) + end + + def wrapped_error(error) + WrappedError.new(error.class, error.message.to_s, error.backtrace) + end + end + end +end diff --git a/lib/states/boot.rb b/lib/states/boot.rb index 92cbca4..0651288 100644 --- a/lib/states/boot.rb +++ b/lib/states/boot.rb @@ -175,7 +175,11 @@ class W3DHub end def service_status + @status_label.value = "Checking service status..." #I18n.t(:"server_browser.fetching_server_list") + Api.on_thread(:service_status) do |service_status| + pp service_status + @service_status = service_status if @service_status diff --git a/w3d_hub_linux_launcher.rb b/w3d_hub_linux_launcher.rb index d30fdf2..04b9d11 100644 --- a/w3d_hub_linux_launcher.rb +++ b/w3d_hub_linux_launcher.rb @@ -113,7 +113,7 @@ require_relative "lib/hardware_survey" require_relative "lib/game_settings" require_relative "lib/websocket_client" require_relative "lib/network_manager" -require_relative "lib/network_manager/api_client" +require_relative "lib/network_manager/http_client" require_relative "lib/application_manager" require_relative "lib/application_manager/manifest" require_relative "lib/application_manager/status"