import hashlib import os import pathlib import re import socket from typing import Optional from .constants import HTTPS_PORTS def ensure_dir(path: pathlib.Path) -> None: path.mkdir(parents=True, exist_ok=True) def sha256_file(path: pathlib.Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def normalize_path(path: Optional[str]) -> str: """Normalize optional service path. Empty -> ''. Ensure leading slash.""" if not path: return "" p = str(path).strip() if not p: return "" if not p.startswith("/"): p = "/" + p return p def default_host() -> str: """Return preferred hostname (append .local if bare).""" host = socket.gethostname() if "." not in host: host = f"{host}.local" return host def detect_https(host: str, port: int) -> bool: """Heuristic: known HTTPS ports or .local certs.""" return int(port) in HTTPS_PORTS or host.lower().endswith(".local") or host.lower() == "pikit" def port_online(host: str, port: int) -> bool: try: with socket.create_connection((host, int(port)), timeout=1.5): return True except Exception: return False def reboot_required() -> bool: return pathlib.Path("/run/reboot-required").exists() def strip_comments(text: str) -> str: """Remove // and # line comments for safer parsing.""" lines = [] for ln in text.splitlines(): l = ln.strip() if l.startswith("//") or l.startswith("#"): continue lines.append(ln) return "\n".join(lines) def validate_time(val: str, default: str) -> str: if not val: return default m = re.match(r"^(\d{1,2}):(\d{2})$", val.strip()) if not m: return default h, mi = int(m.group(1)), int(m.group(2)) if 0 <= h < 24 and 0 <= mi < 60: return f"{h:02d}:{mi:02d}" return default