mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-12 15:27:14 +03:00
229 lines
7.7 KiB
Python
229 lines
7.7 KiB
Python
"""
|
|
windows.py - Windows config implementation.
|
|
|
|
Copyright (c) EDCD, All Rights Reserved
|
|
Licensed under the GNU General Public License.
|
|
See LICENSE file.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import functools
|
|
import pathlib
|
|
import sys
|
|
import uuid
|
|
import winreg
|
|
from ctypes.wintypes import DWORD, HANDLE
|
|
from typing import Literal
|
|
from config import AbstractConfig, applongname, appname, logger
|
|
|
|
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) -> str | None:
|
|
"""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) -> None:
|
|
super().__init__()
|
|
REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806
|
|
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=REGISTRY_SUBKEY)
|
|
|
|
except OSError:
|
|
logger.exception('Could not create required registry keys')
|
|
raise
|
|
|
|
self.app_dir_path = pathlib.Path(known_folder_path(FOLDERID_LocalAppData)) / appname # type: ignore
|
|
self.app_dir_path.mkdir(exist_ok=True)
|
|
|
|
self.default_plugin_dir_path = self.app_dir_path / 'plugins'
|
|
if (plugdir_str := self.get_str('plugin_dir')) is None or not pathlib.Path(plugdir_str).is_dir():
|
|
self.set("plugin_dir", str(self.default_plugin_dir_path))
|
|
plugdir_str = self.default_plugin_dir
|
|
self.plugin_dir_path = pathlib.Path(plugdir_str)
|
|
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_path = pathlib.Path(
|
|
known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore
|
|
self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore
|
|
|
|
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 __get_regentry(self, key: str) -> 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
|
|
|
|
# For programmers who want to actually know what is going on
|
|
if _type == winreg.REG_SZ:
|
|
return str(value)
|
|
|
|
if _type == winreg.REG_DWORD:
|
|
return int(value)
|
|
|
|
if _type == winreg.REG_MULTI_SZ:
|
|
return list(value)
|
|
|
|
logger.warning(f'Registry key {key=} returned unknown type {_type=} {value=}')
|
|
return None
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
if 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 = 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
|
|
|
|
if 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 = 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 # Yes it could be None, but we're _assuming_ that people gave us a default
|
|
|
|
return bool(res)
|
|
|
|
def set(self, key: str, val: int | str | list[str] | bool) -> None:
|
|
"""
|
|
Set the given key's data to the given value.
|
|
|
|
Implements :meth:`AbstractConfig.set`.
|
|
"""
|
|
# These are the types that winreg.REG_* below resolve to.
|
|
reg_type: Literal[1] | Literal[4] | Literal[7]
|
|
if isinstance(val, str):
|
|
reg_type = winreg.REG_SZ
|
|
|
|
elif isinstance(val, int):
|
|
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)=}')
|
|
|
|
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()
|