From a4311e53ad1b9e49080455658e2256b432bb6ba8 Mon Sep 17 00:00:00 2001 From: Nose Date: Fri, 23 Jan 2026 03:48:41 -0800 Subject: [PATCH] f --- scripts/bootstrap.py | 1 + scripts/hydrusnetwork.py | 31 +++++++++++++++++++++++++- scripts/run_client.py | 47 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py index 73b0bbf..95ad388 100644 --- a/scripts/bootstrap.py +++ b/scripts/bootstrap.py @@ -1286,6 +1286,7 @@ def main() -> int: str(run_client_script), "--install-service", "--service-name", "hydrus-client", + "--service-user", "hydrusnetwork", "--repo-root", str(target_repo), "--headless", "--pull" diff --git a/scripts/hydrusnetwork.py b/scripts/hydrusnetwork.py index 1ba736c..338801d 100644 --- a/scripts/hydrusnetwork.py +++ b/scripts/hydrusnetwork.py @@ -64,6 +64,22 @@ except Exception: return kwargs + def _determine_service_user(args) -> Optional[str]: + user = getattr(args, "service_user", None) + if user: + user = user.strip() + if not user: + user = None + if ( + not user + and os.name != "nt" + and hasattr(os, "geteuid") + and os.geteuid() == 0 + ): + user = "hydrusnetwork" + return user + + def find_git_executable() -> Optional[str]: """Return the git executable path or None if not found.""" import shutil as _shutil @@ -867,6 +883,11 @@ def main(argv: Optional[list[str]] = None) -> int: default="hydrus-client", help="Name for the installed service/scheduled task (default: hydrus-client)", ) + parser.add_argument( + "--service-user", + default=None, + help="When installing a systemd system service as root, run it under this user", + ) parser.add_argument( "--no-project-venv", action="store_true", @@ -1197,6 +1218,7 @@ def main(argv: Optional[list[str]] = None) -> int: run_client_script = cand break + service_user = _determine_service_user(args) if run_client_script and run_client_script.exists(): cmd = [ str(venv_py), @@ -1207,6 +1229,8 @@ def main(argv: Optional[list[str]] = None) -> int: "--detached", "--headless", ] + if service_user: + cmd.extend(["--service-user", service_user]) logging.info("Installing service via helper: %s", cmd) try: subprocess.run(cmd, cwd=str(dest), check=True) @@ -1223,6 +1247,7 @@ def main(argv: Optional[list[str]] = None) -> int: venv_py, headless=True, detached=True, + service_user=service_user, ) if ok: logging.info("Service installed (user-level).") @@ -1559,6 +1584,7 @@ def main(argv: Optional[list[str]] = None) -> int: ) else: if getattr(args, "install_service", False): + service_user = _determine_service_user(args) if run_client_script and run_client_script.exists(): cmd = [ str(venv_py), @@ -1569,6 +1595,8 @@ def main(argv: Optional[list[str]] = None) -> int: "--detached", "--headless", ] + if service_user: + cmd.extend(["--service-user", service_user]) logging.info("Installing service via helper: %s", cmd) try: subprocess.run(cmd, cwd=str(dest), check=True) @@ -1582,7 +1610,8 @@ def main(argv: Optional[list[str]] = None) -> int: dest, venv_py, headless=True, - detached=True + detached=True, + service_user=service_user ) if ok: logging.info("Service installed (user-level).") diff --git a/scripts/run_client.py b/scripts/run_client.py index e2c8539..6af4ab5 100644 --- a/scripts/run_client.py +++ b/scripts/run_client.py @@ -262,6 +262,26 @@ def ensure_service_user(username: str) -> bool: return False +def grant_service_user_repo_access(repo_root: Path, username: str) -> bool: + try: + for dirpath, dirnames, filenames in os.walk(repo_root): + shutil.chown(dirpath, user=username, group=username) + for name in dirnames + filenames: + path = Path(dirpath) / name + shutil.chown(path, user=username, group=username) + return True + except PermissionError as exc: + print( + f"Failed to grant ownership of '{repo_root}' to '{username}': {exc}" + ) + return False + except Exception as exc: + print( + f"Error while adjusting permissions for '{username}' in '{repo_root}': {exc}" + ) + return False + + # --- Service install/uninstall helpers ----------------------------------- @@ -552,6 +572,11 @@ def install_service_systemd_system( if service_user and not ensure_service_user(service_user): print(f"Unable to prepare service user '{service_user}' for system service.") return False + if service_user and not grant_service_user_repo_access(repo_root, service_user): + print( + f"Failed to assign '{service_user}' as the owner of '{repo_root}'." + ) + return False unit_dir = Path("/etc/systemd/system") service_file = unit_dir / f"{service_name}.service" @@ -698,6 +723,7 @@ def install_service_auto( detached: bool = True, pull: bool = False, workspace_root: Optional[Path] = None, + service_user: Optional[str] = None, ) -> bool: try: if os.name == "nt": @@ -719,7 +745,8 @@ def install_service_auto( headless=headless, detached=detached, pull=pull, - workspace_root=workspace_root + workspace_root=workspace_root, + service_user=service_user, ) else: return install_service_cron( @@ -918,6 +945,11 @@ def main(argv: Optional[List[str]] = None) -> int: default="hydrus-client", help="Name of the service / scheduled task to install (default: hydrus-client)", ) + p.add_argument( + "--service-user", + default=None, + help="When installing a system-wide unit as root, optionally run it under this user (default: hydrusnetwork)", + ) p.add_argument( "--cwd", default=None, @@ -1144,6 +1176,16 @@ def main(argv: Optional[List[str]] = None) -> int: else: use_headless = not first_run + service_user = args.service_user.strip() if args.service_user else None + if ( + args.install_service + and not service_user + and os.name != "nt" + and hasattr(os, "geteuid") + and os.geteuid() == 0 + ): + service_user = "hydrusnetwork" + if args.install_service: ok = install_service_auto( args.service_name, @@ -1152,7 +1194,8 @@ def main(argv: Optional[List[str]] = None) -> int: headless=use_headless, detached=True, pull=args.pull, - workspace_root=workspace_root + workspace_root=workspace_root, + service_user=service_user ) return 0 if ok else 6 if args.uninstall_service: