Adresy a data podle systému, ne podle Linuxu
Než začneme rozdávat binárky pro macOS a Windows, musí se na nich chovat rozumně to, co dosud počítalo s Linuxem. Adresy rozhraní se hledaly linuxovým ioctl. Na Windows chybí fcntl, na macOS to číslo znamená něco jiného, takže obojí propadlo až na poslední zálohu přes hostname — a ta na macOS vrátí jednu adresu. Tabule by tam nabídla jedinou volbu, přesně to, co jsme minule opravovali. Přibyla mezizáloha přes ifconfig; spouští se jen tam, kde ioctl nic nenašel, takže na Linuxu se neplatí nic. Parsování je zvlášť jako čistá funkce, ať jde otestovat i bez macOS pod rukama. Databáze z binárky mířila do XDG adresáře na všech systémech. Na Windows by to znamenalo C:\Users\...\.local\share\, což tam nikdo nečeká. Teď se volí podle systému: LOCALAPPDATA, Library/Application Support, XDG. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
298919d71b
commit
26338bfa29
4 files changed
+118
-5
No files matched your search
@@ -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))
|
||||
|
||||
|
||||
+13
-3
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
|
||||
\toptions=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
|
||||
\tinet 127.0.0.1 netmask 0xff000000
|
||||
\tinet6 ::1 prefixlen 128
|
||||
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
|
||||
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> 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<UP,POINTOPOINT,RUNNING,MULTICAST> 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"
|
||||
@@ -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"))
|
||||
|
||||
Reference in new issue
Block a user