This commit is contained in:
2026-01-23 03:23:02 -08:00
parent af313557dd
commit 5368caf876

View File

@@ -22,6 +22,7 @@ from __future__ import annotations
import argparse
import os
import pwd
import shlex
import shutil
import subprocess
@@ -223,6 +224,44 @@ def is_first_run(repo_root: Path) -> bool:
return True
def _user_exists(username: str) -> bool:
try:
pwd.getpwnam(username)
return True
except KeyError:
return False
def ensure_service_user(username: str) -> bool:
if not username:
return False
if _user_exists(username):
return True
useradd = shutil.which("useradd")
if not useradd:
print("useradd not found; cannot create service user.")
return False
shell = "/usr/sbin/nologin" if Path("/usr/sbin/nologin").exists() else "/bin/false"
cmd = [
useradd,
"--system",
"--no-create-home",
"--shell",
shell,
username,
]
try:
subprocess.run(cmd, check=True)
print(f"Created service user '{username}'.")
return True
except FileNotFoundError:
print("useradd executable missing; cannot create service user.")
return False
except subprocess.CalledProcessError as exc:
print(f"Failed to create service user '{username}': {exc}")
return False
# --- Service install/uninstall helpers -----------------------------------
@@ -332,6 +371,7 @@ def install_service_systemd(
detached: bool = True,
pull: bool = False,
workspace_root: Optional[Path] = None,
service_user: Optional[str] = None,
) -> bool:
try:
helper_path = Path(__file__).resolve()
@@ -385,6 +425,7 @@ def install_service_systemd(
detached=detached,
pull=pull,
workspace_root=workspace_root,
service_user=service_user,
)
unit_dir = Path.home() / ".config" / "systemd" / "user"
@@ -456,81 +497,6 @@ def install_service_systemd(
return False
def install_service_systemd_system(
service_name: str,
repo_root: Path,
venv_py: Path,
headless: bool = True,
detached: bool = True,
pull: bool = False,
workspace_root: Optional[Path] = None,
) -> bool:
try:
systemctl = shutil.which("systemctl")
if not systemctl:
print(
"systemctl not available; falling back to crontab @reboot (if present)."
)
return install_service_cron(
service_name,
repo_root,
venv_py,
headless,
detached,
pull=pull,
workspace_root=workspace_root,
)
unit_dir = Path("/etc/systemd/system")
service_file = unit_dir / f"{service_name}.service"
unit_dir.mkdir(parents=True, exist_ok=True)
local_helper = repo_root / "run_client.py"
if not local_helper.exists():
local_helper = repo_root / "scripts" / "run_client.py"
target_script = local_helper if local_helper.exists() else Path(__file__).resolve()
exec_args = f'"{venv_py}" "{target_script}" --detached '
if headless:
exec_args += "--headless "
if pull:
exec_args += "--pull "
exec_args += f'--repo-root "{repo_root}" '
content = (
"[Unit]\n"
"Description=Medios-Macina Client (system service)\n"
"After=network.target\n\n"
"[Service]\n"
"Type=simple\n"
f"ExecStart={exec_args}\n"
f"WorkingDirectory={repo_root}\n"
"Restart=on-failure\n"
"Environment=PYTHONUNBUFFERED=1\n"
"[Install]\n"
"WantedBy=multi-user.target\n"
)
service_file.write_text(content, encoding="utf-8")
for cmd in [
[systemctl, "daemon-reload"],
[systemctl, "enable", "--now", f"{service_name}.service"],
]:
subprocess.run(cmd, check=True)
print(f"system-wide systemd service '{service_name}' enabled and started.")
return True
except subprocess.CalledProcessError as exc:
print("Failed to install system-wide service:", exc)
return False
except PermissionError as exc:
print("Permission denied while writing systemd unit:", exc)
return False
except Exception as exc:
print("system-wide install error:", exc)
return False
def uninstall_service_systemd(service_name: str) -> bool:
try:
systemctl = shutil.which("systemctl")
@@ -557,6 +523,94 @@ def uninstall_service_systemd(service_name: str) -> bool:
return False
def install_service_systemd_system(
service_name: str,
repo_root: Path,
venv_py: Path,
headless: bool = True,
detached: bool = True,
pull: bool = False,
workspace_root: Optional[Path] = None,
service_user: Optional[str] = None,
) -> bool:
try:
systemctl = shutil.which("systemctl")
if not systemctl:
print(
"systemctl not available; falling back to crontab @reboot (if present)."
)
return install_service_cron(
service_name,
repo_root,
venv_py,
headless,
detached,
pull=pull,
workspace_root=workspace_root,
)
if service_user and not ensure_service_user(service_user):
print(f"Unable to prepare service user '{service_user}' for system service.")
return False
unit_dir = Path("/etc/systemd/system")
service_file = unit_dir / f"{service_name}.service"
unit_dir.mkdir(parents=True, exist_ok=True)
local_helper = repo_root / "run_client.py"
if not local_helper.exists():
local_helper = repo_root / "scripts" / "run_client.py"
target_script = local_helper if local_helper.exists() else Path(__file__).resolve()
exec_args = f'"{venv_py}" "{target_script}" --detached '
if headless:
exec_args += "--headless "
if pull:
exec_args += "--pull "
exec_args += f'--repo-root "{repo_root}" '
service_lines = [
"[Unit]",
"Description=Medios-Macina Client (system service)",
"After=network.target",
"",
"[Service]",
"Type=simple",
f"ExecStart={exec_args}",
f"WorkingDirectory={repo_root}",
"Restart=on-failure",
"Environment=PYTHONUNBUFFERED=1",
]
if service_user:
service_lines.append(f"User={service_user}")
service_lines.append(f"Group={service_user}")
service_lines.extend([
"",
"[Install]",
"WantedBy=multi-user.target",
])
content = "\n".join(service_lines) + "\n"
service_file.write_text(content, encoding="utf-8")
for cmd in [
[systemctl, "daemon-reload"],
[systemctl, "enable", "--now", f"{service_name}.service"],
]:
subprocess.run(cmd, check=True)
print(f"system-wide systemd service '{service_name}' enabled and started.")
return True
except subprocess.CalledProcessError as exc:
print("Failed to install system-wide service:", exc)
return False
except PermissionError as exc:
print("Permission denied while writing systemd unit:", exc)
return False
except Exception as exc:
print("system-wide install error:", exc)
return False
def install_service_cron(
service_name: str,
repo_root: Path,