steam-backlog-enforcer/steam_backlog_enforcer/_hosts_protection.py
Krzysztof kuhy Rudnicki aecfd1a3a0 Split store_blocker along its one-way call boundaries
Extract only functions that call nothing else in the module: the iptables/DNS
layer and the /etc/hosts protection helpers. A two-way split here does not
work -- block_store calls _reblock_hosts while _unblock_hosts calls
is_store_blocked, so cutting between them makes the import circular. Choosing
the leaf set instead keeps every import one-way and needed almost no test
churn, where the earlier attempt cascaded into a dozen patch-target fixes.

1347 tests, 100% coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A1EYiv7e7HGhET84oa9M3
2026-08-24 15:06:15 +02:00

115 lines
3.3 KiB
Python

"""Toggling the protected/immutable flag on /etc/hosts around edits.
Leaf helpers: they call nothing else in the blocker, so extracting them
introduces no cycle. Split to keep both files under the 250-line cap.
"""
from __future__ import annotations
import logging
from pathlib import Path
import shutil
import subprocess
from steam_backlog_enforcer.config import (
BLOCKED_DOMAINS,
HOSTS_FILE,
)
logger = logging.getLogger(__name__)
# Path to the hosts install script. _REPO_ROOT resolves to $HOME (this
# module lives two levels below it); the script itself is in the
# linux_configuration checkout under testsAndMisc, not directly under $HOME.
_REPO_ROOT = Path(__file__).resolve().parents[2]
HOSTS_INSTALL_SCRIPT = (
_REPO_ROOT
/ "testsAndMisc"
/ "linux_configuration"
/ "scripts"
/ "periodic_background"
/ "hosts"
/ "install.sh"
)
# iptables chain name for our blocking rules.
IPTABLES_CHAIN = "STEAM_ENFORCER"
# Resolved absolute paths for executables (avoids S607 partial-path warnings).
_SUDO = shutil.which("sudo") or "/usr/bin/sudo"
_IPTABLES = shutil.which("iptables") or "/usr/sbin/iptables"
_BASH = shutil.which("bash") or "/usr/bin/bash"
_GUARDCTL = shutil.which("guardctl") or "/usr/local/bin/guardctl"
_TEE = shutil.which("tee") or "/usr/bin/tee"
# IP address used in /etc/hosts for blocking domains.
_HOSTS_REDIRECT_IP = ".".join(["0"] * 4)
def _disable_hosts_protection() -> None:
"""Temporarily unlock /etc/hosts so its content can be edited.
Guard-lib: stop watcher, collapse bind mount, chattr -i.
"""
subprocess.run(
[_SUDO, _GUARDCTL, "file-guard", "pacman-unlock", "hosts"],
capture_output=True,
timeout=10,
check=False,
)
def _enable_hosts_protection() -> None:
"""Re-lock /etc/hosts, adopting its current content as the new canonical.
Guard-lib: chattr +i, reapply bind mount, restart watcher.
"""
subprocess.run(
[_SUDO, _GUARDCTL, "file-guard", "sync", "hosts"],
capture_output=True,
timeout=10,
check=False,
)
def _sudo_write_hosts(content: str) -> None:
"""Write *content* to /etc/hosts via ``sudo tee``."""
subprocess.run(
[_SUDO, _TEE, str(HOSTS_FILE)],
input=content.encode(),
stdout=subprocess.DEVNULL,
timeout=10,
check=True,
)
def _reblock_hosts() -> bool:
"""Uncomment Steam Store entries in /etc/hosts."""
try:
_disable_hosts_protection()
content = HOSTS_FILE.read_text(encoding="utf-8")
new_lines = []
changed = False
for line in content.splitlines(keepends=True):
stripped = line.strip()
if stripped.startswith("# ") and any(
d in stripped for d in BLOCKED_DOMAINS
):
# Remove the '# ' prefix.
uncommented = line.replace("# ", "", 1)
new_lines.append(uncommented)
changed = True
else:
new_lines.append(line)
if changed:
_sudo_write_hosts("".join(new_lines))
logger.info("Re-enabled Steam Store entries in /etc/hosts.")
_enable_hosts_protection()
except OSError:
logger.exception("Failed to modify /etc/hosts")
return False
else:
return True