mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-14 08:17:13 +03:00
Merge pull request #1759 from EDCD/enhancement/1714/capi-killswitches
companion: A start on adding CAPI killswitches
This commit is contained in:
commit
cfbce496c5
@ -5,5 +5,5 @@ scripts_are_modules = True
|
||||
; Without this bare `mypy <file>` may get warnings for e.g.
|
||||
; `<var> = <value>`
|
||||
; i.e. no typing info.
|
||||
check-untyped-defs = True
|
||||
check_untyped_defs = True
|
||||
; platform = darwin
|
||||
|
@ -46,7 +46,7 @@ repos:
|
||||
# mypy - static type checking
|
||||
# mypy --follow-imports skip <file>
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.931'
|
||||
rev: 'v0.991'
|
||||
hooks:
|
||||
- id: mypy
|
||||
# verbose: true
|
||||
|
@ -168,6 +168,11 @@ if __name__ == '__main__': # noqa: C901
|
||||
help='Have EDDN plugin show what it is tracking',
|
||||
action='store_true',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--killswitches-file',
|
||||
help='Specify a custom killswitches file',
|
||||
)
|
||||
###########################################################################
|
||||
|
||||
args = parser.parse_args()
|
||||
@ -907,6 +912,12 @@ class AppWindow(object):
|
||||
|
||||
def login(self):
|
||||
"""Initiate CAPI/Frontier login and set other necessary state."""
|
||||
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
|
||||
if should_return:
|
||||
logger.warning('capi.auth has been disabled via killswitch. Returning.')
|
||||
self.status['text'] = 'CAPI auth disabled by killswitch'
|
||||
return
|
||||
|
||||
if not self.status['text']:
|
||||
# LANG: Status - Attempting to get a Frontier Auth Access Token
|
||||
self.status['text'] = _('Logging in...')
|
||||
@ -992,6 +1003,13 @@ class AppWindow(object):
|
||||
:param event: Tk generated event details.
|
||||
"""
|
||||
logger.trace_if('capi.worker', 'Begin')
|
||||
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
|
||||
if should_return:
|
||||
logger.warning('capi.auth has been disabled via killswitch. Returning.')
|
||||
self.status['text'] = 'CAPI auth disabled by killswitch'
|
||||
hotkeymgr.play_bad()
|
||||
return
|
||||
|
||||
auto_update = not event
|
||||
play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.get_int('hotkey_mute')
|
||||
|
||||
@ -1210,10 +1228,15 @@ class AppWindow(object):
|
||||
if err:
|
||||
play_bad = True
|
||||
|
||||
# Export market data
|
||||
if not self.export_market_data(capi_response.capi_data):
|
||||
err = 'Error: Exporting Market data'
|
||||
play_bad = True
|
||||
should_return, new_data = killswitch.check_killswitch('capi.request./market', {})
|
||||
if should_return:
|
||||
logger.warning("capi.request./market has been disabled by killswitch. Returning.")
|
||||
|
||||
else:
|
||||
# Export market data
|
||||
if not self.export_market_data(capi_response.capi_data):
|
||||
err = 'Error: Exporting Market data'
|
||||
play_bad = True
|
||||
|
||||
self.capi_query_holdoff_time = capi_response.query_time + companion.capi_query_cooldown
|
||||
|
||||
@ -1460,7 +1483,9 @@ class AppWindow(object):
|
||||
auto_update = True
|
||||
|
||||
if auto_update:
|
||||
self.w.after(int(SERVER_RETRY * 1000), self.capi_request_data)
|
||||
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
|
||||
if not should_return:
|
||||
self.w.after(int(SERVER_RETRY * 1000), self.capi_request_data)
|
||||
|
||||
if entry['event'] == 'ShutDown':
|
||||
# Enable WinSparkle automatic update checks
|
||||
@ -1859,10 +1884,13 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
|
||||
)
|
||||
|
||||
|
||||
def setup_killswitches():
|
||||
def setup_killswitches(filename: Optional[str]):
|
||||
"""Download and setup the main killswitch list."""
|
||||
logger.debug('fetching killswitches...')
|
||||
killswitch.setup_main_list()
|
||||
if filename is not None:
|
||||
filename = "file:" + filename
|
||||
|
||||
killswitch.setup_main_list(filename)
|
||||
|
||||
|
||||
def show_killswitch_poppup(root=None):
|
||||
@ -1891,9 +1919,9 @@ def show_killswitch_poppup(root=None):
|
||||
for version in kills:
|
||||
tk.Label(frame, text=f'Version: {version.version}').grid(row=idx, sticky=tk.W)
|
||||
idx += 1
|
||||
for id, reason in version.kills.items():
|
||||
for id, kill in version.kills.items():
|
||||
tk.Label(frame, text=id).grid(column=0, row=idx, sticky=tk.W, padx=(10, 0))
|
||||
tk.Label(frame, text=reason).grid(column=1, row=idx, sticky=tk.E, padx=(0, 10))
|
||||
tk.Label(frame, text=kill.reason).grid(column=1, row=idx, sticky=tk.E, padx=(0, 10))
|
||||
idx += 1
|
||||
idx += 1
|
||||
|
||||
@ -2026,7 +2054,8 @@ sys.path: {sys.path}'''
|
||||
|
||||
Translations.install(config.get_str('language')) # Can generate errors so wait til log set up
|
||||
|
||||
setup_killswitches()
|
||||
setup_killswitches(args.killswitches_file)
|
||||
|
||||
root = tk.Tk(className=appname.lower())
|
||||
if sys.platform != 'win32' and ((f := config.get_str('font')) is not None or f != ''):
|
||||
size = config.get_int('font_size', default=-1)
|
||||
|
17
PLUGINS.md
17
PLUGINS.md
@ -910,10 +910,9 @@ constants.
|
||||
|
||||
### Commander Data from Frontier CAPI
|
||||
If a plugin has a `cmdr_data()` function it gets called when the application
|
||||
has just fetched fresh Cmdr and station data from Frontier's servers, **but not
|
||||
for the Legacy galaxy**. See `cmdr_data_legacy()` below for Legacy data
|
||||
handling.
|
||||
|
||||
has just fetched fresh Cmdr and station data from Frontier's CAPI servers,
|
||||
**but not for the Legacy galaxy**. See `cmdr_data_legacy()` below for Legacy
|
||||
data handling.
|
||||
```python
|
||||
from companion import CAPIData, SERVER_LIVE, SERVER_LEGACY, SERVER_BETA
|
||||
|
||||
@ -948,6 +947,16 @@ have extra properties, such as `source_host`, as shown above. Plugin authors
|
||||
are free to use *that* property, **but MUST NOT rely on any other extra
|
||||
properties present in `CAPIData`, they are for internal use only.**
|
||||
|
||||
The contents of `data` will always have at least the data returned by a CAPI
|
||||
`/profile` query. If the player is docked at a station, and the relevant
|
||||
services are available then the `lastStarport` key's value will have been
|
||||
augmented with `/market` and/or `/shipyard` data. **But do not assume this
|
||||
will always be the case**.
|
||||
|
||||
If there is a killswitch in effect for some of the CAPI endpoints, then the
|
||||
data passed to this function might not be as complete as you expect. Code
|
||||
defensively.
|
||||
|
||||
|
||||
#### CAPI data for Legacy
|
||||
When CAPI data has been retrieved from the separate CAPI host for the Legacy
|
||||
|
50
companion.py
50
companion.py
@ -28,6 +28,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, OrderedDic
|
||||
import requests
|
||||
|
||||
import config as conf_module
|
||||
import killswitch
|
||||
import protocol
|
||||
from config import config, user_agent
|
||||
from edmc_data import companion_category_map as category_map
|
||||
@ -325,6 +326,11 @@ class Auth(object):
|
||||
"""
|
||||
logger.debug(f'Trying for "{self.cmdr}"')
|
||||
|
||||
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
|
||||
if should_return:
|
||||
logger.warning('capi.auth has been disabled via killswitch. Returning.')
|
||||
return None
|
||||
|
||||
self.verifier = None
|
||||
cmdrs = config.get_list('cmdrs', default=[])
|
||||
logger.debug(f'Cmdrs: {cmdrs}')
|
||||
@ -393,9 +399,11 @@ class Auth(object):
|
||||
return None
|
||||
|
||||
def authorize(self, payload: str) -> str: # noqa: CCR001
|
||||
"""Handle oAuth authorization callback.
|
||||
"""
|
||||
Handle oAuth authorization callback.
|
||||
|
||||
:return: access token if successful, otherwise raises CredentialsError.
|
||||
:return: access token if successful
|
||||
:raises CredentialsError
|
||||
"""
|
||||
logger.debug('Checking oAuth authorization callback')
|
||||
if '?' not in payload:
|
||||
@ -593,7 +601,7 @@ class EDMCCAPIFailedRequest(EDMCCAPIReturn):
|
||||
):
|
||||
super().__init__(query_time=query_time, play_sound=play_sound, auto_update=auto_update)
|
||||
self.message: str = message # User-friendly reason for failure.
|
||||
self.exception: int = exception # Exception that recipient should raise.
|
||||
self.exception: Exception = exception # Exception that recipient should raise.
|
||||
|
||||
|
||||
class Session(object):
|
||||
@ -611,7 +619,7 @@ class Session(object):
|
||||
def __init__(self) -> None:
|
||||
self.state = Session.STATE_INIT
|
||||
self.credentials: Optional[Dict[str, Any]] = None
|
||||
self.requests_session: Optional[requests.Session] = None
|
||||
self.requests_session: Optional[requests.Session] = requests.Session()
|
||||
self.auth: Optional[Auth] = None
|
||||
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
|
||||
self.tk_master: Optional[tk.Tk] = None
|
||||
@ -644,9 +652,10 @@ class Session(object):
|
||||
def start_frontier_auth(self, access_token: str) -> None:
|
||||
"""Start an oAuth2 session."""
|
||||
logger.debug('Starting session')
|
||||
self.requests_session = requests.Session()
|
||||
self.requests_session.headers['Authorization'] = f'Bearer {access_token}'
|
||||
self.requests_session.headers['User-Agent'] = user_agent
|
||||
if self.requests_session is not None:
|
||||
self.requests_session.headers['Authorization'] = f'Bearer {access_token}'
|
||||
self.requests_session.headers['User-Agent'] = user_agent
|
||||
|
||||
self.state = Session.STATE_OK
|
||||
|
||||
def login(self, cmdr: Optional[str] = None, is_beta: Optional[bool] = None) -> bool:
|
||||
@ -655,6 +664,11 @@ class Session(object):
|
||||
|
||||
:return: True if login succeeded, False if re-authorization initiated.
|
||||
"""
|
||||
should_return, new_data = killswitch.check_killswitch('capi.auth', {})
|
||||
if should_return:
|
||||
logger.warning('capi.auth has been disabled via killswitch. Returning.')
|
||||
return False
|
||||
|
||||
if not Auth.CLIENT_ID:
|
||||
logger.error('self.CLIENT_ID is None')
|
||||
raise CredentialsError('cannot login without a valid Client ID')
|
||||
@ -759,7 +773,12 @@ class Session(object):
|
||||
:param timeout: requests query timeout to use.
|
||||
:return: The resulting CAPI data, of type CAPIData.
|
||||
"""
|
||||
capi_data: CAPIData
|
||||
capi_data: CAPIData = CAPIData()
|
||||
should_return, new_data = killswitch.check_killswitch('capi.request.' + capi_endpoint, {})
|
||||
if should_return:
|
||||
logger.warning(f"capi.request.{capi_endpoint} has been disabled by killswitch. Returning.")
|
||||
return capi_data
|
||||
|
||||
try:
|
||||
logger.trace_if('capi.worker', 'Sending HTTP request...')
|
||||
if conf_module.capi_pretend_down:
|
||||
@ -841,6 +860,10 @@ class Session(object):
|
||||
"""
|
||||
station_data = capi_single_query(capi_host, self.FRONTIER_CAPI_PATH_PROFILE, timeout=timeout)
|
||||
|
||||
if not station_data.get('commander'):
|
||||
# If even this doesn't exist, probably killswitched.
|
||||
return station_data
|
||||
|
||||
if not station_data['commander'].get('docked') and not monitor.state['OnFoot']:
|
||||
return station_data
|
||||
|
||||
@ -881,6 +904,10 @@ class Session(object):
|
||||
|
||||
if services.get('commodities'):
|
||||
market_data = capi_single_query(capi_host, self.FRONTIER_CAPI_PATH_MARKET, timeout=timeout)
|
||||
if not market_data.get('id'):
|
||||
# Probably killswitched
|
||||
return station_data
|
||||
|
||||
if last_starport_id != int(market_data['id']):
|
||||
logger.warning(f"{last_starport_id!r} != {int(market_data['id'])!r}")
|
||||
raise ServerLagging()
|
||||
@ -891,6 +918,10 @@ class Session(object):
|
||||
|
||||
if services.get('outfitting') or services.get('shipyard'):
|
||||
shipyard_data = capi_single_query(capi_host, self.FRONTIER_CAPI_PATH_SHIPYARD, timeout=timeout)
|
||||
if not shipyard_data.get('id'):
|
||||
# Probably killswitched
|
||||
return station_data
|
||||
|
||||
if last_starport_id != int(shipyard_data['id']):
|
||||
logger.warning(f"{last_starport_id!r} != {int(shipyard_data['id'])!r}")
|
||||
raise ServerLagging()
|
||||
@ -946,7 +977,8 @@ class Session(object):
|
||||
# event too, so assume it will be polling the response queue.
|
||||
if query.tk_response_event is not None:
|
||||
logger.trace_if('capi.worker', 'Sending <<CAPIResponse>>')
|
||||
self.tk_master.event_generate('<<CAPIResponse>>')
|
||||
if self.tk_master is not None:
|
||||
self.tk_master.event_generate('<<CAPIResponse>>')
|
||||
|
||||
logger.info('CAPI worker thread DONE')
|
||||
|
||||
|
@ -119,10 +119,17 @@ JSON primitives and their python equivalents
|
||||
json compound types (`object -- {}` and `array -- []`) may be set.
|
||||
|
||||
### Testing
|
||||
You can supply a custom killswitches file for testing against
|
||||
EDMarketConnector:
|
||||
```bash
|
||||
python EDMarketConnector.py --killswitches-file <filename>
|
||||
```
|
||||
This will be relative to the CWD of the process.
|
||||
|
||||
Killswitch files can be tested using the script in `scripts/killswitch_test.py`.
|
||||
Providing a file as an argument or `-` for stdin will output the behaviour of
|
||||
the provided file, including indicating typos, if applicable.
|
||||
Alternatively, killswitch files can be independently tested using the script in
|
||||
`scripts/killswitch_test.py`. Providing a file as an argument or `-` for stdin
|
||||
will output the behaviour of the provided file, including indicating typos, if
|
||||
applicable.
|
||||
|
||||
### Versions
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Fetch kill switches from EDMC Repo."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from copy import deepcopy
|
||||
from typing import (
|
||||
@ -339,6 +340,16 @@ def fetch_kill_switches(target=DEFAULT_KILLSWITCH_URL) -> Optional[KillSwitchJSO
|
||||
:return: a list of dicts containing kill switch data, or None
|
||||
"""
|
||||
logger.info("Attempting to fetch kill switches")
|
||||
if target.startswith('file:'):
|
||||
target = target.replace('file:', '')
|
||||
try:
|
||||
with open(target, 'r') as t:
|
||||
return json.load(t)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"No such file '{target}'")
|
||||
return None
|
||||
|
||||
try:
|
||||
data = requests.get(target, timeout=10).json()
|
||||
|
||||
@ -458,13 +469,16 @@ def get_kill_switches_thread(
|
||||
active: KillSwitchSet = KillSwitchSet([])
|
||||
|
||||
|
||||
def setup_main_list():
|
||||
def setup_main_list(filename: Optional[str]):
|
||||
"""
|
||||
Set up the global set of kill switches for querying.
|
||||
|
||||
Plugins should NOT call this EVER.
|
||||
"""
|
||||
if (data := get_kill_switches(DEFAULT_KILLSWITCH_URL, OLD_KILLSWITCH_URL)) is None:
|
||||
if filename is None:
|
||||
filename = DEFAULT_KILLSWITCH_URL
|
||||
|
||||
if (data := get_kill_switches(filename, OLD_KILLSWITCH_URL)) is None:
|
||||
logger.warning("Unable to fetch kill switches. Setting global set to an empty set")
|
||||
return
|
||||
|
||||
|
@ -636,6 +636,16 @@ class EDDN:
|
||||
:param data: a dict containing the starport data
|
||||
:param is_beta: whether or not we're currently in beta mode
|
||||
"""
|
||||
should_return, new_data = killswitch.check_killswitch('capi.request./market', {})
|
||||
if should_return:
|
||||
logger.warning("capi.request./market has been disabled by killswitch. Returning.")
|
||||
return
|
||||
|
||||
should_return, new_data = killswitch.check_killswitch('eddn.capi_export.commodities', {})
|
||||
if should_return:
|
||||
logger.warning("eddn.capi_export.commodities has been disabled by killswitch. Returning.")
|
||||
return
|
||||
|
||||
modules, ships = self.safe_modules_and_ships(data)
|
||||
horizons: bool = capi_is_horizons(
|
||||
data['lastStarport'].get('economies', {}),
|
||||
@ -757,6 +767,16 @@ class EDDN:
|
||||
:param data: dict containing the outfitting data
|
||||
:param is_beta: whether or not we're currently in beta mode
|
||||
"""
|
||||
should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {})
|
||||
if should_return:
|
||||
logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.")
|
||||
return
|
||||
|
||||
should_return, new_data = killswitch.check_killswitch('eddn.capi_export.outfitting', {})
|
||||
if should_return:
|
||||
logger.warning("eddn.capi_export.outfitting has been disabled by killswitch. Returning.")
|
||||
return
|
||||
|
||||
modules, ships = self.safe_modules_and_ships(data)
|
||||
|
||||
# Horizons flag - will hit at least Int_PlanetApproachSuite other than at engineer bases ("Colony"),
|
||||
@ -813,6 +833,16 @@ class EDDN:
|
||||
:param data: dict containing the shipyard data
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
"""
|
||||
should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {})
|
||||
if should_return:
|
||||
logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.")
|
||||
return
|
||||
|
||||
should_return, new_data = killswitch.check_killswitch('eddn.capi_export.shipyard', {})
|
||||
if should_return:
|
||||
logger.warning("eddn.capi_export.shipyard has been disabled by killswitch. Returning.")
|
||||
return
|
||||
|
||||
modules, ships = self.safe_modules_and_ships(data)
|
||||
|
||||
horizons: bool = capi_is_horizons(
|
||||
@ -1857,7 +1887,7 @@ class EDDN:
|
||||
match = self.CANONICALISE_RE.match(item)
|
||||
return match and match.group(1) or item
|
||||
|
||||
def capi_gameversion_from_host_endpoint(self, capi_host: str, capi_endpoint: str) -> str:
|
||||
def capi_gameversion_from_host_endpoint(self, capi_host: Optional[str], capi_endpoint: str) -> str:
|
||||
"""
|
||||
Return the correct CAPI gameversion string for the given host/endpoint.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user