1
0
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:
Athanasius 2021-08-16 15:37:55 +01:00
parent 2fc1568bf7
commit 655c7ea1ca
No known key found for this signature in database
GPG Key ID: AE3E527847057C7D
2 changed files with 179 additions and 133 deletions

View File

@ -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:

View File

@ -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 ')