mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-06-04 01:21:03 +03:00
CAPI: Some progress in using a Queue to signal result/error
Due to main app being Tk we can't just use Python async functionality. So instead we have a class to hold a message and optional exception. And instance of that goes into a queue. The reading of that in the main thread is triggered by sending a Tk event. Much more to come.
This commit is contained in:
parent
2fc1568bf7
commit
655c7ea1ca
@ -7,6 +7,7 @@ import html
|
|||||||
import json
|
import json
|
||||||
import locale
|
import locale
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import queue
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
# import threading
|
# import threading
|
||||||
@ -14,6 +15,7 @@ import webbrowser
|
|||||||
from builtins import object, str
|
from builtins import object, str
|
||||||
from os import chdir, environ
|
from os import chdir, environ
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
|
from queue import Queue
|
||||||
from sys import platform
|
from sys import platform
|
||||||
from time import localtime, strftime, time
|
from time import localtime, strftime, time
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
@ -384,6 +386,8 @@ class AppWindow(object):
|
|||||||
def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly factor something out
|
def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly factor something out
|
||||||
|
|
||||||
self.holdofftime = config.get_int('querytime', default=0) + companion.holdoff
|
self.holdofftime = config.get_int('querytime', default=0) + companion.holdoff
|
||||||
|
self.capi_response_queue: Queue = Queue()
|
||||||
|
companion.session.set_capi_response_queue(self.capi_response_queue)
|
||||||
|
|
||||||
self.w = master
|
self.w = master
|
||||||
self.w.title(applongname)
|
self.w.title(applongname)
|
||||||
@ -933,126 +937,138 @@ class AppWindow(object):
|
|||||||
|
|
||||||
def capi_handle_response(self, event=None):
|
def capi_handle_response(self, event=None):
|
||||||
"""Handle the resulting data from a CAPI query."""
|
"""Handle the resulting data from a CAPI query."""
|
||||||
data = ...
|
try:
|
||||||
# Validation
|
data = self.capi_response_queue.get(block=False)
|
||||||
if 'commander' not in data:
|
|
||||||
# This can happen with EGS Auth if no commander created yet
|
|
||||||
# LANG: No data was returned for the commander from the Frontier CAPI
|
|
||||||
err = self.status['text'] = _('CAPI: No commander data returned')
|
|
||||||
|
|
||||||
elif not data.get('commander', {}).get('name'):
|
except queue.Empty:
|
||||||
# LANG: We didn't have the commander name when we should have
|
logger.error('There was no response in the queue!')
|
||||||
err = self.status['text'] = _("Who are you?!") # Shouldn't happen
|
# TODO: Set status text
|
||||||
|
return
|
||||||
|
|
||||||
elif (not data.get('lastSystem', {}).get('name')
|
else:
|
||||||
or (data['commander'].get('docked')
|
if isinstance(data, companion.CAPIFailedRequest):
|
||||||
and not data.get('lastStarport', {}).get('name'))):
|
logger.trace_if('capi.worker', f'Failed Request: {data.message}')
|
||||||
# LANG: We don't know where the commander is, when we should
|
raise data.exception
|
||||||
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
|
|
||||||
|
|
||||||
elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
|
# Validation
|
||||||
# LANG: We don't know what ship the commander is in, when we should
|
if 'commander' not in data:
|
||||||
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
|
# This can happen with EGS Auth if no commander created yet
|
||||||
|
# LANG: No data was returned for the commander from the Frontier CAPI
|
||||||
|
err = self.status['text'] = _('CAPI: No commander data returned')
|
||||||
|
|
||||||
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
|
elif not data.get('commander', {}).get('name'):
|
||||||
# Companion API Commander doesn't match Journal
|
# LANG: We didn't have the commander name when we should have
|
||||||
raise companion.CmdrError()
|
err = self.status['text'] = _("Who are you?!") # Shouldn't happen
|
||||||
|
|
||||||
elif auto_update and not monitor.state['OnFoot'] and not data['commander'].get('docked'):
|
elif (not data.get('lastSystem', {}).get('name')
|
||||||
# auto update is only when just docked
|
or (data['commander'].get('docked')
|
||||||
logger.warning(f"{auto_update!r} and not {monitor.state['OnFoot']!r} and "
|
and not data.get('lastStarport', {}).get('name'))):
|
||||||
f"not {data['commander'].get('docked')!r}")
|
# LANG: We don't know where the commander is, when we should
|
||||||
raise companion.ServerLagging()
|
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
|
||||||
|
|
||||||
elif data['lastSystem']['name'] != monitor.system:
|
elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
|
||||||
# CAPI system must match last journal one
|
# LANG: We don't know what ship the commander is in, when we should
|
||||||
logger.warning(f"{data['lastSystem']['name']!r} != {monitor.system!r}")
|
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
|
||||||
raise companion.ServerLagging()
|
|
||||||
|
|
||||||
elif data['lastStarport']['name'] != monitor.station:
|
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
|
||||||
if monitor.state['OnFoot'] and monitor.station:
|
# Companion API Commander doesn't match Journal
|
||||||
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
raise companion.CmdrError()
|
||||||
f"{monitor.state['OnFoot']!r} and {monitor.station!r}")
|
|
||||||
raise companion.ServerLagging()
|
|
||||||
|
|
||||||
else:
|
elif auto_update and not monitor.state['OnFoot'] and not data['commander'].get('docked'):
|
||||||
last_station = None
|
# auto update is only when just docked
|
||||||
if data['commander']['docked']:
|
logger.warning(f"{auto_update!r} and not {monitor.state['OnFoot']!r} and "
|
||||||
last_station = data['lastStarport']['name']
|
f"not {data['commander'].get('docked')!r}")
|
||||||
|
raise companion.ServerLagging()
|
||||||
|
|
||||||
if monitor.station is None:
|
elif data['lastSystem']['name'] != monitor.system:
|
||||||
# Likely (re-)Embarked on ship docked at an EDO settlement.
|
# CAPI system must match last journal one
|
||||||
# Both Disembark and Embark have `"Onstation": false` in Journal.
|
logger.warning(f"{data['lastSystem']['name']!r} != {monitor.system!r}")
|
||||||
# So there's nothing to tell us which settlement we're (still,
|
raise companion.ServerLagging()
|
||||||
# or now, if we came here in Apex and then recalled ship) docked at.
|
|
||||||
logger.debug("monitor.station is None - so EDO settlement?")
|
|
||||||
raise companion.NoMonitorStation()
|
|
||||||
|
|
||||||
if last_station != monitor.station:
|
elif data['lastStarport']['name'] != monitor.station:
|
||||||
# CAPI lastStarport must match
|
if monitor.state['OnFoot'] and monitor.station:
|
||||||
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
||||||
f"{last_station!r} != {monitor.station!r}")
|
f"{monitor.state['OnFoot']!r} and {monitor.station!r}")
|
||||||
raise companion.ServerLagging()
|
|
||||||
|
|
||||||
self.holdofftime = querytime + companion.holdoff
|
|
||||||
|
|
||||||
elif not monitor.state['OnFoot'] and data['ship']['id'] != monitor.state['ShipID']:
|
|
||||||
# CAPI ship must match
|
|
||||||
logger.warning(f"not {monitor.state['OnFoot']!r} and "
|
|
||||||
f"{data['ship']['id']!r} != {monitor.state['ShipID']!r}")
|
|
||||||
raise companion.ServerLagging()
|
|
||||||
|
|
||||||
elif not monitor.state['OnFoot'] and data['ship']['name'].lower() != monitor.state['ShipType']:
|
|
||||||
# CAPI ship type must match
|
|
||||||
logger.warning(f"not {monitor.state['OnFoot']!r} and "
|
|
||||||
f"{data['ship']['name'].lower()!r} != {monitor.state['ShipType']!r}")
|
|
||||||
raise companion.ServerLagging()
|
raise companion.ServerLagging()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if __debug__: # Recording
|
last_station = None
|
||||||
companion.session.dump_capi_data(data)
|
if data['commander']['docked']:
|
||||||
|
last_station = data['lastStarport']['name']
|
||||||
|
|
||||||
if not monitor.state['ShipType']: # Started game in SRV or fighter
|
if monitor.station is None:
|
||||||
self.ship['text'] = ship_name_map.get(data['ship']['name'].lower(), data['ship']['name'])
|
# Likely (re-)Embarked on ship docked at an EDO settlement.
|
||||||
monitor.state['ShipID'] = data['ship']['id']
|
# Both Disembark and Embark have `"Onstation": false` in Journal.
|
||||||
monitor.state['ShipType'] = data['ship']['name'].lower()
|
# So there's nothing to tell us which settlement we're (still,
|
||||||
|
# or now, if we came here in Apex and then recalled ship) docked at.
|
||||||
|
logger.debug("monitor.station is None - so EDO settlement?")
|
||||||
|
raise companion.NoMonitorStation()
|
||||||
|
|
||||||
if not monitor.state['Modules']:
|
if last_station != monitor.station:
|
||||||
self.ship.configure(state=tk.DISABLED)
|
# CAPI lastStarport must match
|
||||||
|
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
||||||
|
f"{last_station!r} != {monitor.station!r}")
|
||||||
|
raise companion.ServerLagging()
|
||||||
|
|
||||||
# We might have disabled this in the conditional above.
|
self.holdofftime = querytime + companion.holdoff
|
||||||
if monitor.state['Modules']:
|
|
||||||
self.ship.configure(state=True)
|
|
||||||
|
|
||||||
if monitor.state.get('SuitCurrent') is not None:
|
elif not monitor.state['OnFoot'] and data['ship']['id'] != monitor.state['ShipID']:
|
||||||
if (loadout := data.get('loadout')) is not None:
|
# CAPI ship must match
|
||||||
if (suit := loadout.get('suit')) is not None:
|
logger.warning(f"not {monitor.state['OnFoot']!r} and "
|
||||||
if (suitname := suit.get('edmcName')) is not None:
|
f"{data['ship']['id']!r} != {monitor.state['ShipID']!r}")
|
||||||
# We've been paranoid about loadout->suit->suitname, now just assume loadouts is there
|
raise companion.ServerLagging()
|
||||||
loadout_name = index_possibly_sparse_list(
|
|
||||||
data['loadouts'], loadout['loadoutSlotId']
|
|
||||||
)['name']
|
|
||||||
|
|
||||||
self.suit['text'] = f'{suitname} ({loadout_name})'
|
elif not monitor.state['OnFoot'] and data['ship']['name'].lower() != monitor.state['ShipType']:
|
||||||
|
# CAPI ship type must match
|
||||||
|
logger.warning(f"not {monitor.state['OnFoot']!r} and "
|
||||||
|
f"{data['ship']['name'].lower()!r} != {monitor.state['ShipType']!r}")
|
||||||
|
raise companion.ServerLagging()
|
||||||
|
|
||||||
self.suit_show_if_set()
|
else:
|
||||||
|
if __debug__: # Recording
|
||||||
|
companion.session.dump_capi_data(data)
|
||||||
|
|
||||||
if data['commander'].get('credits') is not None:
|
if not monitor.state['ShipType']: # Started game in SRV or fighter
|
||||||
monitor.state['Credits'] = data['commander']['credits']
|
self.ship['text'] = ship_name_map.get(data['ship']['name'].lower(), data['ship']['name'])
|
||||||
monitor.state['Loan'] = data['commander'].get('debt', 0)
|
monitor.state['ShipID'] = data['ship']['id']
|
||||||
|
monitor.state['ShipType'] = data['ship']['name'].lower()
|
||||||
|
|
||||||
# stuff we can do when not docked
|
if not monitor.state['Modules']:
|
||||||
err = plug.notify_newdata(data, monitor.is_beta)
|
self.ship.configure(state=tk.DISABLED)
|
||||||
self.status['text'] = err and err or ''
|
|
||||||
if err:
|
|
||||||
play_bad = True
|
|
||||||
|
|
||||||
# Export market data
|
# We might have disabled this in the conditional above.
|
||||||
if not self.export_market_data(data):
|
if monitor.state['Modules']:
|
||||||
err = 'Error: Exporting Market data'
|
self.ship.configure(state=True)
|
||||||
play_bad = True
|
|
||||||
|
|
||||||
self.holdofftime = querytime + companion.holdoff
|
if monitor.state.get('SuitCurrent') is not None:
|
||||||
|
if (loadout := data.get('loadout')) is not None:
|
||||||
|
if (suit := loadout.get('suit')) is not None:
|
||||||
|
if (suitname := suit.get('edmcName')) is not None:
|
||||||
|
# We've been paranoid about loadout->suit->suitname, now just assume loadouts is there
|
||||||
|
loadout_name = index_possibly_sparse_list(
|
||||||
|
data['loadouts'], loadout['loadoutSlotId']
|
||||||
|
)['name']
|
||||||
|
|
||||||
|
self.suit['text'] = f'{suitname} ({loadout_name})'
|
||||||
|
|
||||||
|
self.suit_show_if_set()
|
||||||
|
|
||||||
|
if data['commander'].get('credits') is not None:
|
||||||
|
monitor.state['Credits'] = data['commander']['credits']
|
||||||
|
monitor.state['Loan'] = data['commander'].get('debt', 0)
|
||||||
|
|
||||||
|
# stuff we can do when not docked
|
||||||
|
err = plug.notify_newdata(data, monitor.is_beta)
|
||||||
|
self.status['text'] = err and err or ''
|
||||||
|
if err:
|
||||||
|
play_bad = True
|
||||||
|
|
||||||
|
# Export market data
|
||||||
|
if not self.export_market_data(data):
|
||||||
|
err = 'Error: Exporting Market data'
|
||||||
|
play_bad = True
|
||||||
|
|
||||||
|
self.holdofftime = querytime + companion.holdoff
|
||||||
|
|
||||||
# # Companion API problem
|
# # Companion API problem
|
||||||
# except companion.ServerLagging as e:
|
# except companion.ServerLagging as e:
|
||||||
|
102
companion.py
102
companion.py
@ -15,6 +15,7 @@ import numbers
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import threading
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@ -465,6 +466,13 @@ class Auth(object):
|
|||||||
return base64.urlsafe_b64encode(text).decode().replace('=', '')
|
return base64.urlsafe_b64encode(text).decode().replace('=', '')
|
||||||
|
|
||||||
|
|
||||||
|
class CAPIFailedRequest():
|
||||||
|
"""CAPI failed query error class."""
|
||||||
|
|
||||||
|
def __init__(self, message, exception=None):
|
||||||
|
self.message = message
|
||||||
|
self.exception = exception
|
||||||
|
|
||||||
class Session(object):
|
class Session(object):
|
||||||
"""Methods for handling Frontier Auth and CAPI queries."""
|
"""Methods for handling Frontier Auth and CAPI queries."""
|
||||||
|
|
||||||
@ -477,8 +485,10 @@ class Session(object):
|
|||||||
self.session: Optional[requests.Session] = None
|
self.session: Optional[requests.Session] = None
|
||||||
self.auth: Optional[Auth] = None
|
self.auth: Optional[Auth] = None
|
||||||
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
|
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
|
||||||
|
self.tk_master: Optional[tk.Tk] = None
|
||||||
|
|
||||||
logger.info('Starting CAPI queries thread...')
|
logger.info('Starting CAPI queries thread...')
|
||||||
|
self.capi_response_queue: Queue
|
||||||
self.capi_query_queue: Queue = Queue()
|
self.capi_query_queue: Queue = Queue()
|
||||||
self.capi_query_thread = threading.Thread(
|
self.capi_query_thread = threading.Thread(
|
||||||
target=self.capi_query_worker,
|
target=self.capi_query_worker,
|
||||||
@ -488,6 +498,12 @@ class Session(object):
|
|||||||
self.capi_query_thread.start()
|
self.capi_query_thread.start()
|
||||||
logger.info('Done')
|
logger.info('Done')
|
||||||
|
|
||||||
|
def set_capi_response_queue(self, capi_response_queue: Queue) -> None:
|
||||||
|
self.capi_response_queue = capi_response_queue
|
||||||
|
|
||||||
|
def set_tk_master(self, master: tk.Tk) -> None:
|
||||||
|
self.tk_master = master
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Frontier Authorization
|
# Frontier Authorization
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -591,18 +607,61 @@ class Session(object):
|
|||||||
######################################################################
|
######################################################################
|
||||||
# CAPI queries
|
# CAPI queries
|
||||||
######################################################################
|
######################################################################
|
||||||
def capi_query_worker(self, ):
|
def capi_query_worker(self):
|
||||||
"""Worker thread that performs actual CAPI queries."""
|
"""Worker thread that performs actual CAPI queries."""
|
||||||
logger.info('CAPI worker thread starting')
|
logger.info('CAPI worker thread starting')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
query: Optional[str] = self.capi_query_queue.get()
|
endpoint: Optional[str] = self.capi_query_queue.get()
|
||||||
if not query:
|
if not endpoint:
|
||||||
logger.info('Empty queue message, exiting...')
|
logger.info('Empty queue message, exiting...')
|
||||||
break
|
break
|
||||||
|
|
||||||
logger.trace_if('capi.worker', f'Processing query: {query}')
|
logger.trace_if('capi.worker', f'Processing query: {endpoint}')
|
||||||
time.sleep(1)
|
# XXX
|
||||||
|
self.capi_response_queue.put(
|
||||||
|
CAPIFailedRequest(f'Unable to connect to endpoint {endpoint}')
|
||||||
|
)
|
||||||
|
self.tk_master.event_generate('<<CAPIResponse>>')
|
||||||
|
continue
|
||||||
|
# XXX
|
||||||
|
try:
|
||||||
|
r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore
|
||||||
|
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
|
||||||
|
data = CAPIData(r.json(), endpoint) # May also fail here if token expired since response is empty
|
||||||
|
|
||||||
|
except requests.ConnectionError as e:
|
||||||
|
logger.warning(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})')
|
||||||
|
self.capi_response_queue.put(
|
||||||
|
CAPIFailedRequest(f'Unable to connect to endpoint {endpoint}', exception=e)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# raise ServerConnectionError(f'Unable to connect to endpoint {endpoint}') from e
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug('Attempting GET', exc_info=e)
|
||||||
|
# LANG: Frontier CAPI data retrieval failed
|
||||||
|
# raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e
|
||||||
|
self.capi_response_queue.put(
|
||||||
|
CAPIFailedRequest(f'Frontier CAPI query failure: {endpoint}', exception=e)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if r.url.startswith(SERVER_AUTH):
|
||||||
|
logger.info('Redirected back to Auth Server')
|
||||||
|
# Redirected back to Auth server - force full re-authentication
|
||||||
|
self.dump(r)
|
||||||
|
self.invalidate()
|
||||||
|
self.retrying = False
|
||||||
|
self.login()
|
||||||
|
raise CredentialsError()
|
||||||
|
|
||||||
|
elif 500 <= r.status_code < 600:
|
||||||
|
# Server error. Typically 500 "Internal Server Error" if server is down
|
||||||
|
logger.debug('500 status back from CAPI')
|
||||||
|
self.dump(r)
|
||||||
|
# LANG: Frontier CAPI data retrieval failed with 5XX code
|
||||||
|
raise ServerError(f'{_("Frontier CAPI server error")}: {r.status_code}')
|
||||||
|
|
||||||
logger.info('CAPI worker thread DONE')
|
logger.info('CAPI worker thread DONE')
|
||||||
|
|
||||||
@ -625,38 +684,9 @@ class Session(object):
|
|||||||
if conf_module.capi_pretend_down:
|
if conf_module.capi_pretend_down:
|
||||||
raise ServerConnectionError(f'Pretending CAPI down: {endpoint}')
|
raise ServerConnectionError(f'Pretending CAPI down: {endpoint}')
|
||||||
|
|
||||||
|
self.capi_query_queue.put(endpoint)
|
||||||
try:
|
try:
|
||||||
self.capi_query_queue.put(endpoint)
|
...
|
||||||
r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore
|
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
logger.warning(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})')
|
|
||||||
raise ServerConnectionError(f'Unable to connect to endpoint {endpoint}') from e
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug('Attempting GET', exc_info=e)
|
|
||||||
# LANG: Frontier CAPI data retrieval failed
|
|
||||||
raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e
|
|
||||||
|
|
||||||
if r.url.startswith(SERVER_AUTH):
|
|
||||||
logger.info('Redirected back to Auth Server')
|
|
||||||
# Redirected back to Auth server - force full re-authentication
|
|
||||||
self.dump(r)
|
|
||||||
self.invalidate()
|
|
||||||
self.retrying = False
|
|
||||||
self.login()
|
|
||||||
raise CredentialsError()
|
|
||||||
|
|
||||||
elif 500 <= r.status_code < 600:
|
|
||||||
# Server error. Typically 500 "Internal Server Error" if server is down
|
|
||||||
logger.debug('500 status back from CAPI')
|
|
||||||
self.dump(r)
|
|
||||||
# LANG: Frontier CAPI data retrieval failed with 5XX code
|
|
||||||
raise ServerError(f'{_("Frontier CAPI server error")}: {r.status_code}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
|
|
||||||
data = CAPIData(r.json(), endpoint) # May also fail here if token expired since response is empty
|
|
||||||
|
|
||||||
except (requests.HTTPError, ValueError) as e:
|
except (requests.HTTPError, ValueError) as e:
|
||||||
logger.exception('Frontier CAPI Auth: GET ')
|
logger.exception('Frontier CAPI Auth: GET ')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user