"""Command line entry point: ``python -m webrestrict`` (AGENTS.md ยง11 Phase 4). Usage:: python -m webrestrict run # the client (the Scheduled Task runs this) python -m webrestrict check # one-shot: cached config, effective state, victims python -m webrestrict version ``--config PATH`` overrides the config file (default ``%PROGRAMDATA%\\WebRestrict\\client.json``, or ``WEBRESTRICT_CONFIG``). """ from __future__ import annotations import argparse import json import logging import logging.handlers import sys from . import __version__ from .agent import Agent, check_once, install_signal_handlers from .settings import Settings, default_config_path LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s" def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="webrestrict", description="Kids' Windows computer client") parser.add_argument("--config", help="path to client.json (default: %(default)s)") parser.add_argument("--verbose", action="store_true", help="debug logging") sub = parser.add_subparsers(dest="command") sub.add_parser("run", help="run the client (default)") sub.add_parser("check", help="print the cached config and what would be blocked, then exit") sub.add_parser("version", help="print the client version") return parser def configure_logging(settings: Settings, verbose: bool) -> None: """Log to the console and to a rotating file next to the state. A Scheduled Task runs with no visible console, so the file is the record that survives: it holds the config that arrived and every process blocked. """ root = logging.getLogger() root.setLevel(logging.DEBUG if verbose else logging.INFO) root.handlers.clear() console = logging.StreamHandler(sys.stdout) console.setFormatter(logging.Formatter(LOG_FORMAT)) root.addHandler(console) try: settings.state_path.mkdir(parents=True, exist_ok=True) rotating = logging.handlers.RotatingFileHandler( settings.log_path(), maxBytes=2 * 1024 * 1024, backupCount=3, encoding="utf-8" ) rotating.setFormatter(logging.Formatter(LOG_FORMAT)) root.addHandler(rotating) except OSError as exc: # pragma: no cover - depends on the machine root.warning("cannot write the log file %s: %s", settings.log_path(), exc) def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) command = args.command or "run" if command == "version": print(f"webrestrict-windows {__version__}") return 0 settings_path = args.config or str(default_config_path()) try: settings = Settings.load(settings_path) except (OSError, ValueError, json.JSONDecodeError) as exc: print(f"webrestrict: cannot load {settings_path}: {exc}", file=sys.stderr) return 2 configure_logging(settings, args.verbose) if command == "check": print(json.dumps(check_once(settings), indent=2, sort_keys=True)) return 0 agent = Agent(settings) install_signal_handlers(agent) try: return agent.run() except KeyboardInterrupt: # pragma: no cover - Ctrl-C agent.shutdown() return 0 if __name__ == "__main__": raise SystemExit(main())