mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
169 lines
6.9 KiB
Python
169 lines
6.9 KiB
Python
from __future__ import annotations
|
|
import subprocess
|
|
import threading
|
|
import os
|
|
import shutil
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, Callable, Sequence
|
|
|
|
from installer.state import InstallerState
|
|
|
|
if TYPE_CHECKING:
|
|
from installer.milestone import Milestone
|
|
|
|
class ProleController:
|
|
"""Business logic for Prole installer, separated from UI."""
|
|
def __init__(self, project_root: Path, verbose: bool = False):
|
|
self.project_root = project_root
|
|
self.verbose = verbose
|
|
self.logger = logging.getLogger("ProleController")
|
|
self.state = InstallerState(controller=self)
|
|
|
|
def check_docker_running(self) -> bool:
|
|
"""Check if Docker daemon is responsive."""
|
|
try:
|
|
subprocess.run(['docker', 'info'], capture_output=True, check=True)
|
|
return True
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return False
|
|
|
|
def get_prole_db_version(self) -> str:
|
|
pg_version_file = self.project_root / "conf" / "postgresql" / ".version"
|
|
release_file = self.project_root / "prole-db" / ".version"
|
|
pg_version = pg_version_file.read_text().strip() if pg_version_file.exists() else "17.7"
|
|
release = release_file.read_text().strip() if release_file.exists() else "43"
|
|
if release.isdigit():
|
|
release = release.zfill(3)
|
|
return f"{pg_version}-{release}"
|
|
|
|
def run_script(self, script_name: str, args: list[str] | None = None, env: dict | None = None,
|
|
stdin_text: str | None = None, on_line: Callable[[str], None] | None = None,
|
|
on_stderr_line: Callable[[str], None] | None = None, stderr_to_stdout: bool = True) -> int:
|
|
"""Generic runner for etc/ scripts.
|
|
Copies the script to $PROLE_HOME/etc before running it.
|
|
"""
|
|
self.logger.debug(f"run_script: {script_name} with args={args}")
|
|
if args is None: args = []
|
|
|
|
# Get paths
|
|
source_script = self.project_root / "etc" / script_name
|
|
|
|
# Determine target etc directory
|
|
prole_home_val = (env or os.environ).get("PROLE_HOME")
|
|
if not prole_home_val:
|
|
prole_home = Path.home() / ".prole"
|
|
else:
|
|
prole_home = Path(prole_home_val).expanduser()
|
|
|
|
target_etc = prole_home / "etc"
|
|
target_etc.mkdir(parents=True, exist_ok=True)
|
|
target_script = target_etc / script_name
|
|
|
|
# Also ensure k8s resources are copied to $PROLE_HOME/k8s for reference
|
|
target_k8s = prole_home / "k8s"
|
|
source_k8s = self.project_root / "k8s"
|
|
if source_k8s.exists():
|
|
if source_k8s.resolve() != target_k8s.resolve():
|
|
if not target_k8s.exists() or (source_k8s.stat().st_mtime > target_k8s.stat().st_mtime):
|
|
if target_k8s.exists():
|
|
shutil.rmtree(target_k8s)
|
|
shutil.copytree(source_k8s, target_k8s)
|
|
|
|
# Ensure conf/postgresql is copied for version detection
|
|
target_conf = prole_home / "conf"
|
|
source_conf = self.project_root / "conf"
|
|
if source_conf.exists():
|
|
if source_conf.resolve() != target_conf.resolve():
|
|
if not target_conf.exists() or (source_conf.stat().st_mtime > target_conf.stat().st_mtime):
|
|
if target_conf.exists():
|
|
shutil.rmtree(target_conf)
|
|
shutil.copytree(source_conf, target_conf)
|
|
|
|
# Ensure prole-db/.version is available in PROLE_HOME for image tagging
|
|
source_db_version = self.project_root / "prole-db" / ".version"
|
|
if source_db_version.exists():
|
|
target_db_dir = prole_home / "prole-db"
|
|
target_db_dir.mkdir(parents=True, exist_ok=True)
|
|
target_db_version = target_db_dir / ".version"
|
|
try:
|
|
shutil.copy2(source_db_version, target_db_version)
|
|
except Exception:
|
|
pass
|
|
|
|
# Copy script and set permissions
|
|
if source_script.exists():
|
|
if source_script.resolve() != target_script.resolve():
|
|
shutil.copy2(source_script, target_script)
|
|
os.chmod(target_script, 0o755)
|
|
|
|
# Keep shared config helpers in sync (prole_cfg.sh)
|
|
try:
|
|
source_cfg = self.project_root / "etc" / "prole_cfg.sh"
|
|
target_cfg = target_etc / "prole_cfg.sh"
|
|
if source_cfg.exists():
|
|
if not target_cfg.exists() or (source_cfg.stat().st_mtime > target_cfg.stat().st_mtime):
|
|
shutil.copy2(source_cfg, target_cfg)
|
|
os.chmod(target_cfg, 0o755)
|
|
except Exception:
|
|
pass
|
|
|
|
# Run from the installed location
|
|
cmd = ['bash', str(target_script)] + args
|
|
self.logger.debug(f"Starting subprocess: {' '.join(cmd)}")
|
|
|
|
# Ensure PYTHONUNBUFFERED=1 for any python scripts called within the bash script
|
|
run_env = (env or os.environ).copy()
|
|
run_env['PYTHONUNBUFFERED'] = '1'
|
|
if self.verbose:
|
|
run_env.setdefault("PROLE_VERBOSE", "1")
|
|
run_env.setdefault("VERBOSE", "1")
|
|
run_env.setdefault("CLICOLOR_FORCE", "1")
|
|
run_env.setdefault("PY_COLORS", "1")
|
|
if not run_env.get("TERM"):
|
|
run_env["TERM"] = "xterm-256color"
|
|
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
stdin=subprocess.PIPE if stdin_text else None,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE,
|
|
text=True,
|
|
bufsize=1,
|
|
env=run_env
|
|
)
|
|
|
|
if stdin_text and proc.stdin:
|
|
proc.stdin.write(stdin_text)
|
|
proc.stdin.close()
|
|
|
|
def _read_stream(stream, handler):
|
|
if not stream:
|
|
return
|
|
for line in iter(stream.readline, ''):
|
|
if handler:
|
|
handler(line)
|
|
|
|
stderr_thread = None
|
|
if not stderr_to_stdout:
|
|
stderr_thread = threading.Thread(target=_read_stream, args=(proc.stderr, on_stderr_line), daemon=True)
|
|
stderr_thread.start()
|
|
|
|
while True:
|
|
line = proc.stdout.readline() if proc.stdout else None
|
|
if not line and proc.poll() is not None:
|
|
break
|
|
if line and on_line:
|
|
on_line(line)
|
|
if stderr_thread:
|
|
stderr_thread.join(timeout=2)
|
|
|
|
return proc.returncode
|
|
|
|
def run_milestones(self, milestones: Sequence[Milestone], progress_callback: Callable[[str, float], None] | None = None):
|
|
"""Execute a sequence of milestones."""
|
|
for milestone in milestones:
|
|
self.logger.info(f"==> Executing Milestone: {milestone.title}")
|
|
milestone.execute(self.state, progress=progress_callback)
|
|
self.state.mark_completed(milestone.id)
|