From 2583d07baf4ed362f6f8062136bd7fa72fc2d798 Mon Sep 17 00:00:00 2001 From: A_D Date: Sat, 3 Jul 2021 15:17:31 +0200 Subject: [PATCH] Added additional data fields to killswitches These fields will be used for various things moving forward. The currently intended use is to redact fields when sending data to inara/edsm/eddn in case Frontier accidentally includes something that is unwanted --- docs/Killswitches.md | 40 ++++++++----- killswitch.py | 139 +++++++++++++++++++++++++++++++++---------- 2 files changed, 133 insertions(+), 46 deletions(-) diff --git a/docs/Killswitches.md b/docs/Killswitches.md index 9e08feb5..a2c99c31 100644 --- a/docs/Killswitches.md +++ b/docs/Killswitches.md @@ -12,23 +12,33 @@ Killswitches are stored in a JSON file that is queried by EDMC on startup. The f | `last_updated` | `string` | When last the kill switches were updated (for human use only) | | `kill_switches` | `array` | The kill switches this file contains (expanded below) | -The `kill_switches` array contains kill switch objects. Each contains two fields: +The `kill_switches` array contains kill switch objects. Each contains the following fields: + +| Key | Type | Description | +| --------: | :-------------------------: | :--------------------------------------------------------------------------- | +| `version` | `version spec` | The version of EDMC these kill switches apply to (Must be valid semver spec) | +| `kills` | `Dict[str, Dict[str, Any]]` | The different keys to disable, and the reason for the disable | + +Each entry in `kills` contains a `reason` and `additional_data` field. `reason` is self explanatory, however +additional_data can contain various things. They are outlaid below: +| Key | Type | Description | +| -------------: | :---------: | :-------------------------------------------------------------------- | +| `redact_field` | `List[str]` | A list of fields in the matching event to be redacted, if they exist. | -| Key | Type | Description | -| --------: | :--------------: | :--------------------------------------------------------------------------- | -| `version` | `version spec` | The version of EDMC these kill switches apply to (Must be valid semver spec) | -| `kills` | `Dict[str, str]` | The different keys to disable, and the reason for the disable | An example follows: ```json { - "version": 1, - "last_updated": "19 October 2020", + "version": 2, + "last_updated": "3 July 2021", "kill_switches": [ { "version": "1.0.0", "kills": { - "plugins.eddn.send": "some reason" + "plugins.eddn.send": { + "reason": "some reason", + "additional_data": {} + } } } ] @@ -45,15 +55,17 @@ Plugins may use the killswitch system simply by hosting their own version of the using `killswitch.get_kill_switches(target='https://example.com/myplugin_killswitches.json')`. The returned object can be used to query the kill switch set, see the docstrings for more information on specifying versions. +The version of the JSON file will be automatically upgraded if possible by the code KillSwitch code. + ## Currently supported killswitch strings The current recognised (to EDMC and its internal plugins) killswitch strings are as follows: -| Kill Switch | Description | -| :--------------------------------------------------- | :------------------------------------------------------------------------------------------- | -| `plugins.eddn.send` | Disables all use of the send method on EDDN (effectively disables EDDN updates) | -| `plugins.(eddn\|inara\|edsm\|eddb).journal` | Disables all journal processing for EDDN/EDSM/INARA | -| `plugins.(edsm\|inara).worker` | Disables the EDSM/INARA worker thread (effectively disables updates) (does not close thread) | -| `plugins.(eddn\|inara\|edsm).journal.event.$eventname`| Specific events to disable processing for | +| Kill Switch | Description | +| :----------------------------------------------------- | :------------------------------------------------------------------------------------------- | +| `plugins.eddn.send` | Disables all use of the send method on EDDN (effectively disables EDDN updates) | +| `plugins.(eddn\|inara\|edsm\|eddb).journal` | Disables all journal processing for EDDN/EDSM/INARA | +| `plugins.(edsm\|inara).worker` | Disables the EDSM/INARA worker thread (effectively disables updates) (does not close thread) | +| `plugins.(eddn\|inara\|edsm).journal.event.$eventname` | Specific events to disable processing for | ## File location diff --git a/killswitch.py b/killswitch.py index aca020aa..9d01d569 100644 --- a/killswitch.py +++ b/killswitch.py @@ -1,8 +1,12 @@ """Fetch kill switches from EDMC Repo.""" -from typing import Dict, List, NamedTuple, Optional, Union, cast +from __future__ import annotations +from os import kill +from typing import Any, Dict, List, NamedTuple, Optional, TypedDict, Union, cast import requests import semantic_version +from copy import deepcopy +from semantic_version.base import Version import config import EDMCLogging @@ -10,31 +14,57 @@ import EDMCLogging logger = EDMCLogging.get_main_logger() DEFAULT_KILLSWITCH_URL = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/killswitches.json' +CURRENT_KILLSWITCH_VERSION = 2 _current_version: semantic_version.Version = config.appversion_nobuild() -class KillSwitch(NamedTuple): +class SingleKill(NamedTuple): + """A single KillSwitch. Possibly with additional data.""" + + match: str + reason: str + additional_data: Dict[str, Any] + + +class KillSwitches(NamedTuple): """One version's set of kill switches.""" version: semantic_version.SimpleSpec - kills: Dict[str, str] + kills: Dict[str, SingleKill] + + @staticmethod + def from_dict(data: KillSwitchSetJSON) -> KillSwitches: + """Create a KillSwitches instance from a dictionary.""" + ks = {} + + for match, ks_data in data['kills'].items(): + ks[match] = SingleKill( + match=match, reason=ks_data['reason'], additional_data=ks_data.get('additional_data', {}) + ) + + return KillSwitches(version=semantic_version.SimpleSpec(data['version']), kills=ks) class DisabledResult(NamedTuple): """DisabledResult is the result returned from various is_disabled calls.""" disabled: bool - reason: str + kill: Optional[SingleKill] + + @ property + def reason(self) -> str: + """Reason provided for why this killswitch exists.""" + return self.kill.reason if self.kill is not None else "" class KillSwitchSet: """Queryable set of kill switches.""" - def __init__(self, kill_switches: List[KillSwitch]) -> None: + def __init__(self, kill_switches: List[KillSwitches]) -> None: self.kill_switches = kill_switches - def get_disabled(self, id: str, *, version=_current_version) -> DisabledResult: + def get_disabled(self, id: str, *, version: Union[Version, str] = _current_version) -> DisabledResult: """ Return whether or not the given feature ID is disabled by a killswitch for the given version. @@ -46,9 +76,9 @@ class KillSwitchSet: if version not in ks.version: continue - return DisabledResult(id in ks.kills, ks.kills.get(id, "")) + return DisabledResult(id in ks.kills, ks.kills.get(id, None)) - return DisabledResult(False, "") + return DisabledResult(False, None) def is_disabled(self, id: str, *, version: semantic_version.Version = _current_version) -> bool: """Return whether or not a given feature ID is disabled for the given version.""" @@ -58,7 +88,7 @@ class KillSwitchSet: """Return a reason for why the given id is disabled for the given version, if any.""" return self.get_disabled(id, version=version).reason - def kills_for_version(self, version: semantic_version.Version = _current_version) -> List[KillSwitch]: + def kills_for_version(self, version: semantic_version.Version = _current_version) -> List[KillSwitches]: """ Get all killswitch entries that apply to the given version. @@ -76,17 +106,23 @@ class KillSwitchSet: return f'KillSwitchSet(kill_switches={self.kill_switches!r})' -KILL_SWITCH_JSON = List[Dict[str, Union[str, List[str]]]] -KILL_SWITCH_JSON_DICT = Dict[ - str, Union[ - str, # Last updated - int, # Version - KILL_SWITCH_JSON # kills - ] -] +class SingleKillSwitchJSON(TypedDict): # noqa: D101 + reason: str + additional_data: Dict[str, Any] -def fetch_kill_switches(target=DEFAULT_KILLSWITCH_URL) -> Optional[KILL_SWITCH_JSON_DICT]: +class KillSwitchSetJSON(TypedDict): # noqa: D101 + version: str + kills: Dict[str, SingleKillSwitchJSON] + + +class KillSwitchJSONFile(TypedDict): # noqa: D101 + version: int + last_updated: str + kill_switches: List[KillSwitchSetJSON] + + +def fetch_kill_switches(target=DEFAULT_KILLSWITCH_URL) -> Optional[KillSwitchJSONFile]: """ Fetch the JSON representation of our kill switches. @@ -108,34 +144,70 @@ def fetch_kill_switches(target=DEFAULT_KILLSWITCH_URL) -> Optional[KILL_SWITCH_J return data -def parse_kill_switches(data: KILL_SWITCH_JSON_DICT) -> List[KillSwitch]: +class _KillSwitchV1(TypedDict): + version: str + kills: Dict[str, str] + + +class _KillSwitchJSONFileV1(TypedDict): + version: int + last_updated: str + kill_switches: List[_KillSwitchV1] + + +def _upgrade_kill_switch_dict(data: KillSwitchJSONFile) -> KillSwitchJSONFile: + version = data['version'] + if version == CURRENT_KILLSWITCH_VERSION: + return data + + if version == 1: + logger.info('Got an old version killswitch file (v1) upgrading!') + to_return: KillSwitchJSONFile = deepcopy(data) + data_v1 = cast(_KillSwitchJSONFileV1, data) + # reveal_type(to_return['kill_switches']) + + to_return['kill_switches'] = [ + cast(KillSwitchSetJSON, { # I need to cheat here a touch. It is this I promise + 'version': d['version'], + 'kills': { + match: {'reason': reason, 'additional_data': {}} for match, reason in d['kills'].items() + } + }) + for d in data_v1['kill_switches'] + ] + + to_return['version'] = CURRENT_KILLSWITCH_VERSION + + return to_return + + return data + + +def parse_kill_switches(data: KillSwitchJSONFile) -> List[KillSwitches]: """ Parse kill switch dict to List of KillSwitches. :param data: dict containing raw killswitch data :return: a list of all provided killswitches """ + data = _upgrade_kill_switch_dict(data) last_updated = data['last_updated'] ks_version = data['version'] logger.info(f'Kill switches last updated {last_updated}') - if ks_version != 1: - logger.warning(f'Unknown killswitch version {ks_version}. Bailing out') + if ks_version != CURRENT_KILLSWITCH_VERSION: + logger.warning(f'Unknown killswitch version {ks_version} (expected {CURRENT_KILLSWITCH_VERSION}). Bailing out') return [] - kill_switches = cast(KILL_SWITCH_JSON, data['kill_switches']) - out: List[KillSwitch] = [] - + kill_switches = data['kill_switches'] + out = [] for idx, ks_data in enumerate(kill_switches): try: - ver = semantic_version.SimpleSpec(ks_data['version']) + ks = KillSwitches.from_dict(ks_data) + out.append(ks) - except ValueError as e: - logger.warning(f'could not parse killswitch idx {idx}: {e}') - continue - - ks = KillSwitch(version=ver, kills=cast(Dict[str, str], ks_data['kills'])) - out.append(ks) + except Exception as e: + logger.exception(f'Could not parse killswitch idx {idx}: {e}') return out @@ -169,6 +241,9 @@ def setup_main_list(): global active active = KillSwitchSet(parse_kill_switches(data)) + logger.trace('Active Killswitches:') + for v in active.kill_switches: + logger.trace(v) def get_disabled(id: str, *, version: semantic_version.Version = _current_version) -> DisabledResult: @@ -190,6 +265,6 @@ def get_reason(id: str, *, version: semantic_version.Version = _current_version) return active.get_reason(id, version=version) -def kills_for_version(version: semantic_version.Version = _current_version) -> List[KillSwitch]: +def kills_for_version(version: semantic_version.Version = _current_version) -> List[KillSwitches]: """Query the global KillSwitchSet for kills matching a particular version.""" return active.kills_for_version(version)