mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2025-12-16 09:12:35 +00:00
Removed Async[websocket/http] due to excessive require times and reliablity issues on Windows
This commit is contained in:
78
lib/api.rb
78
lib/api.rb
@@ -4,15 +4,17 @@ class W3DHub
|
||||
|
||||
API_TIMEOUT = 10 # seconds
|
||||
USER_AGENT = "Cyberarm's Linux Friendly W3D Hub Launcher v#{W3DHub::VERSION}".freeze
|
||||
DEFAULT_HEADERS = [
|
||||
["User-Agent", USER_AGENT],
|
||||
["Accept", "application/json"]
|
||||
].freeze
|
||||
FORM_ENCODED_HEADERS = (
|
||||
DEFAULT_HEADERS + [["Content-Type", "application/x-www-form-urlencoded"]]
|
||||
).freeze
|
||||
DEFAULT_HEADERS = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application/json"
|
||||
}.freeze
|
||||
FORM_ENCODED_HEADERS = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}.freeze
|
||||
|
||||
def self.on_fiber(method, *args, &callback)
|
||||
def self.on_thread(method, *args, &callback)
|
||||
BackgroundWorker.job(-> { Api.send(method, *args) }, callback)
|
||||
end
|
||||
|
||||
@@ -27,8 +29,6 @@ class W3DHub
|
||||
ENDPOINT = "https://secure.w3dhub.com".freeze
|
||||
|
||||
def self.post(url, headers = DEFAULT_HEADERS, body = nil)
|
||||
@client ||= Async::HTTP::Client.new(Async::HTTP::Endpoint.parse(ENDPOINT, protocol: Async::HTTP::Protocol::HTTP10))
|
||||
|
||||
# TODO: Check if session has expired and attempt to refresh session before submitting request
|
||||
|
||||
logger.debug(LOG_TAG) { "Fetching POST \"#{url}\"..." }
|
||||
@@ -37,17 +37,15 @@ class W3DHub
|
||||
if Store.account
|
||||
logger.debug(LOG_TAG) { " Injecting Authorization header..." }
|
||||
headers = headers.dup
|
||||
headers << ["Authorization", "Bearer #{Store.account.access_token}"]
|
||||
headers["Authorization"] = "Bearer #{Store.account.access_token}"
|
||||
end
|
||||
|
||||
begin
|
||||
Async::Task.current.with_timeout(API_TIMEOUT) do
|
||||
@client.post(url, headers, body)
|
||||
end
|
||||
rescue Async::TimeoutError
|
||||
Excon.post(url, headers: headers, body: body, tcp_nodelay: true, write_timeout: API_TIMEOUT, read_timeout: API_TIMEOUT, connection_timeout: API_TIMEOUT)
|
||||
rescue Excon::Errors::Timeout
|
||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
||||
DummyResponse.new
|
||||
rescue EOFError => e
|
||||
rescue Excon::Socket::Error => e
|
||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
|
||||
logger.error(LOG_TAG) { e }
|
||||
DummyResponse.new
|
||||
@@ -73,16 +71,16 @@ class W3DHub
|
||||
body = "data=#{JSON.dump({refreshToken: refresh_token})}"
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if response.success?
|
||||
user_data = JSON.parse(response.read, symbolize_names: true)
|
||||
if response.status == 200
|
||||
user_data = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
return false if user_data[:error]
|
||||
|
||||
body = "data=#{JSON.dump({ id: user_data[:userid] })}"
|
||||
user_details = post("#{ENDPOINT}/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if user_details.success?
|
||||
user_details_data = JSON.parse(user_details.read, symbolize_names: true)
|
||||
if user_details.status == 200
|
||||
user_details_data = JSON.parse(user_details.body, symbolize_names: true)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch refresh user details:" }
|
||||
logger.error(LOG_TAG) { user_details }
|
||||
@@ -101,16 +99,16 @@ class W3DHub
|
||||
body = "data=#{JSON.dump({username: username, password: password})}"
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if response.success?
|
||||
user_data = JSON.parse(response.read, symbolize_names: true)
|
||||
if response.status == 200
|
||||
user_data = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
return false if user_data[:error]
|
||||
|
||||
body = "data=#{JSON.dump({ id: user_data[:userid] })}"
|
||||
user_details = post("#{ENDPOINT}/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if user_details.success?
|
||||
user_details_data = JSON.parse(user_details.read, symbolize_names: true)
|
||||
if user_details.status == 200
|
||||
user_details_data = JSON.parse(user_details.body, symbolize_names: true)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch user details:" }
|
||||
logger.error(LOG_TAG) { user_details }
|
||||
@@ -137,8 +135,8 @@ class W3DHub
|
||||
def self.service_status
|
||||
response = post("#{ENDPOINT}/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS)
|
||||
|
||||
if response.success?
|
||||
ServiceStatus.new(response.read)
|
||||
if response.status == 200
|
||||
ServiceStatus.new(response.body)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch service status:" }
|
||||
logger.error(LOG_TAG) { response }
|
||||
@@ -153,8 +151,8 @@ class W3DHub
|
||||
def self.applications
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/get-applications")
|
||||
|
||||
if response.success?
|
||||
Applications.new(response.read)
|
||||
if response.status == 200
|
||||
Applications.new(response.body)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch applications list:" }
|
||||
logger.error(LOG_TAG) { response }
|
||||
@@ -170,8 +168,8 @@ class W3DHub
|
||||
body = "data=#{JSON.dump({category: category})}"
|
||||
response = post("#{ENDPOINT}/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if response.success?
|
||||
News.new(response.read)
|
||||
if response.status == 200
|
||||
News.new(response.body)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch news for:" }
|
||||
logger.error(LOG_TAG) { category }
|
||||
@@ -188,8 +186,8 @@ class W3DHub
|
||||
body = URI.encode_www_form("data": JSON.dump({ packages: packages }))
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if response.success?
|
||||
hash = JSON.parse(response.read, symbolize_names: true)
|
||||
if response.status == 200
|
||||
hash = JSON.parse(response.body, symbolize_names: true)
|
||||
hash[:packages].map { |pkg| Package.new(pkg) }
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch package details for:" }
|
||||
@@ -214,8 +212,8 @@ class W3DHub
|
||||
body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id }))
|
||||
response = post("#{ENDPOINT}/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if response.success?
|
||||
array = JSON.parse(response.read, symbolize_names: true)
|
||||
if response.status == 200
|
||||
array = JSON.parse(response.body, symbolize_names: true)
|
||||
array.map { |e| Event.new(e) }
|
||||
else
|
||||
false
|
||||
@@ -227,11 +225,11 @@ class W3DHub
|
||||
SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze
|
||||
|
||||
def self.get(url, headers = DEFAULT_HEADERS, body = nil)
|
||||
@client ||= Async::HTTP::Client.new(Async::HTTP::Endpoint.parse(SERVER_LIST_ENDPOINT, protocol: Async::HTTP::Protocol::HTTP10))
|
||||
@client ||= Excon.new(SERVER_LIST_ENDPOINT, persistent: true)
|
||||
|
||||
logger.debug(LOG_TAG) { "Fetching GET \"#{url}\"..." }
|
||||
|
||||
@client.get(url, headers, body)
|
||||
Excon.get(url, headers: headers, body: body, persistent: true)
|
||||
end
|
||||
|
||||
# Method: GET
|
||||
@@ -254,8 +252,8 @@ class W3DHub
|
||||
def self.server_list(level = 1)
|
||||
response = get("#{SERVER_LIST_ENDPOINT}/listings/getAll/v2?statusLevel=#{level}")
|
||||
|
||||
if response.success?
|
||||
data = JSON.parse(response.read, symbolize_names: true)
|
||||
if response.status == 200
|
||||
data = JSON.parse(response.body, symbolize_names: true)
|
||||
return data.map { |hash| ServerListServer.new(hash) }
|
||||
end
|
||||
|
||||
@@ -276,8 +274,8 @@ class W3DHub
|
||||
def self.server_details(id, level)
|
||||
response = get("#{SERVER_LIST_ENDPOINT}/listings/getStatus/v2/#{id}?statusLevel=#{level}")
|
||||
|
||||
if response.success?
|
||||
hash = JSON.parse(response.read, symbolize_names: true)
|
||||
if response.status == 200
|
||||
hash = JSON.parse(response.body, symbolize_names: true)
|
||||
return hash
|
||||
end
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ class W3DHub
|
||||
end
|
||||
|
||||
def send_ping(force_ping = false)
|
||||
return
|
||||
|
||||
if force_ping || Gosu.milliseconds - @last_pinged >= @ping_interval
|
||||
@last_pinged = Gosu.milliseconds
|
||||
|
||||
|
||||
@@ -3,65 +3,6 @@ class W3DHub
|
||||
class ServerListUpdater
|
||||
LOG_TAG = "W3DHub::Api::ServerListUpdater".freeze
|
||||
include CyberarmEngine::Common
|
||||
|
||||
##!!! When this breaks update from: https://github.com/socketry/async-websocket/blob/master/lib/async/websocket/connection.rb
|
||||
# refinements preserves super... 😢
|
||||
class PatchedConnection < ::Protocol::WebSocket::Connection
|
||||
include ::Protocol::WebSocket::Headers
|
||||
|
||||
def self.call(framer, protocol = [], **options)
|
||||
instance = self.new(framer, Array(protocol).first, **options)
|
||||
|
||||
return instance unless block_given?
|
||||
|
||||
begin
|
||||
yield instance
|
||||
ensure
|
||||
instance.close
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(framer, protocol = nil, response: nil, **options)
|
||||
super(framer, **options)
|
||||
|
||||
@protocol = protocol
|
||||
@response = response
|
||||
end
|
||||
|
||||
def close
|
||||
super
|
||||
|
||||
if @response
|
||||
@response.finish
|
||||
@response = nil
|
||||
end
|
||||
end
|
||||
|
||||
attr :protocol
|
||||
|
||||
def read
|
||||
if (buffer = super)
|
||||
buffer.split("\x1e").map { |json| parse(json) }
|
||||
end
|
||||
end
|
||||
|
||||
def write(object)
|
||||
super("#{dump(object)}\x1e")
|
||||
end
|
||||
|
||||
def parse(buffer)
|
||||
JSON.parse(buffer, symbolize_names: true)
|
||||
end
|
||||
|
||||
def dump(object)
|
||||
JSON.dump(object)
|
||||
end
|
||||
|
||||
def call
|
||||
self.close
|
||||
end
|
||||
end
|
||||
|
||||
@@instance = nil
|
||||
|
||||
def self.instance
|
||||
@@ -87,45 +28,36 @@ class W3DHub
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
logger.debug(LOG_TAG) { "Cleaning up..." }
|
||||
@@instance = nil
|
||||
end
|
||||
|
||||
def connect
|
||||
Async do |task|
|
||||
internet = Async::HTTP::Internet.instance
|
||||
auto_reconnect = false
|
||||
|
||||
logger.debug(LOG_TAG) { "Requesting connection token..." }
|
||||
response = internet.post("https://gsh.w3dhub.com/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, [""])
|
||||
data = JSON.parse(response.read, symbolize_names: true)
|
||||
logger.debug(LOG_TAG) { "Requesting connection token..." }
|
||||
response = Excon.post("https://gsh.w3dhub.com/listings/push/v2/negotiate?negotiateVersion=1", headers: Api::DEFAULT_HEADERS, body: "")
|
||||
data = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
id = data[:connectionToken]
|
||||
endpoint = Async::HTTP::Endpoint.parse("https://gsh.w3dhub.com/listings/push/v2?id=#{id}", alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
|
||||
id = data[:connectionToken]
|
||||
endpoint = "https://gsh.w3dhub.com/listings/push/v2?id=#{id}"
|
||||
|
||||
logger.debug(LOG_TAG) { "Connecting to websocket..." }
|
||||
Async::WebSocket::Client.connect(endpoint, headers: Api::DEFAULT_HEADERS, handler: PatchedConnection) do |connection|
|
||||
logger.debug(LOG_TAG) { "Requesting json protocol, v1..." }
|
||||
connection.write({ protocol: "json", version: 1 })
|
||||
connection.flush
|
||||
logger.debug(LOG_TAG) { "Received: #{connection.read}" }
|
||||
logger.debug(LOG_TAG) { "Sending \"PING\"(?)" }
|
||||
connection.write({ "type": 6 })
|
||||
logger.debug(LOG_TAG) { "Connecting to websocket..." }
|
||||
WebSocket::Client::Simple.connect(endpoint, headers: Api::DEFAULT_HEADERS) do |ws|
|
||||
ws.on(:message) do |msg|
|
||||
msg = msg.data.split("\x1e").first
|
||||
|
||||
logger.debug(LOG_TAG) { "Subscribing to server changes..." }
|
||||
Store.server_list.each_with_index do |server, i|
|
||||
i += 1
|
||||
mode = 1 # 2 full details, 1 basic details
|
||||
out = { "type": 1, "invocationId": "#{i}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, mode] }
|
||||
connection.write(out)
|
||||
end
|
||||
hash = JSON.parse(msg, symbolize_names: true)
|
||||
|
||||
logger.debug(LOG_TAG) { "Waiting for data..." }
|
||||
while (message = connection.read)
|
||||
connection.write({ type: 6 }) if message.first[:type] == 6
|
||||
|
||||
if message&.first&.fetch(:type) == 1
|
||||
message.each do |rpc|
|
||||
next unless rpc[:target] == "ServerStatusChanged"
|
||||
|
||||
id, data = rpc[:arguments]
|
||||
# Send PING(?)
|
||||
if hash.empty? || hash[:type] == 6
|
||||
ws.send({ type: 6 }.to_json + "\x1e")
|
||||
else
|
||||
case hash[:type]
|
||||
when 1
|
||||
if hash[:target] == "ServerStatusChanged"
|
||||
id, data = hash[:arguments]
|
||||
server = Store.server_list.find { |s| s.id == id }
|
||||
server_updated = server&.update(data)
|
||||
States::Interface.instance&.update_server_browser(server) if server_updated
|
||||
@@ -133,10 +65,32 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
end
|
||||
ensure
|
||||
logger.debug(LOG_TAG) { "Cleaning up..." }
|
||||
@@instance = nil
|
||||
|
||||
ws.on(:open) do
|
||||
logger.debug(LOG_TAG) { "Requesting json protocol, v1..." }
|
||||
ws.send({ protocol: "json", version: 1 }.to_json + "\x1e")
|
||||
|
||||
logger.debug(LOG_TAG) { "Subscribing to server changes..." }
|
||||
Store.server_list.each_with_index do |server, i|
|
||||
i += 1
|
||||
mode = 1 # 2 full details, 1 basic details
|
||||
out = { "type": 1, "invocationId": "#{i}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, mode] }
|
||||
ws.send(out.to_json + "\x1e")
|
||||
end
|
||||
end
|
||||
|
||||
ws.on(:close) do |e|
|
||||
p e
|
||||
auto_reconnect = true
|
||||
end
|
||||
|
||||
ws.on(:error) do |e|
|
||||
p e
|
||||
auto_reconnect = true
|
||||
end
|
||||
end
|
||||
|
||||
connect if auto_reconnect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,7 +56,7 @@ class W3DHub
|
||||
@task_state = :running
|
||||
|
||||
Thread.new do
|
||||
Sync do
|
||||
# Sync do
|
||||
begin
|
||||
status = execute_task
|
||||
rescue FailFast
|
||||
@@ -78,7 +78,7 @@ class W3DHub
|
||||
|
||||
hide_application_taskbar if @task_state == :failed
|
||||
send_message_dialog(:failure, "Task #{type.inspect} failed for #{@application.name}", @task_failure_reason) if @task_state == :failed && !@fail_silently
|
||||
end
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,13 +9,12 @@ class W3DHub
|
||||
logger.info(LOG_TAG) { "Starting background job worker..." }
|
||||
|
||||
|
||||
@@thread = Thread.current
|
||||
@@alive = true
|
||||
@@run = true
|
||||
@@instance = self.new
|
||||
|
||||
Async do
|
||||
@@instance.handle_jobs
|
||||
end
|
||||
@@instance.handle_jobs
|
||||
end
|
||||
|
||||
def self.instance
|
||||
@@ -30,10 +29,18 @@ class W3DHub
|
||||
@@alive
|
||||
end
|
||||
|
||||
def self.busy?
|
||||
instance&.busy?
|
||||
end
|
||||
|
||||
def self.shutdown!
|
||||
@@run = false
|
||||
end
|
||||
|
||||
def self.kill!
|
||||
@@thread.kill
|
||||
end
|
||||
|
||||
def self.job(job, callback, error_handler = nil)
|
||||
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler))
|
||||
end
|
||||
@@ -43,6 +50,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
def initialize
|
||||
@busy = false
|
||||
@jobs = []
|
||||
end
|
||||
|
||||
@@ -50,12 +58,16 @@ class W3DHub
|
||||
while BackgroundWorker.run?
|
||||
job = @jobs.shift
|
||||
|
||||
@busy = true
|
||||
|
||||
begin
|
||||
job&.do
|
||||
rescue => error
|
||||
job&.raise_error(error)
|
||||
end
|
||||
|
||||
@busy = !@jobs.empty?
|
||||
|
||||
sleep 0.1
|
||||
end
|
||||
|
||||
@@ -67,6 +79,10 @@ class W3DHub
|
||||
@jobs << job
|
||||
end
|
||||
|
||||
def busy?
|
||||
@busy
|
||||
end
|
||||
|
||||
class Job
|
||||
def initialize(job:, callback:, error_handler: nil, deliver_to_queue: false)
|
||||
@job = job
|
||||
|
||||
10
lib/cache.rb
10
lib/cache.rb
@@ -16,12 +16,12 @@ class W3DHub
|
||||
path
|
||||
elsif async
|
||||
BackgroundWorker.job(
|
||||
-> { Async::HTTP::Internet.instance.get(uri, W3DHub::Api::DEFAULT_HEADERS) },
|
||||
->(response) { response.save(path, "wb") if response.success? }
|
||||
-> { Api.get(uri, W3DHub::Api::DEFAULT_HEADERS) },
|
||||
->(response) { File.open(path, "wb") { |f| f.write response.body } if response.status == 200 }
|
||||
)
|
||||
else
|
||||
response = Async::HTTP::Internet.instance.get(uri, W3DHub::Api::DEFAULT_HEADERS)
|
||||
response.save(path, "wb") if response.success?
|
||||
response = Api.get(uri, W3DHub::Api::DEFAULT_HEADERS)
|
||||
File.open(path, "wb") { |f| f.write response.body } if response.status == 200
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,7 +82,7 @@ class W3DHub
|
||||
block.call(chunk, remaining_bytes, total_bytes)
|
||||
end
|
||||
|
||||
response.success?
|
||||
response.status == 200
|
||||
ensure
|
||||
file&.close
|
||||
end
|
||||
|
||||
@@ -40,6 +40,14 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
def self.commmand(command)
|
||||
if windows?
|
||||
|
||||
else
|
||||
IO.popen(command)
|
||||
end
|
||||
end
|
||||
|
||||
def self.home_directory
|
||||
File.expand_path("~")
|
||||
end
|
||||
|
||||
@@ -78,7 +78,7 @@ class W3DHub
|
||||
logger.info(LOG_TAG) { "Refreshing user login..." }
|
||||
|
||||
# TODO: Check without network
|
||||
Api.on_fiber(:refresh_user_login, account.refresh_token) do |refreshed_account|
|
||||
Api.on_thread(:refresh_user_login, account.refresh_token) do |refreshed_account|
|
||||
update_account_data(refreshed_account)
|
||||
end
|
||||
|
||||
@@ -108,7 +108,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
def service_status
|
||||
Api.on_fiber(:service_status) do |service_status|
|
||||
Api.on_thread(:service_status) do |service_status|
|
||||
@service_status = service_status
|
||||
|
||||
if @service_status
|
||||
@@ -132,7 +132,7 @@ class W3DHub
|
||||
def applications
|
||||
@status_label.value = I18n.t(:"boot.checking_for_updates")
|
||||
|
||||
Api.on_fiber(:applications) do |applications|
|
||||
Api.on_thread(:applications) do |applications|
|
||||
if applications
|
||||
Store.applications = applications
|
||||
|
||||
@@ -152,7 +152,7 @@ class W3DHub
|
||||
packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" }
|
||||
end
|
||||
|
||||
Api.on_fiber(:package_details, packages) do |package_details|
|
||||
Api.on_thread(:package_details, packages) do |package_details|
|
||||
package_details&.each do |package|
|
||||
path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
||||
generated_icon_path = "#{GAME_ROOT_PATH}/media/icons/#{package.subcategory}.png"
|
||||
@@ -180,7 +180,7 @@ class W3DHub
|
||||
def server_list
|
||||
@status_label.value = I18n.t(:"server_browser.fetching_server_list")
|
||||
|
||||
Api.on_fiber(:server_list, 2) do |list|
|
||||
Api.on_thread(:server_list, 2) do |list|
|
||||
Store.server_list = list.sort_by! { |s| s&.status&.players&.size }.reverse if list
|
||||
|
||||
Store.server_list_last_fetch = Gosu.milliseconds
|
||||
|
||||
@@ -35,6 +35,9 @@ class W3DHub
|
||||
while (block = Store.main_thread_queue.shift)
|
||||
block&.call
|
||||
end
|
||||
|
||||
# Manually sleep main thread so that the BackgroundWorker thread can be scheduled
|
||||
sleep(update_interval / 1000.0) if W3DHub::BackgroundWorker.busy?
|
||||
end
|
||||
|
||||
def gain_focus
|
||||
|
||||
Reference in New Issue
Block a user