zapretgui/tests/test_windivert_service_recovery.py
loop-uh 917aceab71
All checks were successful
Zapret source guards / validate (push) Successful in 7s
Драйвер WinDivert и запуск winws: служба Monkey больше не зависает — программа сама держала её незакрытым хэндлом (CloseServiceHandle без argtypes падал с «int too long to convert»); новый слой winws_runtime/engine на типизированном WinAPI: запуск подтверждается строкой windivert initialized из вывода winws, остановка — сигналом Windows о выходе процесса, без пауз «на глаз»; кнопка «Стоп», выход и обновление выгружают драйвер и убирают его службу, когда им никто не пользуется (кроме Kaspersky), перезапуск и смена пресета драйвер не трогают; останавливаются только winws из папки программы, чужая копия запрета не убивается; реестр службы, DeleteFlag и тип запуска больше не правятся, штатное «отключена и помечена на удаление» у работающего драйвера не считается поломкой; застрявшая служба называется пользователю с причиной; оркестратор переведён на те же подтверждения; вывод winws первой версии пишется в файл; сквозные проверки на живой Windows в tests/e2e_windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:34:45 +03:00

335 lines
14 KiB
Python

import unittest
from pathlib import Path
import tempfile
from unittest.mock import Mock, patch
class WinDivertServiceRecoveryTests(unittest.TestCase):
def test_packaged_monkey_driver_is_accepted_as_windivert_file(self) -> None:
from config.runtime_layout import ApplicationPaths
from winws_runtime.health import winws_exit_diagnosis
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
exe_dir = root / "exe"
exe_dir.mkdir()
(exe_dir / "WinDivert.dll").write_bytes(b"dll")
(exe_dir / "Monkey64.sys").write_bytes(b"driver")
with patch.object(
winws_exit_diagnosis,
"APPLICATION_PATHS",
ApplicationPaths.from_root(root),
):
self.assertEqual(winws_exit_diagnosis._check_windivert_files(), [])
(exe_dir / "Monkey64.sys").unlink()
with patch.object(
winws_exit_diagnosis,
"APPLICATION_PATHS",
ApplicationPaths.from_root(root),
):
missing = winws_exit_diagnosis._check_windivert_files()
self.assertEqual(
missing,
["драйвер WinDivert (Monkey64.sys или WinDivert64.sys)"],
)
def test_detailed_diagnosis_explains_windows_and_process_codes(self) -> None:
from winws_runtime.health.winws_exit_diagnosis import (
WinDivertDiagnosis,
format_winws_exit_diagnosis,
)
diagnosis = WinDivertDiagnosis(
cause="Служба драйвера WinDivert (Monkey) отключена в системе",
solution="Выполните аварийную очистку драйвера и повторите запуск",
exit_code=34,
win32_error=1058,
)
message = format_winws_exit_diagnosis(diagnosis, exe_name="winws2")
self.assertIn("Найдена причина", message)
self.assertIn("код ошибки Windows 1058", message)
self.assertIn("код завершения процесса 34", message)
self.assertIn("Что сделать", message)
def test_recv_errno_5_exit_229_is_not_reported_as_windows_error_229(self) -> None:
from winws_runtime.health.winws_exit_diagnosis import (
diagnose_winws_exit,
format_winws_exit_diagnosis,
)
output = "\n".join(
(
"github version v1.0.3 (b78b52c4) lua_compat_ver 6",
"windivert initialized. capture is started.",
"windivert: recv failed. errno 5",
)
)
diagnosis = diagnose_winws_exit(229, output)
self.assertIsNotNone(diagnosis)
self.assertIsNone(diagnosis.win32_error)
self.assertFalse(diagnosis.cause_is_exact)
message = format_winws_exit_diagnosis(diagnosis, exe_name="winws2")
self.assertIn("Что известно", message)
self.assertIn("windivert: recv failed. errno 5", message)
self.assertIn("код завершения процесса 229", message)
self.assertIn("потерял исходный код Windows", message)
self.assertIn("ERROR_IO_PENDING (997)", message)
self.assertNotIn("код ошибки Windows 229", message)
self.assertNotIn("Найдена причина", message)
def test_empty_exit_34_is_treated_as_transient_windivert_1058(self) -> None:
from winws_runtime.runners import runner_base
class DummyRunner(runner_base.StrategyRunnerBase):
def start_from_preset_file(self, preset_path: str, strategy_name: str = "Preset") -> bool:
return True
def switch_preset_file_fast(self, preset_path: str, strategy_name: str = "Preset", *, is_current=None) -> bool:
return True
with (
patch.object(runner_base.os.path, "exists", return_value=True),
patch(
"winws_runtime.health.winws_exit_diagnosis._probe_service_disabled_cause",
return_value=(
"WinDivert ещё не готов после предыдущего запуска или очистки",
"Повторите запуск после очистки",
None,
),
),
):
runner = DummyRunner(r"C:\Zapret\Dev\exe\winws2.exe")
retry = runner._should_retry_transient_windivert_service_error(
"",
34,
retry_count=0,
max_retry_count=1,
)
self.assertTrue(retry)
def test_windivert_error_after_lua_header_is_reported_as_service_problem(self) -> None:
from winws_runtime.health import process_health_check, winws_exit_diagnosis
stderr = "\n".join(
(
"github version v1.0.1 lua_compat_ver 6",
"Loading hostlist /lists/youtube.txt",
"windivert: error opening filter: The service cannot be started, either because it is disabled or because it has no enabled devices associated with it.",
)
)
with patch.object(
winws_exit_diagnosis,
"_probe_service_disabled_cause",
return_value=("WinDivert service disabled", "Restore service", None),
):
diagnosis = process_health_check.diagnose_winws_exit(87, stderr)
self.assertIsNotNone(diagnosis)
self.assertEqual(diagnosis.win32_error, 1058)
self.assertEqual(diagnosis.cause, "WinDivert service disabled")
def test_runner_runs_safe_windivert_autofix_once_after_failed_spawn(self) -> None:
from winws_runtime.health.process_health_check import WinDivertDiagnosis
from winws_runtime.runners import runner_base
class DummyRunner(runner_base.StrategyRunnerBase):
def start_from_preset_file(self, preset_path: str, strategy_name: str = "Preset") -> bool:
return True
def switch_preset_file_fast(self, preset_path: str, strategy_name: str = "Preset", *, is_current=None) -> bool:
return True
diagnosis = WinDivertDiagnosis(
cause="Служба Base Filtering Engine (BFE) отключена",
solution="Включите BFE",
auto_fix="enable_bfe",
exit_code=1068,
win32_error=1068,
)
with (
patch.object(runner_base.os.path, "exists", return_value=True),
patch.object(runner_base, "diagnose_winws_exit", return_value=diagnosis),
patch.object(runner_base, "execute_windivert_auto_fix", return_value=(True, "BFE запущена")) as auto_fix,
):
runner = DummyRunner(r"C:\Zapret\Dev\exe\winws2.exe")
recovered = runner._maybe_run_windivert_auto_fix_after_failed_spawn(
"dependency service failed",
1068,
retry_count=0,
)
self.assertTrue(recovered)
auto_fix.assert_called_once_with("enable_bfe")
def test_runner_does_not_autofix_same_failure_twice(self) -> None:
from winws_runtime.health.process_health_check import WinDivertDiagnosis
from winws_runtime.runners import runner_base
class DummyRunner(runner_base.StrategyRunnerBase):
def start_from_preset_file(self, preset_path: str, strategy_name: str = "Preset") -> bool:
return True
def switch_preset_file_fast(self, preset_path: str, strategy_name: str = "Preset", *, is_current=None) -> bool:
return True
diagnosis = WinDivertDiagnosis(
cause="Служба Base Filtering Engine (BFE) отключена",
solution="Включите BFE",
auto_fix="enable_bfe",
exit_code=1068,
win32_error=1068,
)
with (
patch.object(runner_base.os.path, "exists", return_value=True),
patch.object(runner_base, "diagnose_winws_exit", return_value=diagnosis),
patch.object(runner_base, "execute_windivert_auto_fix", return_value=(True, "BFE запущена")) as auto_fix,
):
runner = DummyRunner(r"C:\Zapret\Dev\exe\winws2.exe")
recovered = runner._maybe_run_windivert_auto_fix_after_failed_spawn(
"dependency service failed",
1068,
retry_count=1,
)
self.assertFalse(recovered)
auto_fix.assert_not_called()
def test_winws2_retries_after_successful_windivert_autofix(self) -> None:
from winws_runtime.runners.zapret2_runner import Winws2StrategyRunner
runner = object.__new__(Winws2StrategyRunner)
runner._last_spawn_exit_code = 1068
runner._last_spawn_stderr = "dependency service failed"
runner._should_retry_transient_windivert_service_error = Mock(return_value=False)
runner._is_windivert_system_error = Mock(return_value=True)
runner._is_windivert_conflict_error = Mock(return_value=False)
runner._maybe_run_windivert_auto_fix_after_failed_spawn = Mock(return_value=True)
runner._start_from_preset_file_locked = Mock(return_value=True)
retried = runner._maybe_retry_after_failed_spawn_locked(
"preset.txt",
"Preset",
cleanup_required=False,
retry_count=0,
stable_start_window_seconds=0.35,
)
self.assertTrue(retried)
runner._maybe_run_windivert_auto_fix_after_failed_spawn.assert_called_once_with(
"dependency service failed",
1068,
retry_count=0,
)
runner._start_from_preset_file_locked.assert_called_once_with(
"preset.txt",
"Preset",
force_cleanup=True,
retry_count=1,
stable_start_window_seconds=0.35,
)
def test_winws1_retries_after_successful_windivert_autofix(self) -> None:
from winws_runtime.runners.zapret1_runner import Winws1StrategyRunner
runner = object.__new__(Winws1StrategyRunner)
runner._last_spawn_exit_code = 1068
runner._last_spawn_stderr = "dependency service failed"
runner._should_retry_transient_windivert_service_error = Mock(return_value=False)
runner._should_retry_unclassified_code_one = Mock(return_value=False)
runner._is_windivert_system_error = Mock(return_value=True)
runner._is_windivert_conflict_error = Mock(return_value=False)
runner._maybe_run_windivert_auto_fix_after_failed_spawn = Mock(return_value=True)
runner._start_from_preset_file_locked = Mock(return_value=True)
retried = runner._maybe_retry_after_failed_spawn_locked(
"preset.txt",
"Preset",
retry_count=0,
max_retries=2,
stable_start_window_seconds=0.35,
)
self.assertTrue(retried)
runner._maybe_run_windivert_auto_fix_after_failed_spawn.assert_called_once_with(
"dependency service failed",
1068,
retry_count=0,
)
runner._start_from_preset_file_locked.assert_called_once_with(
"preset.txt",
"Preset",
retry_count=1,
max_retries=2,
stable_start_window_seconds=0.35,
)
def test_generic_service_disabled_does_not_blame_secure_boot_without_signature_error(self) -> None:
from winws_runtime.health import process_health_check, winws_exit_diagnosis
stderr = (
"windivert: error opening filter: The service cannot be started, "
"either because it is disabled or because it has no enabled devices associated with it."
)
with (
patch.object(winws_exit_diagnosis, "_check_windivert_files", return_value=[]),
patch.object(winws_exit_diagnosis, "_check_bfe_service", return_value=True),
patch.object(winws_exit_diagnosis, "_check_secure_boot", return_value=True),
patch.object(winws_exit_diagnosis, "_find_stuck_windivert_driver_service", return_value=None),
patch.object(winws_exit_diagnosis, "_detect_active_antivirus", return_value=None),
patch.object(winws_exit_diagnosis, "_check_network_adapters", return_value=True),
):
diagnosis = process_health_check.diagnose_winws_exit(34, stderr)
self.assertIsNotNone(diagnosis)
self.assertEqual(diagnosis.win32_error, 1058)
self.assertNotIn("Secure Boot блокирует", diagnosis.cause)
self.assertIn("WinDivert не может запустить службу драйвера", diagnosis.cause)
def test_lua_compat_mismatch_is_reported_as_version_mismatch(self) -> None:
from winws_runtime.health import process_health_check
stderr = "\n".join(
(
"github version v1.0.1 lua_compat_ver 6",
"Error: LUA ERROR: /lua/zapret-lib.lua:4: Incompatible NFQWS2_COMPAT_VER. Use pktws and lua scripts from the same release !",
)
)
diagnosis = process_health_check.diagnose_winws_exit(87, stderr)
self.assertIsNotNone(diagnosis)
self.assertEqual(diagnosis.cause, "winws2.exe и Lua-скрипты от разных версий")
self.assertIn("Обновите папку lua", diagnosis.solution)
def test_probe_uses_no_install_for_network_readiness(self) -> None:
from winws_runtime.runtime import system_ops
with (
patch.object(system_ops, "_load_windivert_dll_runtime", return_value=object()),
patch.object(
system_ops,
"_probe_windivert_open_runtime",
side_effect=[(False, 1060), (False, 1060)],
) as open_probe,
):
result = system_ops.probe_windivert_state_runtime()
self.assertFalse(result.installed)
self.assertFalse(result.ready)
network_flags = open_probe.call_args_list[1].kwargs["flags"]
self.assertTrue(network_flags & system_ops._WINDIVERT_FLAG_NO_INSTALL)
if __name__ == "__main__":
unittest.main()