from __future__ import annotations import os import sys import subprocess import atexit from pathlib import Path from typing import Optional from SYS.config import load_config from SYS.logger import debug, log _zt_server_proc: Optional[subprocess.Popen] = None _zt_server_last_config: Optional[str] = None # We no longer use atexit here because explicit lifecycle management # is preferred in TUI/REPL, and background servers use a monitor thread # to shut down when the parent dies. # atexit.register(lambda: stop_zerotier_server()) def ensure_zerotier_server_running() -> None: """Check config and ensure the ZeroTier storage server is running if needed.""" global _zt_server_proc, _zt_server_last_config try: # Load config from the project root (where config.conf typically lives) repo_root = Path(__file__).resolve().parent.parent cfg = load_config(repo_root) except Exception: return zt_conf = cfg.get("networking", {}).get("zerotier", {}) serve_target = zt_conf.get("serve") port = zt_conf.get("port") or 999 api_key = zt_conf.get("api_key") # Config hash to detect changes config_id = f"{serve_target}|{port}|{api_key}" # Check if proc is still alive if _zt_server_proc: if _zt_server_proc.poll() is not None: # Process died debug("ZeroTier background server died. Restarting...") _zt_server_proc = None elif config_id == _zt_server_last_config: # Already running with correct config return # If config changed and we have a proc, stop it if _zt_server_proc and config_id != _zt_server_last_config: debug("ZeroTier server config changed. Stopping old process...") try: _zt_server_proc.terminate() _zt_server_proc.wait(timeout=2) except Exception: try: _zt_server_proc.kill() except Exception: pass _zt_server_proc = None _zt_server_last_config = config_id if not serve_target: return # Resolve path storage_path = None folders = cfg.get("store", {}).get("folder", {}) for name, block in folders.items(): if name.lower() == serve_target.lower(): storage_path = block.get("path") or block.get("PATH") break if not storage_path: # Fallback to direct path storage_path = serve_target if not storage_path or not Path(storage_path).exists(): debug(f"ZeroTier host target '{serve_target}' not found at {storage_path}. Cannot start server.") return repo_root = Path(__file__).resolve().parent.parent server_script = repo_root / "scripts" / "remote_storage_server.py" if not server_script.exists(): debug(f"ZeroTier server script not found at {server_script}") return # Use the same python executable that is currently running # On Windows, explicitly prefer the .venv python if it exists python_exe = sys.executable if sys.platform == "win32": venv_py = repo_root / ".venv" / "Scripts" / "python.exe" if venv_py.exists(): python_exe = str(venv_py) cmd = [python_exe, str(server_script), "--storage-path", str(storage_path), "--port", str(port), "--monitor"] if api_key: cmd += ["--api-key", str(api_key)] try: debug(f"Starting ZeroTier storage server: {cmd}") # Capture errors to a log file instead of DEVNULL log_file = repo_root / "zt_server_error.log" with open(log_file, "a") as f: f.write(f"\n--- Starting server at {__import__('datetime').datetime.now()} ---\n") f.write(f"Command: {' '.join(cmd)}\n") f.write(f"CWD: {repo_root}\n") f.write(f"Python: {python_exe}\n") err_f = open(log_file, "a") # On Windows, CREATE_NO_WINDOW = 0x08000000 ensures no console pops up import subprocess _zt_server_proc = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=err_f, cwd=str(repo_root), creationflags=0x08000000 if sys.platform == "win32" else 0 ) log(f"ZeroTier background server started on port {port} (sharing {serve_target})") except Exception as e: debug(f"Failed to start ZeroTier server: {e}") _zt_server_proc = None def stop_zerotier_server() -> None: """Stop the background server if it is running.""" global _zt_server_proc if _zt_server_proc: try: _zt_server_proc.terminate() _zt_server_proc.wait(timeout=2) except Exception: try: _zt_server_proc.kill() except Exception: pass _zt_server_proc = None