mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2026-10-29 01:26:08 +03:00
Merge pull request #2520 from HullSeals/enhancement/2113/update-python-version
[#2113] Update Python Version to 3.13
This commit is contained in:
14 files changed
+205
-179
No files matched your search
@@ -77,7 +77,7 @@ repos:
|
||||
always_run: true
|
||||
|
||||
default_language_version:
|
||||
python: python3.11
|
||||
python: python3.13
|
||||
|
||||
default_stages: [ pre-commit, pre-push ]
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.11
|
||||
3.13
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first).
|
||||
---
|
||||
* We currently test against, and package with, Python 3.11, 32-bit.
|
||||
* We currently test against, and package with, Python 3.13, 32-bit.
|
||||
* As a result, we do not support Windows 7, 8, or 8.1.
|
||||
* Developers can check the contents of the `.python-version` file
|
||||
in the source (not distributed with the Windows installer) for the
|
||||
@@ -1637,7 +1637,7 @@ updated to using Python 3.9.8.
|
||||
- When the new option is not active, the application will minimize to the
|
||||
taskbar as normal.
|
||||
|
||||
Bug Fixex
|
||||
Bug Fixes
|
||||
---
|
||||
|
||||
* If a CAPI query failed in such a way that no `requests.Response` object
|
||||
|
||||
+1
-1
@@ -821,7 +821,7 @@ class Session:
|
||||
capi_data = CAPIData(capi_json, capi_host, capi_endpoint, monitor.cmdr)
|
||||
self.capi_raw_data.record_endpoint(
|
||||
capi_endpoint, r.content.decode(encoding='utf-8'),
|
||||
datetime.datetime.utcnow()
|
||||
datetime.datetime.now(datetime.timezone.utc)
|
||||
)
|
||||
|
||||
except requests.ConnectionError as e:
|
||||
|
||||
+9
-13
@@ -335,6 +335,7 @@ class AbstractConfig(abc.ABC):
|
||||
|
||||
return None
|
||||
|
||||
@warnings.deprecated("get() is Deprecated. use the specific getter for your type. Will remove in 6.0")
|
||||
def get(
|
||||
self, key: str,
|
||||
default: list | str | bool | int | None = None
|
||||
@@ -347,9 +348,6 @@ class AbstractConfig(abc.ABC):
|
||||
: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
|
||||
|
||||
@@ -399,6 +397,7 @@ class AbstractConfig(abc.ABC):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@warnings.deprecated("Migrate to get_int. Will remove in 6.0 or later.")
|
||||
def getint(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Getint is a Deprecated getter method.
|
||||
@@ -406,9 +405,6 @@ class AbstractConfig(abc.ABC):
|
||||
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
|
||||
@@ -464,18 +460,20 @@ class AbstractConfig(abc.ABC):
|
||||
"""Close this config and release any associated resources."""
|
||||
raise NotImplementedError
|
||||
|
||||
# DEPRECATED: Password system doesn't do anything. Will remove in 6.0 or later.
|
||||
@warnings.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)
|
||||
pass
|
||||
|
||||
@warnings.deprecated("Password system doesn't do anything. Will remove in 6.0 or later.")
|
||||
def set_password(self, account: str, password: str) -> None:
|
||||
"""Legacy password setting."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2)
|
||||
pass
|
||||
|
||||
@warnings.deprecated("Password system doesn't do anything. Will remove in 6.0 or later.")
|
||||
def delete_password(self, account: str) -> None:
|
||||
"""Legacy password deletion."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2)
|
||||
pass
|
||||
# End Dep Zone
|
||||
|
||||
|
||||
@@ -509,10 +507,8 @@ def get_update_feed() -> str:
|
||||
return 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'
|
||||
|
||||
|
||||
# DEPRECATED: Migrate to get_update_feed(). Will remove in 6.0 or later.
|
||||
@warnings.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)
|
||||
@@ -126,7 +126,7 @@ class Translations:
|
||||
|
||||
def contents(self, lang: str, plugin_path: pathlib.Path | None = None) -> dict[str, str]:
|
||||
"""Load all the translations from a translation file."""
|
||||
if lang not in self.available():
|
||||
if lang not in self.available() and lang != self.FALLBACK: # Because "en" doesn't appear in self.available()
|
||||
raise KeyError(f'Language {lang} not available')
|
||||
translations = {}
|
||||
|
||||
@@ -262,17 +262,17 @@ def _wszarray_to_list(array):
|
||||
class _Locale:
|
||||
"""Locale holds a few utility methods to convert data to and from localized versions."""
|
||||
|
||||
# DEPRECATED: Migrate to _Locale.string_from_number. Will remove in 6.0 or later.
|
||||
@warnings.deprecated("Migrate to _Locale.string_from_number. Will remove in 6.0 or later.")
|
||||
def stringFromNumber(self, number: float | int, decimals: int | None = None) -> str: # noqa: N802
|
||||
warnings.warn('use _Locale.string_from_number instead.', DeprecationWarning, stacklevel=2)
|
||||
return self.string_from_number(number, decimals) # type: ignore
|
||||
|
||||
# DEPRECATED: Migrate to _Locale.number_from_string. Will remove in 6.0 or later.
|
||||
@warnings.deprecated("Migrate to _Locale.number_from_string. Will remove in 6.0 or later.")
|
||||
def numberFromString(self, string: str) -> int | float | None: # noqa: N802
|
||||
warnings.warn('use _Locale.number_from_string instead.', DeprecationWarning, stacklevel=2)
|
||||
return self.number_from_string(string)
|
||||
|
||||
# DEPRECATED: Migrate to _Locale.preferred_languages. Will remove in 6.0 or later.
|
||||
@warnings.deprecated("Migrate to _Locale.preferred_languages. Will remove in 6.0 or later.")
|
||||
def preferredLanguages(self) -> Iterable[str]: # noqa: N802
|
||||
warnings.warn('use _Locale.preferred_languages instead.', DeprecationWarning, stacklevel=2)
|
||||
return self.preferred_languages()
|
||||
@@ -354,13 +354,10 @@ Locale = _Locale()
|
||||
translations = Translations()
|
||||
|
||||
|
||||
# DEPRECATED: Migrate to `translations`. Will be removed in 6.0 or later.
|
||||
# 'Translations' singleton is deprecated.
|
||||
@warnings.deprecated("Migrate to `translations`. Will be removed in 6.0 or later.")
|
||||
# Begin Deprecation Zone
|
||||
class _Translations(Translations):
|
||||
def __init__(self):
|
||||
warnings.warn('Translations and _Translations() are deprecated. '
|
||||
'Please use translations and Translations() instead.', DeprecationWarning, stacklevel=2)
|
||||
super().__init__()
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
def __init__(self) -> None:
|
||||
# TODO(A_D): A bunch of these should be switched to default values (eg '' for strings) and no longer be Optional
|
||||
FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog
|
||||
self.root: 'tkinter.Tk' = None # type: ignore # Don't use Optional[] - mypy thinks no methods
|
||||
self.root: tkinter.Tk = None # type: ignore # Don't use Optional[] - mypy thinks no methods
|
||||
self.currentdir: str | None = None # The actual logdir that we're monitoring
|
||||
self.logfile: str | None = None
|
||||
self.observer: BaseObserver | None = None
|
||||
@@ -188,7 +188,7 @@ class EDLogs(FileSystemEventHandler):
|
||||
},
|
||||
}
|
||||
|
||||
def start(self, root: 'tkinter.Tk') -> bool: # noqa: CCR001
|
||||
def start(self, root: tkinter.Tk) -> bool: # noqa: CCR001
|
||||
"""
|
||||
Start journal monitoring.
|
||||
|
||||
|
||||
+2
-4
@@ -125,9 +125,8 @@ class EntryMenu(ttk.Entry):
|
||||
class Entry(EntryMenu):
|
||||
"""Custom ttk.Entry class to fix some display issues."""
|
||||
|
||||
# DEPRECATED: Migrate to EntryMenu. Will remove in 6.0 or later.
|
||||
@warnings.deprecated("Migrate to EntryMenu. Will remove in 6.0 or later.")
|
||||
def __init__(self, master: ttk.Frame | None = None, **kw):
|
||||
warnings.warn('Migrate to EntryMenu. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2)
|
||||
EntryMenu.__init__(self, master, **kw)
|
||||
|
||||
|
||||
@@ -144,9 +143,8 @@ class Button(ttk.Button):
|
||||
class ColoredButton(tk.Button):
|
||||
"""Custom tk.Button class to fix some display issues."""
|
||||
|
||||
# DEPRECATED: Migrate to tk.Button. Will remove in 6.0 or later.
|
||||
@warnings.deprecated("Migrate to tk.Button. Will remove in 6.0 or later.")
|
||||
def __init__(self, master: ttk.Frame | None = None, **kw):
|
||||
warnings.warn('Migrate to tk.Button. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2)
|
||||
tk.Button.__init__(self, master, **kw)
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import pathlib
|
||||
import re
|
||||
import sqlite3
|
||||
import tkinter as tk
|
||||
import warnings
|
||||
from platform import system
|
||||
from textwrap import dedent
|
||||
from threading import Lock
|
||||
@@ -221,9 +222,9 @@ class EDDNSender:
|
||||
|
||||
return db_conn
|
||||
|
||||
@warnings.deprecated("Legacy files no longer used as of 5.6.0. Will remove in 6.0 or later.")
|
||||
def convert_legacy_file(self):
|
||||
"""Convert a legacy file's contents into the sqlite3 db."""
|
||||
# DEPRECATED: Legacy files no longer used as of 5.6.0. Will remove in 6.0 or later.
|
||||
filename = config.app_dir_path / 'replay.jsonl'
|
||||
if not filename.exists():
|
||||
return
|
||||
|
||||
@@ -37,11 +37,9 @@ logger = get_main_logger()
|
||||
|
||||
# May be imported by plugins
|
||||
|
||||
# DEPRECATED: Migrate to open_log_folder. Will remove in 6.0 or later.
|
||||
@warnings.deprecated("Migrate to open_log_folder. Will remove in 6.0 or later.")
|
||||
def help_open_log_folder() -> None:
|
||||
"""Open the folder logs are stored in."""
|
||||
warnings.warn('prefs.help_open_log_folder is deprecated, use open_log_folder instead. '
|
||||
'This function will be removed in 6.0 or later', DeprecationWarning, stacklevel=2)
|
||||
open_folder(Path(config.app_dir_path / 'logs'))
|
||||
|
||||
|
||||
|
||||
+166
-130
@@ -5,6 +5,7 @@ Copyright (c) EDCD, All Rights Reserved
|
||||
Licensed under the GNU General Public License.
|
||||
See LICENSE file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -23,8 +24,8 @@ logger = get_main_logger()
|
||||
|
||||
is_wine = False
|
||||
|
||||
if sys.platform == 'win32':
|
||||
is_wine = bool(os.getenv('WINEPREFIX'))
|
||||
if sys.platform == "win32":
|
||||
is_wine = bool(os.getenv("WINEPREFIX"))
|
||||
|
||||
|
||||
class GenericProtocolHandler:
|
||||
@@ -32,10 +33,10 @@ class GenericProtocolHandler:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.redirect = protocolhandler_redirect # Base redirection URL
|
||||
self.master: 'tkinter.Tk' = None # type: ignore
|
||||
self.master: tkinter.Tk = None # type: ignore
|
||||
self.lastpayload: str | None = None
|
||||
|
||||
def start(self, master: 'tkinter.Tk') -> None:
|
||||
def start(self, master: tkinter.Tk) -> None:
|
||||
"""Start Protocol Handler."""
|
||||
self.master = master
|
||||
|
||||
@@ -46,29 +47,50 @@ class GenericProtocolHandler:
|
||||
"""Generate an auth event."""
|
||||
self.lastpayload = url
|
||||
|
||||
logger.trace_if('frontier-auth', f'Payload: {self.lastpayload}')
|
||||
logger.trace_if("frontier-auth", f"Payload: {self.lastpayload}")
|
||||
if not config.shutting_down:
|
||||
logger.debug('event_generate("<<CompanionAuthEvent>>")')
|
||||
self.master.event_generate('<<CompanionAuthEvent>>', when="tail")
|
||||
self.master.event_generate("<<CompanionAuthEvent>>", when="tail")
|
||||
|
||||
|
||||
if (config.auth_force_edmc_protocol # noqa: C901
|
||||
or (
|
||||
sys.platform == 'win32'
|
||||
and getattr(sys, 'frozen', False)
|
||||
and not is_wine
|
||||
and not config.auth_force_localserver
|
||||
)):
|
||||
if config.auth_force_edmc_protocol or ( # noqa: C901
|
||||
sys.platform == "win32"
|
||||
and getattr(sys, "frozen", False)
|
||||
and not is_wine
|
||||
and not config.auth_force_localserver
|
||||
):
|
||||
# This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces
|
||||
if sys.platform != 'win32':
|
||||
if sys.platform != "win32":
|
||||
raise EnvironmentError("This code is for Windows only.")
|
||||
# spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL
|
||||
from ctypes import ( # type: ignore
|
||||
windll, POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at
|
||||
windll,
|
||||
POINTER,
|
||||
WINFUNCTYPE,
|
||||
Structure,
|
||||
byref,
|
||||
c_long,
|
||||
c_void_p,
|
||||
create_unicode_buffer,
|
||||
wstring_at,
|
||||
)
|
||||
from ctypes.wintypes import (
|
||||
ATOM, BOOL, HBRUSH, HGLOBAL, HICON, HINSTANCE, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR,
|
||||
MSG, UINT, WPARAM
|
||||
ATOM,
|
||||
BOOL,
|
||||
HBRUSH,
|
||||
HGLOBAL,
|
||||
HICON,
|
||||
HINSTANCE,
|
||||
HWND,
|
||||
INT,
|
||||
LPARAM,
|
||||
LPCWSTR,
|
||||
LPMSG,
|
||||
LPVOID,
|
||||
LPWSTR,
|
||||
MSG,
|
||||
UINT,
|
||||
WPARAM,
|
||||
)
|
||||
import win32gui
|
||||
import win32api
|
||||
@@ -83,16 +105,16 @@ if (config.auth_force_edmc_protocol # noqa: C901
|
||||
"""
|
||||
|
||||
_fields_ = [
|
||||
('style', UINT),
|
||||
('lpfnWndProc', WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM)),
|
||||
('cbClsExtra', INT),
|
||||
('cbWndExtra', INT),
|
||||
('hInstance', HINSTANCE),
|
||||
('hIcon', HICON),
|
||||
('hCursor', c_void_p),
|
||||
('hbrBackground', HBRUSH),
|
||||
('lpszMenuName', LPCWSTR),
|
||||
('lpszClassName', LPCWSTR)
|
||||
("style", UINT),
|
||||
("lpfnWndProc", WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM)),
|
||||
("cbClsExtra", INT),
|
||||
("cbWndExtra", INT),
|
||||
("hInstance", HINSTANCE),
|
||||
("hIcon", HICON),
|
||||
("hCursor", c_void_p),
|
||||
("hbrBackground", HBRUSH),
|
||||
("lpszMenuName", LPCWSTR),
|
||||
("lpszClassName", LPCWSTR),
|
||||
]
|
||||
|
||||
CW_USEDEFAULT = 0x80000000
|
||||
@@ -140,52 +162,58 @@ if (config.auth_force_edmc_protocol # noqa: C901
|
||||
# Windows Message handler stuff (IPC)
|
||||
# https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms633573(v=vs.85)
|
||||
@WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM)
|
||||
def WndProc(hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM) -> c_long: # noqa: N803 N802
|
||||
def WndProc(hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM) -> int: # noqa: N803 N802
|
||||
"""
|
||||
Deal with DDE requests.
|
||||
Windows message procedure for handling DDE requests.
|
||||
|
||||
:param hwnd: Window handle
|
||||
:param message: The message being sent
|
||||
:param wParam: Additional Message Information (depends on message type)
|
||||
:param lParam: Also additional message information
|
||||
:return: ???
|
||||
This routine responds to WM_DDE_INITIATE messages for the application's
|
||||
DDE service (the app name) and topic ("System"). It sends an ACK if the
|
||||
incoming atoms match the expected values.
|
||||
|
||||
Returns int due to Python 3.11+ CTYPES Callback - decorator changes to c_long
|
||||
"""
|
||||
if message != WM_DDE_INITIATE:
|
||||
# Not a DDE init message, bail and tell windows to do the default
|
||||
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowproca?redirectedfrom=MSDN
|
||||
return win32gui.DefWindowProc(hwnd, message, wParam, lParam)
|
||||
try:
|
||||
if message != WM_DDE_INITIATE:
|
||||
# Let Windows handle all other messages normally
|
||||
return win32gui.DefWindowProc(hwnd, message, wParam, lParam)
|
||||
|
||||
service = create_unicode_buffer(256)
|
||||
topic = create_unicode_buffer(256)
|
||||
# Note that lParam is 32 bits, and broken into two 16 bit words. This will break on 64bit as the math is
|
||||
# wrong
|
||||
# if nonzero, the target application for which a conversation is requested
|
||||
lparam_low = lParam & 0xFFFF # type: ignore
|
||||
# if nonzero, the topic of said conversation
|
||||
lparam_high = lParam >> 16 # type: ignore
|
||||
# --- Decode the 64-bit lParam into LOWORD/HIWORD safely ---
|
||||
# These helper functions handle pointer-sized values correctly on 64-bit builds
|
||||
lparam_low = win32api.LOWORD(lParam)
|
||||
lparam_high = win32api.HIWORD(lParam)
|
||||
|
||||
# if either of the words are nonzero, they contain
|
||||
# atoms https://docs.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables
|
||||
# which we can read out as shown below, and then compare.
|
||||
service = create_unicode_buffer(256)
|
||||
topic = create_unicode_buffer(256)
|
||||
|
||||
target_is_valid = lparam_low == 0 or (
|
||||
GlobalGetAtomNameW(lparam_low, service, 256) and service.value == appname
|
||||
)
|
||||
|
||||
topic_is_valid = lparam_high == 0 or (
|
||||
GlobalGetAtomNameW(lparam_high, topic, 256) and topic.value.lower() == 'system'
|
||||
)
|
||||
|
||||
if target_is_valid and topic_is_valid:
|
||||
# if everything is happy, send an acknowledgement of the DDE request
|
||||
SendMessageW(
|
||||
wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, GlobalAddAtomW(appname), GlobalAddAtomW('System'))
|
||||
# --- Retrieve atom names ---
|
||||
target_is_valid = lparam_low == 0 or (
|
||||
GlobalGetAtomNameW(lparam_low, service, 256)
|
||||
and service.value == appname
|
||||
)
|
||||
|
||||
# It works as a constructor as per <https://docs.python.org/3/library/ctypes.html#fundamental-data-types>
|
||||
return c_long(0)
|
||||
topic_is_valid = lparam_high == 0 or (
|
||||
GlobalGetAtomNameW(lparam_high, topic, 256)
|
||||
and topic.value.lower() == "system"
|
||||
)
|
||||
|
||||
return c_long(1) # This is an utter guess -Ath
|
||||
if target_is_valid and topic_is_valid:
|
||||
# Send acknowledgment of successful DDE handshake
|
||||
SendMessageW(
|
||||
wParam,
|
||||
WM_DDE_ACK,
|
||||
hwnd,
|
||||
PackDDElParam(
|
||||
WM_DDE_ACK, GlobalAddAtomW(appname), GlobalAddAtomW("System")
|
||||
),
|
||||
)
|
||||
return 0 # Success
|
||||
|
||||
return 1 # Not handled / invalid DDE initiation
|
||||
|
||||
except Exception as e:
|
||||
# Log unexpected exceptions but don’t crash the message loop
|
||||
logger.error(f"WndProc error: {e}", exc_info=True)
|
||||
return win32gui.DefWindowProc(hwnd, message, wParam, lParam)
|
||||
|
||||
class WindowsProtocolHandler(GenericProtocolHandler):
|
||||
"""
|
||||
@@ -199,10 +227,10 @@ if (config.auth_force_edmc_protocol # noqa: C901
|
||||
super().__init__()
|
||||
self.thread: threading.Thread | None = None
|
||||
|
||||
def start(self, master: 'tkinter.Tk') -> None:
|
||||
def start(self, master: tkinter.Tk) -> None:
|
||||
"""Start the DDE thread."""
|
||||
super().start(master)
|
||||
self.thread = threading.Thread(target=self.worker, name='DDE worker')
|
||||
self.thread = threading.Thread(target=self.worker, name="DDE worker")
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
@@ -226,41 +254,33 @@ if (config.auth_force_edmc_protocol # noqa: C901
|
||||
wndclass.hCursor = None
|
||||
wndclass.hbrBackground = None
|
||||
wndclass.lpszMenuName = None
|
||||
wndclass.lpszClassName = 'DDEServer'
|
||||
wndclass.lpszClassName = "DDEServer"
|
||||
|
||||
if not RegisterClassW(byref(wndclass)):
|
||||
print('Failed to register Dynamic Data Exchange for cAPI')
|
||||
print("Failed to register Dynamic Data Exchange for cAPI")
|
||||
return
|
||||
|
||||
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
|
||||
hwnd = win32gui.CreateWindowEx(
|
||||
0, # dwExStyle
|
||||
'DDEServer', # lpClassName (use string directly as win32gui expects it)
|
||||
"DDE Server", # lpWindowName
|
||||
0, # dwStyle
|
||||
win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT,
|
||||
win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, # X, Y, nWidth, nHeight
|
||||
0, # dwExStyle
|
||||
"DDEServer", # lpClassName (use string directly as win32gui expects it)
|
||||
"DDE Server", # lpWindowName
|
||||
0, # dwStyle
|
||||
win32con.CW_USEDEFAULT,
|
||||
win32con.CW_USEDEFAULT,
|
||||
win32con.CW_USEDEFAULT,
|
||||
win32con.CW_USEDEFAULT, # X, Y, nWidth, nHeight
|
||||
self.master.winfo_id(), # hWndParent
|
||||
0, # hMenu (use 0 instead of None for win32gui)
|
||||
wndclass.hInstance, # hInstance
|
||||
None # lpParam
|
||||
0, # hMenu (use 0 instead of None for win32gui)
|
||||
wndclass.hInstance, # hInstance
|
||||
None, # lpParam
|
||||
)
|
||||
|
||||
msg = MSG()
|
||||
# Calls GetMessageW: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew
|
||||
# Something is off with the return from this, meaning you'll see:
|
||||
# Exception ignored on converting result of ctypes callback function: <function WndProc
|
||||
# at 0x000001F5B8738FE0>
|
||||
# Traceback (most recent call last):
|
||||
# File "C:\Users\Athan\Documents\Devel\EDMarketConnector\protocol.py", line 323, in worker
|
||||
# while int(GetMessageW(byref(msg), None, 0, 0)) != 0:
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
# TypeError: 'c_long' object cannot be interpreted as an integer
|
||||
#
|
||||
# But it does actually work. Either getting a non-0 value and
|
||||
# entering the loop, or getting 0 and exiting it.
|
||||
while GetMessageW(byref(msg), None, 0, 0) != 0:
|
||||
logger.trace_if('frontier-auth.windows', f'DDE message of type: {msg.message}')
|
||||
logger.trace_if(
|
||||
"frontier-auth.windows", f"DDE message of type: {msg.message}"
|
||||
)
|
||||
if msg.message == WM_DDE_EXECUTE:
|
||||
# GlobalLock does some sort of "please dont move this?"
|
||||
# https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock
|
||||
@@ -268,28 +288,41 @@ if (config.auth_force_edmc_protocol # noqa: C901
|
||||
GlobalUnlock(msg.lParam) # Unlocks the GlobalLock-ed object
|
||||
|
||||
if args.lower().startswith('open("') and args.endswith('")'):
|
||||
logger.trace_if('frontier-auth.windows', f'args are: {args}')
|
||||
logger.trace_if("frontier-auth.windows", f"args are: {args}")
|
||||
url = parse.unquote(args[6:-2]).strip()
|
||||
if url.startswith(self.redirect):
|
||||
logger.debug(f'Message starts with {self.redirect}')
|
||||
logger.debug(f"Message starts with {self.redirect}")
|
||||
self.event(url)
|
||||
|
||||
# Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE
|
||||
win32gui.SetForegroundWindow(win32gui.GetParent(self.master.winfo_id()))
|
||||
PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam))
|
||||
win32gui.SetForegroundWindow(
|
||||
win32gui.GetParent(self.master.winfo_id())
|
||||
)
|
||||
PostMessageW(
|
||||
msg.wParam,
|
||||
WM_DDE_ACK,
|
||||
hwnd,
|
||||
PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam),
|
||||
)
|
||||
|
||||
else:
|
||||
# Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE
|
||||
PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam))
|
||||
PostMessageW(
|
||||
msg.wParam,
|
||||
WM_DDE_ACK,
|
||||
hwnd,
|
||||
PackDDElParam(WM_DDE_ACK, 0, msg.lParam),
|
||||
)
|
||||
|
||||
elif msg.message == WM_DDE_TERMINATE:
|
||||
win32gui.PostMessage(msg.wParam, WM_DDE_TERMINATE, hwnd, 0)
|
||||
|
||||
else:
|
||||
TranslateMessage(byref(msg)) # "Translates virtual key messages into character messages" ???
|
||||
TranslateMessage(
|
||||
byref(msg)
|
||||
) # "Translates virtual key messages into character messages" ???
|
||||
DispatchMessageW(byref(msg))
|
||||
|
||||
|
||||
else: # Linux / Run from source
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
@@ -303,17 +336,17 @@ else: # Linux / Run from source
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.httpd = HTTPServer(('localhost', 0), HTTPRequestHandler)
|
||||
self.redirect = f'http://localhost:{self.httpd.server_port}/auth'
|
||||
self.httpd = HTTPServer(("localhost", 0), HTTPRequestHandler)
|
||||
self.redirect = f"http://localhost:{self.httpd.server_port}/auth"
|
||||
if not os.getenv("EDMC_NO_UI"):
|
||||
logger.info(f'Web server listening on {self.redirect}')
|
||||
logger.info(f"Web server listening on {self.redirect}")
|
||||
|
||||
self.thread: threading.Thread | None = None
|
||||
|
||||
def start(self, master: 'tkinter.Tk') -> None:
|
||||
def start(self, master: tkinter.Tk) -> None:
|
||||
"""Start the HTTP server thread."""
|
||||
GenericProtocolHandler.start(self, master)
|
||||
self.thread = threading.Thread(target=self.worker, name='OAuth worker')
|
||||
self.thread = threading.Thread(target=self.worker, name="OAuth worker")
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
@@ -321,20 +354,20 @@ else: # Linux / Run from source
|
||||
"""Shutdown the HTTP server thread."""
|
||||
thread = self.thread
|
||||
if thread:
|
||||
logger.debug('Thread')
|
||||
logger.debug("Thread")
|
||||
self.thread = None
|
||||
|
||||
if self.httpd:
|
||||
logger.info('Shutting down httpd')
|
||||
logger.info("Shutting down httpd")
|
||||
self.httpd.shutdown()
|
||||
|
||||
logger.info('Joining thread')
|
||||
logger.info("Joining thread")
|
||||
thread.join() # Wait for it to quit
|
||||
|
||||
else:
|
||||
logger.debug('No thread')
|
||||
logger.debug("No thread")
|
||||
|
||||
logger.debug('Done.')
|
||||
logger.debug("Done.")
|
||||
|
||||
def worker(self) -> None:
|
||||
"""HTTP Worker."""
|
||||
@@ -346,10 +379,12 @@ else: # Linux / Run from source
|
||||
|
||||
def parse(self) -> bool:
|
||||
"""Parse a request."""
|
||||
logger.trace_if('frontier-auth.http', f'Got message on path: {self.path}')
|
||||
logger.trace_if("frontier-auth.http", f"Got message on path: {self.path}")
|
||||
url = parse.unquote(self.path)
|
||||
if url.startswith('/auth'):
|
||||
logger.debug('Request starts with /auth, sending to protocolhandler.event()')
|
||||
if url.startswith("/auth"):
|
||||
logger.debug(
|
||||
"Request starts with /auth, sending to protocolhandler.event()"
|
||||
)
|
||||
protocolhandler.event(url)
|
||||
self.send_response(200)
|
||||
return True
|
||||
@@ -364,14 +399,14 @@ else: # Linux / Run from source
|
||||
def do_GET(self) -> None: # noqa: N802 # Required to override
|
||||
"""Handle GET Request."""
|
||||
if self.parse():
|
||||
self.send_header('Content-Type', 'text/html')
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(self._generate_auth_response().encode())
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_request(self, code: int | str = '-', size: int | str = '-') -> None:
|
||||
def log_request(self, code: int | str = "-", size: int | str = "-") -> None:
|
||||
"""Override to prevent logging."""
|
||||
|
||||
def _generate_auth_response(self) -> str:
|
||||
@@ -381,21 +416,21 @@ else: # Linux / Run from source
|
||||
:return: The HTML content of the authentication response.
|
||||
"""
|
||||
return (
|
||||
'<html>'
|
||||
'<head>'
|
||||
'<title>Authentication successful - Elite: Dangerous</title>'
|
||||
'<style>'
|
||||
"<html>"
|
||||
"<head>"
|
||||
"<title>Authentication successful - Elite: Dangerous</title>"
|
||||
"<style>"
|
||||
'body { background-color: #000; color: #fff; font-family: "Helvetica Neue", Arial, sans-serif; }'
|
||||
'h1 { text-align: center; margin-top: 100px; }'
|
||||
'p { text-align: center; }'
|
||||
'</style>'
|
||||
'</head>'
|
||||
'<body>'
|
||||
'<h1>Authentication successful</h1>'
|
||||
'<p>Thank you for authenticating.</p>'
|
||||
'<p>Please close this browser tab now.</p>'
|
||||
'</body>'
|
||||
'</html>'
|
||||
"h1 { text-align: center; margin-top: 100px; }"
|
||||
"p { text-align: center; }"
|
||||
"</style>"
|
||||
"</head>"
|
||||
"<body>"
|
||||
"<h1>Authentication successful</h1>"
|
||||
"<p>Thank you for authenticating.</p>"
|
||||
"<p>Please close this browser tab now.</p>"
|
||||
"</body>"
|
||||
"</html>"
|
||||
)
|
||||
|
||||
|
||||
@@ -405,9 +440,10 @@ def get_handler_impl() -> Type[GenericProtocolHandler]:
|
||||
|
||||
:return: An instantiatable GenericProtocolHandler
|
||||
"""
|
||||
if (
|
||||
(sys.platform == 'win32' and config.auth_force_edmc_protocol)
|
||||
or (getattr(sys, 'frozen', False) and not is_wine and not config.auth_force_localserver)
|
||||
if (sys.platform == "win32" and config.auth_force_edmc_protocol) or (
|
||||
getattr(sys, "frozen", False)
|
||||
and not is_wine
|
||||
and not config.auth_force_localserver
|
||||
):
|
||||
return WindowsProtocolHandler
|
||||
|
||||
@@ -415,4 +451,4 @@ def get_handler_impl() -> Type[GenericProtocolHandler]:
|
||||
|
||||
|
||||
# *late init* singleton
|
||||
protocolhandler: GenericProtocolHandler
|
||||
protocolhandler: GenericProtocolHandler = None # type: ignore
|
||||
+9
-10
@@ -4,39 +4,38 @@
|
||||
setuptools==80.9.0
|
||||
|
||||
# Static analysis tools
|
||||
flake8==7.2.0
|
||||
flake8==7.3.0
|
||||
flake8-annotations-coverage==0.0.6
|
||||
flake8-cognitive-complexity==0.1.0
|
||||
flake8-comprehensions==3.16.0
|
||||
flake8-comprehensions==3.17.0
|
||||
flake8-docstrings==1.7.0
|
||||
flake8-json==24.4.0
|
||||
flake8-noqa==1.4.0
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-use-fstring==1.4
|
||||
|
||||
mypy==1.18.2
|
||||
pep8-naming==0.15.1
|
||||
safety==3.6.2
|
||||
types-requests==2.32.0.20250328
|
||||
safety==3.7.0
|
||||
types-requests==2.32.4.20250913
|
||||
types-setuptools==80.9.0.20250822
|
||||
|
||||
# Code formatting tools
|
||||
autopep8==2.3.2
|
||||
|
||||
# Git pre-commit checking
|
||||
pre-commit==4.3.0
|
||||
pre-commit==4.4.0
|
||||
|
||||
# HTML changelogs
|
||||
mistune==3.1.3
|
||||
mistune==3.1.4
|
||||
|
||||
# Packaging
|
||||
# We only need py2exe on windows.
|
||||
py2exe==0.13.0.2; sys_platform == 'win32'
|
||||
py2exe==0.14.0.0; sys_platform == 'win32'
|
||||
|
||||
# Testing
|
||||
pytest==8.4.2
|
||||
pytest==9.0.0
|
||||
pytest-cov==7.0.0 # Pytest code coverage support
|
||||
coverage[toml]==7.11.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
coverage[toml]==7.11.2 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
|
||||
coverage-conditional-plugin==0.9.0
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -4,5 +4,5 @@ watchdog==6.0.0
|
||||
simplesystray==0.1.0; sys_platform == 'win32'
|
||||
semantic-version==2.10.0
|
||||
# For manipulating folder permissions and the like.
|
||||
pywin32==310; sys_platform == 'win32'
|
||||
psutil==7.1.2
|
||||
pywin32==311; sys_platform == 'win32'
|
||||
psutil==7.1.3
|
||||
+2
-1
@@ -5,6 +5,7 @@ Copyright (c) EDCD, All Rights Reserved
|
||||
Licensed under the GNU General Public License.
|
||||
See LICENSE file.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from edmc_data import ship_name_map
|
||||
|
||||
@@ -17,7 +18,7 @@ def ship_file_name(ship_name: str, ship_type: str) -> str:
|
||||
name = Path(name).with_suffix("").name
|
||||
|
||||
# Check if the name is a reserved filename
|
||||
if Path(name).is_reserved():
|
||||
if os.path.isreserved(name):
|
||||
name += "_"
|
||||
|
||||
return name.translate(
|
||||
|
||||
Reference in new issue
Block a user