mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-12 15:27:14 +03:00
505 lines
17 KiB
Python
505 lines
17 KiB
Python
"""
|
|
__init__.py - Code dealing with the configuration of the program.
|
|
|
|
Copyright (c) EDCD, All Rights Reserved
|
|
Licensed under the GNU General Public License.
|
|
See LICENSE file.
|
|
|
|
Windows uses the Registry to store values in a flat manner.
|
|
Linux uses a file, but for commonality it's still a flat data structure.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
__all__ = [
|
|
# defined in the order they appear in the file
|
|
'GITVERSION_FILE',
|
|
'appname',
|
|
'applongname',
|
|
'appcmdname',
|
|
'copyright',
|
|
'update_interval',
|
|
'debug_senders',
|
|
'trace_on',
|
|
'capi_pretend_down',
|
|
'capi_debug_access_token',
|
|
'logger',
|
|
'git_shorthash_from_head',
|
|
'appversion',
|
|
'user_agent',
|
|
'appversion_nobuild',
|
|
'AbstractConfig',
|
|
'config',
|
|
'get_update_feed',
|
|
]
|
|
|
|
import abc
|
|
import contextlib
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import warnings
|
|
from abc import abstractmethod
|
|
from typing import Any, Callable, Type, TypeVar
|
|
import semantic_version
|
|
from constants import GITVERSION_FILE, applongname, appname
|
|
|
|
# Any of these may be imported by plugins
|
|
appcmdname = 'EDMC'
|
|
# appversion **MUST** follow Semantic Versioning rules:
|
|
# <https://semver.org/#semantic-versioning-specification-semver>
|
|
# Major.Minor.Patch(-prerelease)(+buildmetadata)
|
|
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
|
|
_static_appversion = '5.12.4'
|
|
_cached_version: semantic_version.Version | None = None
|
|
copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD'
|
|
|
|
|
|
update_interval = 8*60*60 # 8 Hours
|
|
# Providers marked to be in debug mode. Generally this is expected to switch to sending data to a log file
|
|
debug_senders: list[str] = []
|
|
# TRACE logging code that should actually be used. Means not spamming it
|
|
# *all* if only interested in some things.
|
|
trace_on: list[str] = []
|
|
|
|
capi_pretend_down: bool = False
|
|
capi_debug_access_token: str | None = None
|
|
# This must be done here in order to avoid an import cycle with EDMCLogging.
|
|
# Other code should use EDMCLogging.get_main_logger
|
|
if os.getenv("EDMC_NO_UI"):
|
|
logger = logging.getLogger(appcmdname)
|
|
|
|
else:
|
|
logger = logging.getLogger(appname)
|
|
|
|
|
|
_T = TypeVar('_T')
|
|
|
|
|
|
def git_shorthash_from_head() -> str | None:
|
|
"""
|
|
Determine short hash for current git HEAD.
|
|
|
|
Includes `.DIRTY` if any changes have been made from HEAD
|
|
|
|
:return: str | None: None if we couldn't determine the short hash.
|
|
"""
|
|
shorthash: str | None = None
|
|
|
|
try:
|
|
git_cmd = subprocess.Popen(
|
|
"git rev-parse --short HEAD".split(),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
out, err = git_cmd.communicate()
|
|
|
|
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
logger.info(f"Couldn't run git command for short hash: {e!r}")
|
|
|
|
else:
|
|
shorthash = out.decode().rstrip('\n')
|
|
if re.match(r'^[0-9a-f]{7,}$', shorthash) is None:
|
|
logger.error(f"'{shorthash}' doesn't look like a valid git short hash, forcing to None")
|
|
shorthash = None
|
|
|
|
if shorthash is not None:
|
|
with contextlib.suppress(Exception):
|
|
result = subprocess.run('git diff --stat HEAD'.split(), capture_output=True)
|
|
if len(result.stdout) > 0:
|
|
shorthash += '.DIRTY'
|
|
|
|
if len(result.stderr) > 0:
|
|
logger.warning(f'Data from git on stderr:\n{str(result.stderr)}')
|
|
|
|
return shorthash
|
|
|
|
|
|
def appversion() -> semantic_version.Version:
|
|
"""
|
|
Determine app version including git short hash if possible.
|
|
|
|
:return: The augmented app version.
|
|
"""
|
|
global _cached_version
|
|
if _cached_version is not None:
|
|
return _cached_version
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
# Running frozen, so we should have a .gitversion file
|
|
# Yes, .parent because if frozen we're inside library.zip
|
|
with open(pathlib.Path(sys.path[0]).parent / GITVERSION_FILE, encoding='utf-8') as gitv:
|
|
shorthash: str | None = gitv.read()
|
|
|
|
else:
|
|
# Running from source. Use git rev-parse --short HEAD
|
|
# or fall back to .gitversion file if it exists.
|
|
# This is also required for the Flatpak
|
|
shorthash = git_shorthash_from_head()
|
|
if shorthash is None:
|
|
if pathlib.Path(sys.path[0] + "/" + GITVERSION_FILE).exists():
|
|
with open(pathlib.Path(sys.path[0] + "/" + GITVERSION_FILE), encoding='utf-8') as gitv:
|
|
shorthash = gitv.read()
|
|
else:
|
|
shorthash = 'UNKNOWN'
|
|
|
|
_cached_version = semantic_version.Version(f'{_static_appversion}+{shorthash}')
|
|
return _cached_version
|
|
|
|
|
|
user_agent = f'EDCD-{appname}-{appversion()}'
|
|
|
|
|
|
def appversion_nobuild() -> semantic_version.Version:
|
|
"""
|
|
Determine app version without *any* build meta data.
|
|
|
|
This will not only strip any added git short hash, but also any trailing
|
|
'+<string>' in _static_appversion.
|
|
|
|
:return: App version without any build meta data.
|
|
"""
|
|
return appversion().truncate('prerelease')
|
|
|
|
|
|
class AbstractConfig(abc.ABC):
|
|
"""
|
|
Abstract root class of all platform specific Config implementations.
|
|
|
|
Commented lines are no longer supported or replaced.
|
|
"""
|
|
|
|
OUT_EDDN_SEND_STATION_DATA = 1
|
|
# OUT_MKT_BPC = 2 # No longer supported
|
|
OUT_MKT_TD = 4
|
|
OUT_MKT_CSV = 8
|
|
OUT_SHIP = 16
|
|
# OUT_SHIP_EDS = 16 # Replaced by OUT_SHIP
|
|
# OUT_SYS_FILE = 32 # No longer supported
|
|
# OUT_STAT = 64 # No longer available
|
|
# OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP
|
|
# OUT_SYS_EDSM = 256 # Now a plugin
|
|
# OUT_SYS_AUTO = 512 # Now always automatic
|
|
OUT_MKT_MANUAL = 1024
|
|
OUT_EDDN_SEND_NON_STATION = 2048
|
|
OUT_EDDN_DELAY = 4096
|
|
OUT_STATION_ANY = OUT_EDDN_SEND_STATION_DATA | OUT_MKT_TD | OUT_MKT_CSV
|
|
|
|
app_dir_path: pathlib.Path
|
|
plugin_dir_path: pathlib.Path
|
|
default_plugin_dir_path: pathlib.Path
|
|
internal_plugin_dir_path: pathlib.Path
|
|
respath_path: pathlib.Path
|
|
home_path: pathlib.Path
|
|
default_journal_dir_path: pathlib.Path
|
|
identifier: str
|
|
|
|
__in_shutdown = False # Is the application currently shutting down ?
|
|
__auth_force_localserver = False # Should we use localhost for auth callback ?
|
|
__auth_force_edmc_protocol = False # Should we force edmc:// protocol ?
|
|
__eddn_url = None # Non-default EDDN URL
|
|
__eddn_tracking_ui = False # Show EDDN tracking UI ?
|
|
|
|
def __init__(self) -> None:
|
|
self.home_path = pathlib.Path.home()
|
|
|
|
def set_shutdown(self):
|
|
"""Set flag denoting we're in the shutdown sequence."""
|
|
self.__in_shutdown = True
|
|
|
|
@property
|
|
def shutting_down(self) -> bool:
|
|
"""
|
|
Determine if we're in the shutdown sequence.
|
|
|
|
:return: bool - True if in shutdown sequence.
|
|
"""
|
|
return self.__in_shutdown
|
|
|
|
def set_auth_force_localserver(self):
|
|
"""Set flag to force use of localhost web server for Frontier Auth callback."""
|
|
self.__auth_force_localserver = True
|
|
|
|
@property
|
|
def auth_force_localserver(self) -> bool:
|
|
"""
|
|
Determine if use of localhost is forced for Frontier Auth callback.
|
|
|
|
:return: bool - True if we should use localhost web server.
|
|
"""
|
|
return self.__auth_force_localserver
|
|
|
|
def set_auth_force_edmc_protocol(self):
|
|
"""Set flag to force use of localhost web server for Frontier Auth callback."""
|
|
self.__auth_force_edmc_protocol = True
|
|
|
|
@property
|
|
def auth_force_edmc_protocol(self) -> bool:
|
|
"""
|
|
Determine if use of localhost is forced for Frontier Auth callback.
|
|
|
|
:return: bool - True if we should use localhost web server.
|
|
"""
|
|
return self.__auth_force_edmc_protocol
|
|
|
|
def set_eddn_url(self, eddn_url: str):
|
|
"""Set the specified eddn URL."""
|
|
self.__eddn_url = eddn_url
|
|
|
|
@property
|
|
def eddn_url(self) -> str | None:
|
|
"""
|
|
Provide the custom EDDN URL.
|
|
|
|
:return: str - Custom EDDN URL to use.
|
|
"""
|
|
return self.__eddn_url
|
|
|
|
def set_eddn_tracking_ui(self):
|
|
"""Activate EDDN tracking UI."""
|
|
self.__eddn_tracking_ui = True
|
|
|
|
@property
|
|
def eddn_tracking_ui(self) -> bool:
|
|
"""
|
|
Determine if the EDDN tracking UI be shown.
|
|
|
|
:return: bool - Should tracking UI be active?
|
|
"""
|
|
return self.__eddn_tracking_ui
|
|
|
|
@property
|
|
def app_dir(self) -> str:
|
|
"""Return a string version of app_dir."""
|
|
return str(self.app_dir_path)
|
|
|
|
@property
|
|
def plugin_dir(self) -> str:
|
|
"""Return a string version of plugin_dir."""
|
|
return str(self.plugin_dir_path)
|
|
|
|
@property
|
|
def default_plugin_dir(self) -> str:
|
|
"""Return a string version of plugin_dir."""
|
|
return str(self.default_plugin_dir_path)
|
|
|
|
@property
|
|
def internal_plugin_dir(self) -> str:
|
|
"""Return a string version of internal_plugin_dir."""
|
|
return str(self.internal_plugin_dir_path)
|
|
|
|
@property
|
|
def respath(self) -> str:
|
|
"""Return a string version of respath."""
|
|
return str(self.respath_path)
|
|
|
|
@property
|
|
def home(self) -> str:
|
|
"""Return a string version of home."""
|
|
return str(self.home_path)
|
|
|
|
@property
|
|
def default_journal_dir(self) -> str:
|
|
"""Return a string version of default_journal_dir."""
|
|
return str(self.default_journal_dir_path)
|
|
|
|
@staticmethod
|
|
def _suppress_call(
|
|
func: Callable[..., _T], exceptions: Type[BaseException] | list[Type[BaseException]] = Exception,
|
|
*args: Any, **kwargs: Any
|
|
) -> _T | None:
|
|
if exceptions is None:
|
|
exceptions = [Exception]
|
|
|
|
if not isinstance(exceptions, list):
|
|
exceptions = [exceptions]
|
|
|
|
with contextlib.suppress(*exceptions):
|
|
return func(*args, **kwargs)
|
|
|
|
return None
|
|
|
|
def get(
|
|
self, key: str,
|
|
default: list | str | bool | int | None = None
|
|
) -> list | str | bool | int | None:
|
|
"""
|
|
Return the data for the requested key, or a default.
|
|
|
|
:param key: The key data is being requested for.
|
|
:param default: The default to return if the key does not exist, defaults to None.
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
:return: The data or the default.
|
|
"""
|
|
# DEPRECATED: Migrate to specific type getters. Will remove in 6.0 or later.
|
|
warnings.warn('get is Deprecated. use the specific getter for your type', DeprecationWarning, stacklevel=2)
|
|
|
|
if (a_list := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None:
|
|
return a_list
|
|
|
|
if (a_str := self._suppress_call(self.get_str, ValueError, key, default=None)) is not None:
|
|
return a_str
|
|
|
|
if (a_bool := self._suppress_call(self.get_bool, ValueError, key, default=None)) is not None:
|
|
return a_bool
|
|
|
|
if (an_int := self._suppress_call(self.get_int, ValueError, key, default=None)) is not None:
|
|
return an_int
|
|
|
|
return default
|
|
|
|
@abstractmethod
|
|
def get_list(self, key: str, *, default: list | None = None) -> list:
|
|
"""
|
|
Return the list referred to by the given key if it exists, or the default.
|
|
|
|
Implements :meth:`AbstractConfig.get_list`.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def get_str(self, key: str, *, default: str | None = None) -> str:
|
|
"""
|
|
Return the string referred to by the given key if it exists, or the default.
|
|
|
|
:param key: The key data is being requested for.
|
|
:param default: Default to return if the key does not exist, defaults to None.
|
|
:raises ValueError: If an internal error occurs getting or converting a value.
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
:return: The requested data or the default.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def get_bool(self, key: str, *, default: bool | None = None) -> bool:
|
|
"""
|
|
Return the bool referred to by the given key if it exists, or the default.
|
|
|
|
:param key: The key data is being requested for.
|
|
:param default: Default to return if the key does not exist, defaults to None
|
|
:raises ValueError: If an internal error occurs getting or converting a value
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
:return: The requested data or the default
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def getint(self, key: str, *, default: int = 0) -> int:
|
|
"""
|
|
Getint is a Deprecated getter method.
|
|
|
|
See get_int for its replacement.
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
"""
|
|
# DEPRECATED: Migrate to get_int. Will remove in 6.0 or later.
|
|
warnings.warn('getint is Deprecated. Use get_int instead', DeprecationWarning, stacklevel=2)
|
|
|
|
return self.get_int(key, default=default)
|
|
|
|
@abstractmethod
|
|
def get_int(self, key: str, *, default: int = 0) -> int:
|
|
"""
|
|
Return the int referred to by key if it exists in the config.
|
|
|
|
For legacy reasons, the default is 0 and not None.
|
|
|
|
:param key: The key data is being requested for.
|
|
:param default: Default to return if the key does not exist, defaults to 0.
|
|
:raises ValueError: If the internal representation of this key cannot be converted to an int.
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
:return: The requested data or the default.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def set(self, key: str, val: int | str | list[str] | bool) -> None:
|
|
"""
|
|
Set the given key's data to the given value.
|
|
|
|
:param key: The key to set the value on.
|
|
:param val: The value to set the key's data to.
|
|
:raises ValueError: On an invalid type.
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def delete(self, key: str, *, suppress=False) -> None:
|
|
"""
|
|
Delete the given key from the config.
|
|
|
|
:param key: The key to delete.
|
|
:param suppress: bool - Whether to suppress any errors. Useful in case
|
|
code to migrate settings is blindly removing an old key.
|
|
:raises OSError: On Windows, if a registry error occurs.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def save(self) -> None:
|
|
"""
|
|
Save the current configuration.
|
|
|
|
:raises OSError: On Windows, if a Registry error occurs.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def close(self) -> None:
|
|
"""Close this config and release any associated resources."""
|
|
raise NotImplementedError
|
|
|
|
# DEPRECATED: Password system doesn't do anything. Will remove in 6.0 or later.
|
|
def get_password(self, account: str) -> None:
|
|
"""Legacy password retrieval."""
|
|
warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2)
|
|
|
|
def set_password(self, account: str, password: str) -> None:
|
|
"""Legacy password setting."""
|
|
warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2)
|
|
|
|
def delete_password(self, account: str) -> None:
|
|
"""Legacy password deletion."""
|
|
warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2)
|
|
# End Dep Zone
|
|
|
|
|
|
def get_config(*args, **kwargs) -> AbstractConfig:
|
|
"""
|
|
Get the appropriate config class for the current platform.
|
|
|
|
:param args: Args to be passed through to implementation.
|
|
:param kwargs: Args to be passed through to implementation.
|
|
:return: Instance of the implementation.
|
|
"""
|
|
if sys.platform == "win32": # pragma: sys-platform-win32
|
|
from .windows import WinConfig
|
|
return WinConfig(*args, **kwargs)
|
|
|
|
if sys.platform == "linux": # pragma: sys-platform-linux
|
|
from .linux import LinuxConfig
|
|
return LinuxConfig(*args, **kwargs)
|
|
|
|
raise ValueError(f'Unknown platform: {sys.platform=}')
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
|
# Wiki: https://github.com/EDCD/EDMarketConnector/wiki/Participating-in-Open-Betas-of-EDMC
|
|
def get_update_feed() -> str:
|
|
"""Select the proper update feed for the current update track."""
|
|
if config.get_bool('beta_optin'):
|
|
return 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector-beta.xml'
|
|
return 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'
|
|
|
|
|
|
# DEPRECATED: Migrate to get_update_feed(). Will remove in 6.0 or later.
|
|
def __getattr__(name: str):
|
|
if name == 'update_feed':
|
|
warnings.warn('update_feed is deprecated, and will be removed in 6.0 or later. '
|
|
'Please migrate to get_update_feed()', DeprecationWarning, stacklevel=2)
|
|
return get_update_feed()
|
|
raise AttributeError(name=name)
|