Make the app cross-platform and add a Windows build
- paths: per-platform detection behind an injectable Env. Windows finds Player.log in the user profile and the install via Steam (registry), the standalone installer, Epic, or the uninstall registry. App dirs come from platformdirs (unchanged XDG locations on Linux). - logtail: open Player.log only while reading, so Windows can still rotate it when MTGA restarts. - carddb: on Windows read a private copy of the card database so MTGA's updater can replace the original. - App icon (SVG source + generated ICO/PNG), Windows taskbar identity, and a Windows-safe timer glyph. - PyInstaller spec, launcher and PowerShell build script producing a one-folder windowed build and zip. - Tests for path detection, the log follower and the card-DB copy; README covers Windows usage, building and per-OS locations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
171fc64ca9
commit
e82f8d1858
19 files changed
+685
-54
No files matched your search
@@ -0,0 +1,75 @@
|
||||
"""The log follower: chunked appends, partial lines, MTGA replacing the log, and no lingering handle."""
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
|
||||
from mtga_assistant.logtail import LogFollower
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "bo3_match.log"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def app():
|
||||
return QCoreApplication.instance() or QCoreApplication([])
|
||||
|
||||
|
||||
def wait_for(app, cond, timeout=5.0):
|
||||
end = time.monotonic() + timeout
|
||||
while time.monotonic() < end:
|
||||
app.processEvents()
|
||||
if cond():
|
||||
return True
|
||||
time.sleep(0.02)
|
||||
return False
|
||||
|
||||
|
||||
def open_handles_to(path: Path) -> int:
|
||||
"""Number of this process's file descriptors pointing at path (Linux /proc)."""
|
||||
fd_dir = Path("/proc/self/fd")
|
||||
if not fd_dir.exists():
|
||||
pytest.skip("needs /proc")
|
||||
count = 0
|
||||
for fd in fd_dir.iterdir():
|
||||
try:
|
||||
if Path(os.readlink(fd)) == path:
|
||||
count += 1
|
||||
except OSError:
|
||||
pass
|
||||
return count
|
||||
|
||||
|
||||
def test_follow_append_replace_and_no_open_handle(app, tmp_path):
|
||||
src = FIXTURE.read_bytes()
|
||||
log = tmp_path / "Player.log"
|
||||
log.write_bytes(b"")
|
||||
got, resets = [], []
|
||||
f = LogFollower(log)
|
||||
f.events.connect(lambda evs: got.extend(evs))
|
||||
f.reset.connect(lambda: resets.append(1))
|
||||
f.start()
|
||||
try:
|
||||
cut = len(src) * 3 // 4 # mid-line
|
||||
with open(log, "ab") as fh:
|
||||
fh.write(src[:cut])
|
||||
assert wait_for(app, lambda: len(got) >= 11)
|
||||
with open(log, "ab") as fh:
|
||||
fh.write(src[cut:])
|
||||
assert wait_for(app, lambda: len(got) == 22)
|
||||
|
||||
# Between polls the follower must not keep Player.log open (Windows would then block
|
||||
# MTGA from renaming it).
|
||||
time.sleep(0.3)
|
||||
assert open_handles_to(log.resolve()) == 0
|
||||
|
||||
# MTGA restarts: Player.log is replaced by a new file.
|
||||
new = tmp_path / "Player.new"
|
||||
new.write_bytes(src[:cut])
|
||||
os.replace(new, log)
|
||||
assert wait_for(app, lambda: resets and len(got) >= 33)
|
||||
finally:
|
||||
f.stop()
|
||||
f.wait(3000)
|
||||
Reference in new issue
Block a user