Feature: Wrapper, Logging und Doku überarbeitet

- Debug-Logging optional gemacht, den Legacy-Wrapper portabler aufgebaut (relative Pfade, Debug-Weitergabe, Python-Fallback) und die README für Endanwender und Entwickler klarer neu strukturiert.
This commit is contained in:
Andre
2026-04-18 17:10:23 +02:00
parent a00bd1ed30
commit d91735a2fa
5 changed files with 242 additions and 94 deletions
+35 -21
View File
@@ -26,11 +26,9 @@ import win32process
from pathlib import Path
_LOG_PATH = Path(__file__).resolve().parent / "focusfix.log"
logging.basicConfig(
filename=str(_LOG_PATH),
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
)
_LOGGER = logging.getLogger("focusfix")
_LOGGER.addHandler(logging.NullHandler())
_LOGGER.propagate = False
# Minimale Fensterbreite in Pixeln, um den Splashscreen auszuschließen.
MIN_WINDOW_WIDTH = 800
@@ -46,8 +44,21 @@ def derive_process_name(command: list[str]) -> str:
return Path(command[0]).stem
def configure_logging(enabled: bool, log_path: Path = _LOG_PATH) -> None:
_LOGGER.handlers.clear()
if not enabled:
_LOGGER.addHandler(logging.NullHandler())
_LOGGER.setLevel(logging.CRITICAL + 1)
return
file_handler = logging.FileHandler(log_path, mode="w", encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
_LOGGER.addHandler(file_handler)
_LOGGER.setLevel(logging.DEBUG)
def launch_game(command: list[str]) -> subprocess.Popen:
logging.info("Launching game command: %r", command)
_LOGGER.info("Launching game command: %r", command)
return subprocess.Popen(command)
@@ -82,7 +93,7 @@ def find_game_window(pids: set[int]) -> int | None:
height = rect[3] - rect[1]
if width >= MIN_WINDOW_WIDTH:
title = win32gui.GetWindowText(hwnd)
logging.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid)
_LOGGER.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid)
found.append((hwnd, title, width, height))
return True
@@ -115,7 +126,7 @@ def wait_for_stable_size(hwnd: int, polls: int = 3, interval: float = 0.5,
stable_count = 0
last_size = size
time.sleep(interval)
logging.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)",
_LOGGER.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)",
hwnd, last_size[0], last_size[1], stable_count)
return last_size
@@ -154,11 +165,11 @@ def bring_to_front(hwnd: int) -> bool:
ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, False)
result = is_foreground(hwnd)
logging.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)",
result, win32gui.GetForegroundWindow(), hwnd)
_LOGGER.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)",
result, win32gui.GetForegroundWindow(), hwnd)
return result
except Exception as e:
logging.exception("bring_to_front failed: %s", e)
_LOGGER.exception("bring_to_front failed: %s", e)
return False
finally:
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
@@ -167,12 +178,15 @@ def bring_to_front(hwnd: int) -> bool:
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", "-debug", action="store_true",
help="Debug-Logging nach focusfix.log aktivieren")
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--process", type=str,
help="Prozessname des Spiels (ohne .exe)")
parser.add_argument("command", nargs="*",
help="Steam-Spielbefehl, z.B. %%command%%")
args, extra_command_args = parser.parse_known_args()
configure_logging(args.debug)
process_name = args.process
command = args.command + extra_command_args
@@ -187,13 +201,13 @@ def main() -> None:
try:
launch_game(command)
except OSError as exc:
logging.exception("Failed to launch game command: %s", exc)
_LOGGER.exception("Failed to launch game command: %s", exc)
sys.exit(1)
elif not process_name:
parser.error("Either --process or a game command is required.")
deadline = time.time() + args.timeout
logging.info("Monitoring process name: %s", process_name)
_LOGGER.info("Monitoring process name: %s", process_name)
# --- Phase 1: Warten auf den Prozess ---
pids: set[int] = set()
@@ -211,13 +225,13 @@ def main() -> None:
pids = find_process_pids(process_name)
hwnd = find_game_window(pids)
if hwnd:
logging.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd))
_LOGGER.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd))
# Warten bis das Spiel sein Fenster vollständig aufgebaut hat
time.sleep(3)
break
time.sleep(0.5)
else:
logging.error("Timeout waiting for game window")
_LOGGER.error("Timeout waiting for game window")
sys.exit(1)
# --- Phase 3: Spielfenster fokussieren und auf Fensterwechsel reagieren ---
@@ -227,7 +241,7 @@ def main() -> None:
for attempt in range(FOCUS_RETRIES):
pids = find_process_pids(process_name)
if not pids:
logging.warning("Game process gone, stopping")
_LOGGER.warning("Game process gone, stopping")
break
current_hwnd = find_game_window(pids)
@@ -237,10 +251,10 @@ def main() -> None:
old_title = win32gui.GetWindowText(hwnd) if hwnd else ""
# Nicht von einem Fenster mit Titel auf ein titelloses wechseln
if not new_title and old_title:
logging.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)",
_LOGGER.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)",
current_hwnd, hwnd, old_title)
else:
logging.info("Window changed: old=%s new=%s title=%r",
_LOGGER.info("Window changed: old=%s new=%s title=%r",
hwnd, current_hwnd, new_title)
hwnd = current_hwnd
focused_hwnd = None # neues Fenster muss neu fokussiert werden
@@ -258,15 +272,15 @@ def main() -> None:
# Warten bis Fenstergröße stabil ist (Resize abgeschlossen)
wait_for_stable_size(hwnd)
logging.info("Focus attempt %d/%d for hwnd=%s title=%r",
_LOGGER.info("Focus attempt %d/%d for hwnd=%s title=%r",
attempt + 1, FOCUS_RETRIES, hwnd, win32gui.GetWindowText(hwnd))
success = bring_to_front(hwnd)
if success:
logging.info("Focus succeeded on attempt %d", attempt + 1)
_LOGGER.info("Focus succeeded on attempt %d", attempt + 1)
focused_hwnd = hwnd
time.sleep(FOCUS_RETRY_DELAY)
logging.info("Done. Final focused hwnd=%s", focused_hwnd)
_LOGGER.info("Done. Final focused hwnd=%s", focused_hwnd)
if __name__ == "__main__":