mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: detect double-click launch on Windows and abort with info message (#570)
--------- Co-authored-by: Jens Bergmann <1742418+1cu@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
14a917a1c7
commit
b7882065b7
@@ -72,7 +72,7 @@ exe = EXE(pyz,
|
|||||||
analysis.binaries,
|
analysis.binaries,
|
||||||
analysis.datas,
|
analysis.datas,
|
||||||
# bootloader_ignore_signals = False,
|
# bootloader_ignore_signals = False,
|
||||||
# console = True,
|
console = True,
|
||||||
# hide_console = None,
|
# hide_console = None,
|
||||||
# disable_windowed_traceback = False,
|
# disable_windowed_traceback = False,
|
||||||
# debug = False,
|
# debug = False,
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ from gettext import gettext as _
|
|||||||
|
|
||||||
import kleinanzeigen_bot
|
import kleinanzeigen_bot
|
||||||
from kleinanzeigen_bot.utils.exceptions import CaptchaEncountered
|
from kleinanzeigen_bot.utils.exceptions import CaptchaEncountered
|
||||||
|
from kleinanzeigen_bot.utils.launch_mode_guard import ensure_not_launched_from_windows_explorer
|
||||||
from kleinanzeigen_bot.utils.misc import format_timedelta
|
from kleinanzeigen_bot.utils.misc import format_timedelta
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Refuse GUI/double-click launch on Windows
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
ensure_not_launched_from_windows_explorer()
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Main loop: run bot → if captcha → sleep → restart
|
# Main loop: run bot → if captcha → sleep → restart
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|||||||
82
src/kleinanzeigen_bot/utils/launch_mode_guard.py
Normal file
82
src/kleinanzeigen_bot/utils/launch_mode_guard.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
|
import ctypes, sys # isort: skip
|
||||||
|
|
||||||
|
from kleinanzeigen_bot.utils.i18n import get_current_locale
|
||||||
|
from kleinanzeigen_bot.utils.misc import is_frozen
|
||||||
|
|
||||||
|
|
||||||
|
def _is_launched_from_windows_explorer() -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if this process is the *only* one attached to the console,
|
||||||
|
i.e. the user started us by double-clicking in Windows Explorer.
|
||||||
|
"""
|
||||||
|
if not is_frozen():
|
||||||
|
return False # Only relevant when compiled exe
|
||||||
|
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return False # Only relevant on Windows
|
||||||
|
|
||||||
|
# Allocate small buffer for at most 3 PIDs
|
||||||
|
DWORD = ctypes.c_uint
|
||||||
|
pids = (DWORD * 3)()
|
||||||
|
n = int(ctypes.windll.kernel32.GetConsoleProcessList(pids, 3))
|
||||||
|
return n <= 2 # our PID (+ maybe conhost.exe) -> console dies with us # noqa: PLR2004 # Magic value used in comparison
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_not_launched_from_windows_explorer() -> None:
|
||||||
|
"""
|
||||||
|
Terminates the application if the EXE was started by double-clicking in Windows Explorer
|
||||||
|
instead of from a terminal (cmd.exe / PowerShell).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not _is_launched_from_windows_explorer():
|
||||||
|
return
|
||||||
|
|
||||||
|
if get_current_locale().language == "de":
|
||||||
|
banner = (
|
||||||
|
"\n"
|
||||||
|
" ┌─────────────────────────────────────────────────────────────┐\n"
|
||||||
|
" │ Kleinanzeigen-Bot ist ein *Kommandozeilentool*. │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ Du hast das Programm scheinbar per Doppelklick gestartet. │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ -> Bitte starte es stattdessen in einem Terminal: │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ kleinanzeigen-bot.exe [OPTIONEN] │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ Schneller Weg, ein Terminal zu öffnen: │\n"
|
||||||
|
" │ 1. Drücke Win + R, gib cmd ein und drücke Enter. │\n"
|
||||||
|
" │ 2. Wechsle per `cd` in das Verzeichnis mit dieser Datei. │\n"
|
||||||
|
" │ 3. Gib den obigen Befehl ein und drücke Enter. │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │─────────────────────────────────────────────────────────────│\n"
|
||||||
|
" │ Drücke <Enter>, um dieses Fenster zu schließen. │\n"
|
||||||
|
" └─────────────────────────────────────────────────────────────┘\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
banner = (
|
||||||
|
"\n"
|
||||||
|
" ┌─────────────────────────────────────────────────────────────┐\n"
|
||||||
|
" │ Kleinanzeigen-Bot is a *command-line* tool. │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ It looks like you launched it by double-clicking the EXE. │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ -> Please run it from a terminal instead: │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ kleinanzeigen-bot.exe [OPTIONS] │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │ Quick way to open a terminal: │\n"
|
||||||
|
" │ 1. Press Win + R , type cmd and press Enter. │\n"
|
||||||
|
" │ 2. cd to the folder that contains this file. │\n"
|
||||||
|
" │ 3. Type the command above and press Enter. │\n"
|
||||||
|
" │ │\n"
|
||||||
|
" │─────────────────────────────────────────────────────────────│\n"
|
||||||
|
" │ Press <Enter> to close this window. │\n"
|
||||||
|
" └─────────────────────────────────────────────────────────────┘\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(banner, file = sys.stderr, flush = True)
|
||||||
|
input() # keep window open
|
||||||
|
sys.exit(1)
|
||||||
107
tests/unit/test_launch_mode_guard.py
Normal file
107
tests/unit/test_launch_mode_guard.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||||
|
|
||||||
|
import builtins, importlib, sys # isort: skip
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from kleinanzeigen_bot.utils.i18n import Locale
|
||||||
|
|
||||||
|
|
||||||
|
# --- Platform-specific test for Windows double-click guard ---
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("compiled_exe", "windows_double_click_launch", "expected_error_msg_lang"),
|
||||||
|
[
|
||||||
|
(True, True, "en"), # Windows Explorer double-click - English locale
|
||||||
|
(True, True, "de"), # Windows Explorer double-click - German locale
|
||||||
|
(True, False, None), # Windows Terminal launch - compiled exe
|
||||||
|
(False, False, None), # Windows Terminal launch - from source code
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.skipif(sys.platform != "win32", reason = "ctypes.windll only exists on Windows")
|
||||||
|
def test_guard_triggers_on_double_click_windows(
|
||||||
|
monkeypatch:pytest.MonkeyPatch,
|
||||||
|
capsys:pytest.CaptureFixture[str],
|
||||||
|
compiled_exe:bool,
|
||||||
|
windows_double_click_launch:bool | None,
|
||||||
|
expected_error_msg_lang:str | None
|
||||||
|
) -> None:
|
||||||
|
# Prevent blocking in tests
|
||||||
|
monkeypatch.setattr(builtins, "input", lambda: None)
|
||||||
|
|
||||||
|
# Simulate target platform
|
||||||
|
monkeypatch.setattr(sys, "platform", "win32")
|
||||||
|
|
||||||
|
# Simulate compiled executable
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"kleinanzeigen_bot.utils.misc.is_frozen",
|
||||||
|
lambda: compiled_exe,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Force specific locale
|
||||||
|
if expected_error_msg_lang:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"kleinanzeigen_bot.utils.i18n.get_current_locale",
|
||||||
|
lambda: Locale(expected_error_msg_lang),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spy on sys.exit
|
||||||
|
exit_mock = mock.Mock(wraps = sys.exit)
|
||||||
|
monkeypatch.setattr(sys, "exit", exit_mock)
|
||||||
|
|
||||||
|
# Simulate double-click launch on Windows
|
||||||
|
if windows_double_click_launch is not None:
|
||||||
|
pid_count = 2 if windows_double_click_launch else 3 # 2 -> Explorer, 3 -> Terminal
|
||||||
|
k32 = mock.Mock()
|
||||||
|
k32.GetConsoleProcessList.return_value = pid_count
|
||||||
|
monkeypatch.setattr("ctypes.windll.kernel32", k32)
|
||||||
|
|
||||||
|
# Reload module to pick up system monkeypatches
|
||||||
|
guard = importlib.reload(
|
||||||
|
importlib.import_module("kleinanzeigen_bot.utils.launch_mode_guard")
|
||||||
|
)
|
||||||
|
|
||||||
|
if expected_error_msg_lang:
|
||||||
|
with pytest.raises(SystemExit) as exc:
|
||||||
|
guard.ensure_not_launched_from_windows_explorer()
|
||||||
|
assert exc.value.code == 1
|
||||||
|
exit_mock.assert_called_once_with(1)
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
if expected_error_msg_lang == "de":
|
||||||
|
assert "Du hast das Programm scheinbar per Doppelklick gestartet." in captured.err
|
||||||
|
else:
|
||||||
|
assert "It looks like you launched it by double-clicking the EXE." in captured.err
|
||||||
|
assert not captured.out # nothing to stdout
|
||||||
|
else:
|
||||||
|
guard.ensure_not_launched_from_windows_explorer()
|
||||||
|
exit_mock.assert_not_called()
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert not captured.err # nothing to stderr
|
||||||
|
|
||||||
|
|
||||||
|
# --- Platform-agnostic tests for non-Windows and non-frozen code paths ---
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("platform", "compiled_exe"),
|
||||||
|
[
|
||||||
|
("linux", True),
|
||||||
|
("linux", False),
|
||||||
|
("darwin", True),
|
||||||
|
("darwin", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_guard_non_windows_and_non_frozen(
|
||||||
|
monkeypatch:pytest.MonkeyPatch,
|
||||||
|
platform:str,
|
||||||
|
compiled_exe:bool
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(sys, "platform", platform)
|
||||||
|
monkeypatch.setattr("kleinanzeigen_bot.utils.misc.is_frozen", lambda: compiled_exe)
|
||||||
|
# Reload module to pick up system monkeypatches
|
||||||
|
guard = importlib.reload(
|
||||||
|
importlib.import_module("kleinanzeigen_bot.utils.launch_mode_guard")
|
||||||
|
)
|
||||||
|
# Should not raise or print anything
|
||||||
|
guard.ensure_not_launched_from_windows_explorer()
|
||||||
Reference in New Issue
Block a user