"""Platform-specific paths and tool resolution for meshbay-node.""" import asyncio import os import shutil import sys from pathlib import Path # ── Console ────────────────────────────────────────────────────────────────── def force_utf8_stdio() -> None: """ Make stdout/stderr UTF-8. A Windows console is cp1252 by default, so any ``print()`` carrying a character outside it — the ``->`` arrows and em dashes the CLI help and messages are full of — raises UnicodeEncodeError and takes the command down with it. No effect where the streams are already UTF-8 or cannot be reconfigured. """ for stream in (sys.stdout, sys.stderr): try: stream.reconfigure(encoding="utf-8") except (AttributeError, ValueError, OSError): pass # ── Event loop ─────────────────────────────────────────────────────────────── def configure_event_loop() -> None: """ The daemon runs on Windows' default ProactorEventLoop: verified end to end (a live browser peer connecting, an index sync, a file download and an ffmpeg-transcoded video stream). aiortc only ever hangs on it in the *same-process loopback* the tests use, which the test suite handles on its own (repo-root conftest). Escape hatch, opt-in only: MESHBAY_NODE_EVENT_LOOP=selector switches to the SelectorEventLoop. That fixes aiortc-in-one-process but breaks ffmpeg (SelectorEventLoop cannot spawn subprocesses on Windows), so it is not the default and probably never should be. """ if sys.platform != "win32": return if os.environ.get("MESHBAY_NODE_EVENT_LOOP", "").lower() == "selector": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # ── Directories ────────────────────────────────────────────────────────────── def config_dir() -> Path: if sys.platform == "win32": return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "meshbay" return Path.home() / ".config" / "meshbay" def data_dir() -> Path: if sys.platform == "win32": return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "meshbay" / "data" return Path.home() / ".local" / "share" / "meshbay" def state_dir() -> Path: if sys.platform == "win32": return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "meshbay" / "state" return Path.home() / ".local" / "state" / "meshbay" # ── File permissions ───────────────────────────────────────────────────────── def chmod_private(path: Path, *, mode: int = 0o600) -> None: """Set restrictive permissions on a file. No-op on Windows (NTFS ignores mode bits).""" if sys.platform != "win32": path.chmod(mode) # ── Media tools ────────────────────────────────────────────────────────────── _ffmpeg_path: str = "ffmpeg" _ffprobe_path: str = "ffprobe" def check_media_tools( ffmpeg: str = "ffmpeg", ffprobe: str = "ffprobe", ) -> None: """Resolve ffmpeg/ffprobe at daemon startup. Raises RuntimeError if not found.""" global _ffmpeg_path, _ffprobe_path resolved = shutil.which(ffmpeg) if not resolved: raise RuntimeError( f"{ffmpeg!r} not found in PATH. " "Install ffmpeg or set [node] ffmpeg_path in node.toml." ) _ffmpeg_path = resolved resolved = shutil.which(ffprobe) if not resolved: raise RuntimeError( f"{ffprobe!r} not found in PATH. " "Install ffmpeg or set [node] ffprobe_path in node.toml." ) _ffprobe_path = resolved def ffmpeg_cmd() -> str: return _ffmpeg_path def ffprobe_cmd() -> str: return _ffprobe_path