diff --git a/net.py b/net.py index bc001d6..03ec7e5 100644 --- a/net.py +++ b/net.py @@ -11,7 +11,9 @@ adresy a obecný typ rozhraní, ne to, co má kdo zrovna nainstalované. import ipaddress import os +import re import socket +import subprocess from dataclasses import dataclass # Rozsah, ze kterého adresy přiděluje Tailscale (RFC 6598, CGNAT). @@ -123,8 +125,45 @@ def _from_interfaces() -> list[tuple[str, str]]: return found +def parse_ifconfig(text: str) -> list[tuple[str, str]]: + """Rozhraní a jejich IPv4 adresy z výstupu `ifconfig`. + + Oddělená čistá funkce, ať se dá otestovat i bez macOS pod rukama. + """ + found = [] + name = "" + + for line in text.splitlines(): + head = re.match(r"(\S+):", line) + if head: + name = head.group(1) + continue + + addr = re.search(r"\binet (\d+\.\d+\.\d+\.\d+)", line) + if addr: + found.append((name, addr.group(1))) + + return found + + +def _from_ifconfig() -> list[tuple[str, str]]: + """Adresy přes `ifconfig`. Záloha pro macOS, kde ioctl výše nesedí. + + Spouští se jen tam, kde předchozí cesta nic nenašla, takže na Linuxu + se neplatí nic. Trvá jednotky milisekund, cachovat to nemá cenu. + """ + try: + done = subprocess.run(["ifconfig", "-a"], capture_output=True, + text=True, timeout=5, check=True) + except (OSError, subprocess.SubprocessError): + return [] + + return parse_ifconfig(done.stdout) + + def _from_hostname() -> list[tuple[str, str]]: - """Záloha pro systémy, kde předchozí cesta nefunguje.""" + """Poslední záloha. Na Windows vrátí adresy všech rozhraní, jinde + obvykle jen jednu — jména rozhraní se takhle nedozvíme.""" try: infos = socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET) except OSError: @@ -144,7 +183,7 @@ def addresses() -> list[Address]: primary = default_route_address() - found = _from_interfaces() or _from_hostname() + found = _from_interfaces() or _from_ifconfig() or _from_hostname() if primary and primary not in [ip for _name, ip in found]: found.append(("", primary)) diff --git a/storage.py b/storage.py index 35cce6e..80eca90 100644 --- a/storage.py +++ b/storage.py @@ -16,20 +16,30 @@ from pathlib import Path from core import FINAL_SCORE, Game, Turn +def data_home() -> Path: + """Adresář, kam podle zvyklostí systému patří data aplikací.""" + if sys.platform == "win32": + local = os.environ.get("LOCALAPPDATA") + return Path(local) if local else Path.home() / "AppData" / "Local" + if sys.platform == "darwin": + return Path.home() / "Library" / "Application Support" + return Path(os.environ.get("XDG_DATA_HOME") + or Path.home() / ".local" / "share") + + def default_db() -> Path: """Kam se ukládá databáze. Ze zdrojáků do pracovního adresáře — dá se tak mít víc rozehraných sad vedle sebe a je vidět, s čím se pracuje. Z binárky do datového - adresáře podle XDG: binárka se spouští odkudkoli a rozbaluje se do + adresáře podle systému: binárka se spouští odkudkoli a rozbaluje se do dočasného adresáře, takže relativní cesta by databázi rozsypala po disku. """ chosen = os.environ.get("DICE_DB") if chosen: return Path(chosen) if getattr(sys, "frozen", False): - share = os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share" - return Path(share) / "kostky" / "dice.db" + return data_home() / "kostky" / "dice.db" return Path("dice.db") diff --git a/tests/test_net.py b/tests/test_net.py index 8096162..a326839 100644 --- a/tests/test_net.py +++ b/tests/test_net.py @@ -90,3 +90,50 @@ def test_v_systemu_se_neco_najde(): assert all(net.classify(a.ip, a.interface) for a in found) assert len({a.ip for a in found}) == len(found) + + +IFCONFIG_MACOS = """\ +lo0: flags=8049 mtu 16384 +\toptions=1203 +\tinet 127.0.0.1 netmask 0xff000000 +\tinet6 ::1 prefixlen 128 +gif0: flags=8010 mtu 1280 +en0: flags=8863 mtu 1500 +\tether 3c:22:fb:aa:bb:cc +\tinet6 fe80::14b0:6b1a:9e2f:1a4c%en0 prefixlen 64 secured scopeid 0xb +\tinet 192.168.0.42 netmask 0xffffff00 broadcast 192.168.0.255 +\tmedia: autoselect +\tstatus: active +utun4: flags=8051 mtu 1280 +\tinet 100.99.98.97 --> 100.99.98.97 netmask 0xff000000 +""" + + +def test_ifconfig_vytahne_rozhrani_a_adresy(): + """macOS na ioctl z Linuxu neslyší, tak se čte výstup ifconfigu.""" + assert net.parse_ifconfig(IFCONFIG_MACOS) == [ + ("lo0", "127.0.0.1"), + ("en0", "192.168.0.42"), + ("utun4", "100.99.98.97"), + ] + + +def test_ifconfig_si_neplete_inet6_s_inet(): + """`inet6` začíná stejně jako `inet`; IPv6 adresy tudy projít nesmí.""" + assert net.parse_ifconfig("en0: flags=1\n\tinet6 fe80::1 prefixlen 64\n") == [] + + +def test_adresy_z_ifconfigu_projdou_celym_retezem(monkeypatch): + """Bez linuxového ioctl se sáhne po ifconfigu, teprve pak po hostname.""" + monkeypatch.delenv("DICE_HOST", raising=False) + monkeypatch.setattr(net, "_from_interfaces", lambda: []) + monkeypatch.setattr(net, "_from_ifconfig", + lambda: net.parse_ifconfig(IFCONFIG_MACOS)) + monkeypatch.setattr(net, "_from_hostname", lambda: pytest.fail("moc brzy")) + monkeypatch.setattr(net, "default_route_address", lambda: "192.168.0.42") + + found = net.addresses() + + assert [a.ip for a in found] == ["192.168.0.42", "100.99.98.97"] + assert found[0].primary and found[0].interface == "en0" + assert found[1].kind == "tailnet" diff --git a/tests/test_storage.py b/tests/test_storage.py index 603d8df..a07e2c6 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -148,11 +148,28 @@ def test_umisteni_databaze_z_binarky(monkeypatch, tmp_path): """Binárka se rozbaluje do dočasného adresáře, databáze tam nesmí.""" monkeypatch.delenv("DICE_DB", raising=False) monkeypatch.setattr(storage.sys, "frozen", True, raising=False) + monkeypatch.setattr(storage.sys, "platform", "linux") monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) assert storage.default_db() == tmp_path / "kostky" / "dice.db" +def test_data_home_podle_systemu(monkeypatch, tmp_path): + """Každý systém má svůj adresář na data; XDG je jen ten linuxový.""" + monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "AppData" / "Local")) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + + monkeypatch.setattr(storage.sys, "platform", "win32") + assert storage.data_home() == tmp_path / "AppData" / "Local" + + monkeypatch.setattr(storage.sys, "platform", "darwin") + assert storage.data_home() == tmp_path / "Library" / "Application Support" + + monkeypatch.setattr(storage.sys, "platform", "linux") + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + assert storage.data_home() == tmp_path / ".local" / "share" + + def test_dice_db_prebije_oboje(monkeypatch, tmp_path): monkeypatch.setattr(storage.sys, "frozen", True, raising=False) monkeypatch.setenv("DICE_DB", str(tmp_path / "jinde.db"))