g
This commit is contained in:
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import pwd
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -223,6 +224,44 @@ def is_first_run(repo_root: Path) -> bool:
|
|||||||
return True
|
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 -----------------------------------
|
# --- Service install/uninstall helpers -----------------------------------
|
||||||
|
|
||||||
|
|
||||||
@@ -332,6 +371,7 @@ def install_service_systemd(
|
|||||||
detached: bool = True,
|
detached: bool = True,
|
||||||
pull: bool = False,
|
pull: bool = False,
|
||||||
workspace_root: Optional[Path] = None,
|
workspace_root: Optional[Path] = None,
|
||||||
|
service_user: Optional[str] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
try:
|
try:
|
||||||
helper_path = Path(__file__).resolve()
|
helper_path = Path(__file__).resolve()
|
||||||
@@ -385,6 +425,7 @@ def install_service_systemd(
|
|||||||
detached=detached,
|
detached=detached,
|
||||||
pull=pull,
|
pull=pull,
|
||||||
workspace_root=workspace_root,
|
workspace_root=workspace_root,
|
||||||
|
service_user=service_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
||||||
@@ -456,81 +497,6 @@ def install_service_systemd(
|
|||||||
return False
|
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:
|
def uninstall_service_systemd(service_name: str) -> bool:
|
||||||
try:
|
try:
|
||||||
systemctl = shutil.which("systemctl")
|
systemctl = shutil.which("systemctl")
|
||||||
@@ -557,6 +523,94 @@ def uninstall_service_systemd(service_name: str) -> bool:
|
|||||||
return False
|
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(
|
def install_service_cron(
|
||||||
service_name: str,
|
service_name: str,
|
||||||
repo_root: Path,
|
repo_root: Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user