mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-13 15:57:14 +03:00
Does what it says on the tin. Moves config implementations out to individual files, guards those around platforms to make stuff more reasonably split out.
260 lines
9.1 KiB
Python
260 lines
9.1 KiB
Python
"""Windows config implementation."""
|
|
|
|
# spell-checker: words folderid deps hkey edcd
|
|
import ctypes
|
|
import functools
|
|
import pathlib
|
|
import sys
|
|
import uuid
|
|
import winreg
|
|
from ctypes.wintypes import DWORD, HANDLE
|
|
from typing import List, Optional, Union
|
|
|
|
from config import AbstractConfig, applongname, appname, logger, update_interval
|
|
|
|
assert sys.platform == 'win32'
|
|
|
|
REG_RESERVED_ALWAYS_ZERO = 0
|
|
|
|
# This is the only way to do this from python without external deps (which do this anyway).
|
|
FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}')
|
|
FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}')
|
|
FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}')
|
|
FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}')
|
|
|
|
SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath
|
|
SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)]
|
|
|
|
CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree
|
|
CoTaskMemFree.argtypes = [ctypes.c_void_p]
|
|
|
|
|
|
def known_folder_path(guid: uuid.UUID) -> Optional[str]:
|
|
"""Look up a Windows GUID to actual folder path name."""
|
|
buf = ctypes.c_wchar_p()
|
|
if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)):
|
|
return None
|
|
retval = buf.value # copy data
|
|
CoTaskMemFree(buf) # and free original
|
|
return retval
|
|
|
|
|
|
class WinConfig(AbstractConfig):
|
|
"""Implementation of AbstractConfig for Windows."""
|
|
|
|
def __init__(self, do_winsparkle=True) -> None:
|
|
self.app_dir_path = pathlib.Path(str(known_folder_path(FOLDERID_LocalAppData))) / appname
|
|
self.app_dir_path.mkdir(exist_ok=True)
|
|
|
|
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
|
self.plugin_dir_path.mkdir(exist_ok=True)
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
self.respath_path = pathlib.Path(sys.executable).parent
|
|
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
|
|
|
else:
|
|
self.respath_path = pathlib.Path(__file__).parent.parent
|
|
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
|
|
|
self.home_path = pathlib.Path.home()
|
|
|
|
journal_dir_str = known_folder_path(FOLDERID_SavedGames)
|
|
journaldir = pathlib.Path(journal_dir_str) if journal_dir_str is not None else None
|
|
self.default_journal_dir_path = None # type: ignore
|
|
if journaldir is not None:
|
|
self.default_journal_dir_path = journaldir / 'Frontier Developments' / 'Elite Dangerous'
|
|
|
|
create_key_defaults = functools.partial(
|
|
winreg.CreateKeyEx,
|
|
key=winreg.HKEY_CURRENT_USER,
|
|
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
|
|
)
|
|
|
|
try:
|
|
self.__reg_handle: winreg.HKEYType = create_key_defaults(
|
|
sub_key=r'Software\Marginal\EDMarketConnector'
|
|
)
|
|
if do_winsparkle:
|
|
self.__setup_winsparkle()
|
|
|
|
except OSError:
|
|
logger.exception('could not create required registry keys')
|
|
raise
|
|
|
|
self.identifier = applongname
|
|
if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir():
|
|
docs = known_folder_path(FOLDERID_Documents)
|
|
self.set('outdir', docs if docs is not None else self.home)
|
|
|
|
def __setup_winsparkle(self):
|
|
"""Ensure the necessary Registry keys for WinSparkle are present."""
|
|
create_key_defaults = functools.partial(
|
|
winreg.CreateKeyEx,
|
|
key=winreg.HKEY_CURRENT_USER,
|
|
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
|
|
)
|
|
try:
|
|
edcd_handle: winreg.HKEYType = create_key_defaults(sub_key=r'Software\EDCD\EDMarketConnector')
|
|
winsparkle_reg: winreg.HKEYType = winreg.CreateKeyEx(
|
|
edcd_handle, sub_key='WinSparkle', access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY
|
|
)
|
|
|
|
except OSError:
|
|
logger.exception('could not open WinSparkle handle')
|
|
raise
|
|
|
|
# set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings
|
|
winreg.SetValueEx(
|
|
winsparkle_reg, 'UpdateInterval', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, str(update_interval)
|
|
)
|
|
|
|
try:
|
|
winreg.QueryValueEx(winsparkle_reg, 'CheckForUpdates')
|
|
|
|
except FileNotFoundError:
|
|
# Key doesn't exist, set it to a default
|
|
winreg.SetValueEx(winsparkle_reg, 'CheckForUpdates', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, '1')
|
|
|
|
winsparkle_reg.Close()
|
|
edcd_handle.Close()
|
|
|
|
def __get_regentry(self, key: str) -> Union[None, list, str, int]:
|
|
"""Access the Registry for the raw entry."""
|
|
try:
|
|
value, _type = winreg.QueryValueEx(self.__reg_handle, key)
|
|
except FileNotFoundError:
|
|
# Key doesn't exist
|
|
return None
|
|
|
|
# The type returned is actually as we'd expect for each of these. The casts are here for type checkers and
|
|
# For programmers who want to actually know what is going on
|
|
if _type == winreg.REG_SZ:
|
|
return str(value)
|
|
|
|
elif _type == winreg.REG_DWORD:
|
|
return int(value)
|
|
|
|
elif _type == winreg.REG_MULTI_SZ:
|
|
return list(value)
|
|
|
|
else:
|
|
logger.warning(f'registry key {key=} returned unknown type {_type=} {value=}')
|
|
return None
|
|
|
|
def get_str(self, key: str, *, default: str = None) -> str:
|
|
"""
|
|
Return the string referred to by the given key if it exists, or the default.
|
|
|
|
Implements :meth:`AbstractConfig.get_str`.
|
|
"""
|
|
res = self.__get_regentry(key)
|
|
if res is None:
|
|
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
|
|
|
elif not isinstance(res, str):
|
|
raise ValueError(f'Data from registry is not a string: {type(res)=} {res=}')
|
|
|
|
return res
|
|
|
|
def get_list(self, key: str, *, default: list = None) -> list:
|
|
"""
|
|
Return the list referred to by the given key if it exists, or the default.
|
|
|
|
Implements :meth:`AbstractConfig.get_list`.
|
|
"""
|
|
res = self.__get_regentry(key)
|
|
if res is None:
|
|
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
|
|
|
elif not isinstance(res, list):
|
|
raise ValueError(f'Data from registry is not a list: {type(res)=} {res}')
|
|
|
|
return res
|
|
|
|
def get_int(self, key: str, *, default: int = 0) -> int:
|
|
"""
|
|
Return the int referred to by key if it exists in the config.
|
|
|
|
Implements :meth:`AbstractConfig.get_int`.
|
|
"""
|
|
res = self.__get_regentry(key)
|
|
if res is None:
|
|
return default
|
|
|
|
if not isinstance(res, int):
|
|
raise ValueError(f'Data from registry is not an int: {type(res)=} {res}')
|
|
|
|
return res
|
|
|
|
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
|
"""
|
|
Return the bool referred to by the given key if it exists, or the default.
|
|
|
|
Implements :meth:`AbstractConfig.get_bool`.
|
|
"""
|
|
res = self.get_int(key, default=default) # type: ignore
|
|
if res is None:
|
|
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
|
|
|
return bool(res)
|
|
|
|
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
|
"""
|
|
Set the given key's data to the given value.
|
|
|
|
Implements :meth:`AbstractConfig.set`.
|
|
"""
|
|
reg_type = None
|
|
if isinstance(val, str):
|
|
reg_type = winreg.REG_SZ
|
|
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, val)
|
|
|
|
elif isinstance(val, int): # The original code checked for numbers.Integral, I dont think that is needed.
|
|
reg_type = winreg.REG_DWORD
|
|
|
|
elif isinstance(val, list):
|
|
reg_type = winreg.REG_MULTI_SZ
|
|
|
|
elif isinstance(val, bool):
|
|
reg_type = winreg.REG_DWORD
|
|
val = int(val)
|
|
|
|
else:
|
|
raise ValueError(f'Unexpected type for value {type(val)=}')
|
|
|
|
# Its complaining about the list, it works, tested on windows, ignored.
|
|
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, reg_type, val) # type: ignore
|
|
|
|
def delete(self, key: str, *, suppress=False) -> None:
|
|
"""
|
|
Delete the given key from the config.
|
|
|
|
'key' is relative to the base Registry path we use.
|
|
|
|
Implements :meth:`AbstractConfig.delete`.
|
|
"""
|
|
try:
|
|
winreg.DeleteValue(self.__reg_handle, key)
|
|
except OSError:
|
|
if suppress:
|
|
return
|
|
|
|
raise
|
|
|
|
def save(self) -> None:
|
|
"""
|
|
Save the configuration.
|
|
|
|
Not required for WinConfig as Registry keys are flushed on write.
|
|
"""
|
|
pass
|
|
|
|
def close(self):
|
|
"""
|
|
Close this config and release any associated resources.
|
|
|
|
Implements :meth:`AbstractConfig.close`.
|
|
"""
|
|
self.__reg_handle.Close()
|