1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-05-30 15:19:40 +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 locale
import pathlib
import queue
import re
import sys
# import threading
@ -14,6 +15,7 @@ import webbrowser
from builtins import object, str
from os import chdir, environ
from os.path import dirname, join
from queue import Queue
from sys import platform
from time import localtime, strftime, time
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
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.title(applongname)
@ -933,126 +937,138 @@ class AppWindow(object):
def capi_handle_response(self, event=None):
"""Handle the resulting data from a CAPI query."""
data = ...
# Validation
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')
try:
data = self.capi_response_queue.get(block=False)
elif not data.get('commander', {}).get('name'):
# LANG: We didn't have the commander name when we should have
err = self.status['text'] = _("Who are you?!") # Shouldn't happen
except queue.Empty:
logger.error('There was no response in the queue!')
# TODO: Set status text
return
elif (not data.get('lastSystem', {}).get('name')
or (data['commander'].get('docked')
and not data.get('lastStarport', {}).get('name'))):
# LANG: We don't know where the commander is, when we should
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
else:
if isinstance(data, companion.CAPIFailedRequest):
logger.trace_if('capi.worker', f'Failed Request: {data.message}')
raise data.exception
elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
# LANG: We don't know what ship the commander is in, when we should
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
# Validation
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 monitor.cmdr and data['commander']['name'] != monitor.cmdr:
# Companion API Commander doesn't match Journal
raise companion.CmdrError()
elif not data.get('commander', {}).get('name'):
# LANG: We didn't have the commander name when we should have
err = self.status['text'] = _("Who are you?!") # Shouldn't happen
elif auto_update and not monitor.state['OnFoot'] and not data['commander'].get('docked'):
# auto update is only when just docked
logger.warning(f"{auto_update!r} and not {monitor.state['OnFoot']!r} and "
f"not {data['commander'].get('docked')!r}")
raise companion.ServerLagging()
elif (not data.get('lastSystem', {}).get('name')
or (data['commander'].get('docked')
and not data.get('lastStarport', {}).get('name'))):
# LANG: We don't know where the commander is, when we should
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
elif data['lastSystem']['name'] != monitor.system:
# CAPI system must match last journal one
logger.warning(f"{data['lastSystem']['name']!r} != {monitor.system!r}")
raise companion.ServerLagging()
elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
# LANG: We don't know what ship the commander is in, when we should
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
elif data['lastStarport']['name'] != monitor.station:
if monitor.state['OnFoot'] and monitor.station:
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
f"{monitor.state['OnFoot']!r} and {monitor.station!r}")
raise companion.ServerLagging()
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
# Companion API Commander doesn't match Journal
raise companion.CmdrError()
else:
last_station = None
if data['commander']['docked']:
last_station = data['lastStarport']['name']
elif auto_update and not monitor.state['OnFoot'] and not data['commander'].get('docked'):
# auto update is only when just docked
logger.warning(f"{auto_update!r} and not {monitor.state['OnFoot']!r} and "
f"not {data['commander'].get('docked')!r}")
raise companion.ServerLagging()
if monitor.station is None:
# Likely (re-)Embarked on ship docked at an EDO settlement.
# Both Disembark and Embark have `"Onstation": false` in Journal.
# 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()
elif data['lastSystem']['name'] != monitor.system:
# CAPI system must match last journal one
logger.warning(f"{data['lastSystem']['name']!r} != {monitor.system!r}")
raise companion.ServerLagging()
if last_station != monitor.station:
# 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()
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}")
elif data['lastStarport']['name'] != monitor.station:
if monitor.state['OnFoot'] and monitor.station:
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
f"{monitor.state['OnFoot']!r} and {monitor.station!r}")
raise companion.ServerLagging()
else:
if __debug__: # Recording
companion.session.dump_capi_data(data)
last_station = None
if data['commander']['docked']:
last_station = data['lastStarport']['name']
if not monitor.state['ShipType']: # Started game in SRV or fighter
self.ship['text'] = ship_name_map.get(data['ship']['name'].lower(), data['ship']['name'])
monitor.state['ShipID'] = data['ship']['id']
monitor.state['ShipType'] = data['ship']['name'].lower()
if monitor.station is None:
# Likely (re-)Embarked on ship docked at an EDO settlement.
# Both Disembark and Embark have `"Onstation": false` in Journal.
# 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']:
self.ship.configure(state=tk.DISABLED)
if last_station != monitor.station:
# 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.
if monitor.state['Modules']:
self.ship.configure(state=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']
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()
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:
monitor.state['Credits'] = data['commander']['credits']
monitor.state['Loan'] = data['commander'].get('debt', 0)
if not monitor.state['ShipType']: # Started game in SRV or fighter
self.ship['text'] = ship_name_map.get(data['ship']['name'].lower(), data['ship']['name'])
monitor.state['ShipID'] = data['ship']['id']
monitor.state['ShipType'] = data['ship']['name'].lower()
# 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
if not monitor.state['Modules']:
self.ship.configure(state=tk.DISABLED)
# Export market data
if not self.export_market_data(data):
err = 'Error: Exporting Market data'
play_bad = True
# We might have disabled this in the conditional above.
if monitor.state['Modules']:
self.ship.configure(state=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
# except companion.ServerLagging as e:

View File

@ -15,6 +15,7 @@ import numbers
import os
import random
import threading
import tkinter as tk
import time
import urllib.parse
import webbrowser
@ -465,6 +466,13 @@ class Auth(object):
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):
"""Methods for handling Frontier Auth and CAPI queries."""
@ -477,8 +485,10 @@ class Session(object):
self.session: Optional[requests.Session] = None
self.auth: Optional[Auth] = None
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
self.tk_master: Optional[tk.Tk] = None
logger.info('Starting CAPI queries thread...')
self.capi_response_queue: Queue
self.capi_query_queue: Queue = Queue()
self.capi_query_thread = threading.Thread(
target=self.capi_query_worker,
@ -488,6 +498,12 @@ class Session(object):
self.capi_query_thread.start()
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
######################################################################
@ -591,18 +607,61 @@ class Session(object):
######################################################################
# CAPI queries
######################################################################
def capi_query_worker(self, ):
def capi_query_worker(self):
"""Worker thread that performs actual CAPI queries."""
logger.info('CAPI worker thread starting')
while True:
query: Optional[str] = self.capi_query_queue.get()
if not query:
endpoint: Optional[str] = self.capi_query_queue.get()
if not endpoint:
logger.info('Empty queue message, exiting...')
break
logger.trace_if('capi.worker', f'Processing query: {query}')
time.sleep(1)
logger.trace_if('capi.worker', f'Processing query: {endpoint}')
# 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')
@ -625,38 +684,9 @@ class Session(object):
if conf_module.capi_pretend_down:
raise ServerConnectionError(f'Pretending CAPI down: {endpoint}')
self.capi_query_queue.put(endpoint)
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:
logger.exception('Frontier CAPI Auth: GET ')