This commit is contained in:
2026-01-22 01:09:09 -08:00
parent eb3fc065c9
commit b3e7f3e277

View File

@@ -75,6 +75,11 @@ def install_requirements(
reinstall: bool = False
) -> bool:
try:
# Suppression flag for Windows
kwargs = {}
if os.name == "nt":
kwargs["creationflags"] = 0x08000000
print(f"Installing {req_path} into venv ({venv_py})...")
subprocess.run(
[str(venv_py),
@@ -83,7 +88,8 @@ def install_requirements(
"install",
"--upgrade",
"pip"],
check=True
check=True,
**kwargs
)
install_cmd = [str(venv_py), "-m", "pip", "install", "-r", str(req_path)]
if reinstall:
@@ -97,7 +103,7 @@ def install_requirements(
"-r",
str(req_path),
]
subprocess.run(install_cmd, check=True)
subprocess.run(install_cmd, check=True, **kwargs)
return True
except subprocess.CalledProcessError as e:
print("Failed to install requirements:", e)
@@ -147,10 +153,18 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
"mpv": "mpv",
"pyside6": "PySide6",
}
# Helper for silent subprocess execution on Windows
def _run_silent(cmd, **kwargs):
if os.name == "nt":
# 0x08000000 = CREATE_NO_WINDOW
kwargs["creationflags"] = kwargs.get("creationflags", 0) | 0x08000000
return subprocess.run(cmd, **kwargs)
missing = []
for pkg in packages:
try:
out = subprocess.run(
out = _run_silent(
[str(venv_py),
"-m",
"pip",
@@ -174,7 +188,7 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
import_name = import_map.get(pkg, pkg)
try:
subprocess.run(
_run_silent(
[str(venv_py),
"-c",
f"import {import_name}"],
@@ -228,26 +242,41 @@ def install_service_windows(
)
return False
# If we have a workspace root (Medios-Macina), we use it for the wrapper scripts
base_dir = workspace_root if workspace_root else repo_root
bat = base_dir / "run-client.bat"
# Use the repository root for the service wrapper script
bat = repo_root / "run-client.bat"
# If there's a local copy of run_client.py in the target repo, use that instead
# of the one from Medios-Macina to keep the service independent.
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()
python_exe = venv_py
this_script = Path(__file__).resolve()
# Use pythonw.exe for windowless execution on Windows
pythonw_exe = python_exe.parent / "pythonw.exe"
if not pythonw_exe.exists():
pythonw_exe = python_exe
if not bat.exists():
# Absolute paths ensure the service works regardless of where it starts
content = f'@echo off\n"{python_exe}" "{this_script}" %*\n'
# The .bat remains using python.exe for manual/interactive runs
content = f'@echo off\n"{python_exe}" "{target_script}" %*\n'
bat.write_text(content, encoding="utf-8")
tr = str(bat)
sc = "ONLOGON" if start_on == "logon" else "ONSTART"
task_args = "--detached "
# When running as a service, we DO NOT use --detached.
# This keeps the run_client.py process alive as a monitor for the task scheduler,
# preventing it from thinking the task finished/crashed and trying to restart it.
task_args = ""
if headless: task_args += "--headless "
if pull: task_args += "--pull "
# Force the correct repo root for the service
task_args += f'--repo-root "{repo_root}" '
# Use pythonw for the task to avoid console window
tr_command = f'"{pythonw_exe}" "{target_script}" {task_args.strip()}'
cmd = [
schtasks,
@@ -257,7 +286,7 @@ def install_service_windows(
"/TN",
service_name,
"/TR",
f'{tr} {task_args}',
tr_command,
"/RL",
"LIMITED",
"/F",
@@ -322,13 +351,18 @@ def install_service_systemd(
unit_dir.mkdir(parents=True, exist_ok=True)
unit_file = unit_dir / f"{service_name}.service"
this_script = Path(__file__).resolve()
exec_args = f'"{venv_py}" "{this_script}" --detached '
# Prefer local helper if it exists
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 = f"[Unit]\nDescription=Medios-Macina Client\nAfter=network.target\n\n[Service]\nType=simple\nExecStart={exec_args}\nWorkingDirectory={str(workspace_root if workspace_root else repo_root)}\nRestart=on-failure\nEnvironment=PYTHONUNBUFFERED=1\n\n[Install]\nWantedBy=default.target\n"
content = f"[Unit]\nDescription=Medios-Macina Client\nAfter=network.target\n\n[Service]\nType=simple\nExecStart={exec_args}\nWorkingDirectory={str(repo_root)}\nRestart=on-failure\nEnvironment=PYTHONUNBUFFERED=1\n\n[Install]\nWantedBy=default.target\n"
unit_file.write_text(content, encoding="utf-8")
subprocess.run([systemctl, "--user", "daemon-reload"], check=True)
subprocess.run(
@@ -390,8 +424,13 @@ def install_service_cron(
print("crontab not available; cannot install reboot cron job.")
return False
this_script = Path(__file__).resolve()
exec_args = f'"{venv_py}" "{this_script}" --detached '
# Prefer local helper if it exists
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}" '
@@ -532,11 +571,11 @@ def print_activation_instructions(
def detach_kwargs_for_platform():
kwargs = {}
if os.name == "nt":
CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
DETACHED_PROCESS = getattr(subprocess, "DETACHED_PROCESS", 0)
flags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
if flags:
kwargs["creationflags"] = flags
# Flags to ensure the process is detached and has NO console window
CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008
CREATE_NO_WINDOW = 0x08000000
kwargs["creationflags"] = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_NO_WINDOW
else:
kwargs["start_new_session"] = True
return kwargs
@@ -710,13 +749,18 @@ def main(argv: Optional[List[str]] = None) -> int:
repo_root = workspace_root
# Handle git pull update if requested
if args.pull:
# Skip execution during service install/uninstall; it will run when the service starts
if args.pull and not (args.install_service or args.uninstall_service):
if shutil.which("git"):
if (repo_root / ".git").exists():
if not args.quiet:
print(f"Updating repository via 'git pull' in {repo_root}...")
try:
subprocess.run(["git", "pull"], cwd=str(repo_root), check=False)
# Use creationflags to hide the window on Windows
k = {}
if os.name == "nt":
k["creationflags"] = 0x08000000
subprocess.run(["git", "pull"], cwd=str(repo_root), check=False, **k)
except Exception as e:
print(f"Warning: git pull failed: {e}")
else:
@@ -740,6 +784,9 @@ def main(argv: Optional[List[str]] = None) -> int:
except Exception:
return False
# Skip heavy verification if we are just installing/uninstalling a service
do_verify = args.verify or (not args.no_verify and not (args.install_service or args.uninstall_service))
# Prefer the current interpreter if the helper was invoked from a virtualenv
# and the user did not explicitly pass --venv. This matches the user's likely
# intent when they called: <venv_python> scripts/run_client.py ...
@@ -748,12 +795,19 @@ def main(argv: Optional[List[str]] = None) -> int:
# If current interpreter looks like a venv and can import required modules,
# prefer it immediately rather than forcing the repo venv.
req = find_requirements(repo_root)
pkgs = parse_requirements_file(req) if req else []
pkgs = parse_requirements_file(req) if req and do_verify else []
check_pkgs = pkgs if pkgs else ["pyyaml"]
try:
ok_cur = verify_imports(cur_py, check_pkgs)
except Exception:
ok_cur = _python_can_import(cur_py, ["yaml"])
ok_cur = False
if do_verify:
try:
ok_cur = verify_imports(cur_py, check_pkgs)
except Exception:
ok_cur = _python_can_import(cur_py, ["yaml"])
else:
# If skipping verification, assume current is OK if it's the right version
ok_cur = True
if ok_cur:
venv_py = cur_py
if not args.quiet:
@@ -762,9 +816,9 @@ def main(argv: Optional[List[str]] = None) -> int:
# If we found a repo-local venv, verify it has at least the core imports (or the
# packages listed in requirements.txt). If not, prefer the current Python
# interpreter when that interpreter looks more suitable (e.g. has deps installed).
if venv_py and venv_py != cur_py:
if venv_py and venv_py != cur_py and do_verify:
if not args.quiet:
print(f"Found venv python: {venv_py}")
print(f"Found venv python: {venv_py}")
req = find_requirements(repo_root)
pkgs = parse_requirements_file(req) if req else []
check_pkgs = pkgs if pkgs else ["pyyaml"]
@@ -772,6 +826,7 @@ def main(argv: Optional[List[str]] = None) -> int:
ok_venv = verify_imports(venv_py, check_pkgs)
except Exception:
ok_venv = _python_can_import(venv_py, ["yaml"])
# ... logic continues below
if not ok_venv:
try:
@@ -839,33 +894,31 @@ def main(argv: Optional[List[str]] = None) -> int:
)
# If the venv appears to be missing required packages, offer to install them interactively
req = find_requirements(repo_root)
pkgs = parse_requirements_file(req) if req else []
check_pkgs = pkgs if pkgs else ["pyyaml"]
try:
venv_ok = verify_imports(venv_py, check_pkgs)
except Exception:
venv_ok = _python_can_import(venv_py, ["yaml"]) # fallback
if do_verify:
req = find_requirements(repo_root)
pkgs = parse_requirements_file(req) if req else []
check_pkgs = pkgs if pkgs else ["pyyaml"]
try:
venv_ok = verify_imports(venv_py, check_pkgs)
except Exception:
venv_ok = _python_can_import(venv_py, ["yaml"]) # fallback
if not venv_ok:
# If user explicitly requested install, we've already attempted it above; otherwise, do not block.
if args.install_deps or args.reinstall:
# if we already did an install attempt and it still fails, bail
print("Dependency verification failed after install; aborting.")
return 4
if not venv_ok:
# If user explicitly requested install, we've already attempted it above; otherwise, do not block.
if args.install_deps or args.reinstall:
# if we already did an install attempt and it still fails, bail
print("Dependency verification failed after install; aborting.")
return 4
# Default: print a clear warning and proceed to launch with the repository venv
if args.no_verify:
print(
"Repository venv is missing required packages; proceeding without verification as requested ( --no-verify ). Client may fail to start."
)
else:
print(
"Warning: repository venv appears to be missing required packages. Proceeding to launch with repository venv; the client may fail to start. Use --install-deps to install requirements into the repo venv."
)
# Do not prompt to switch to another interpreter automatically; the user can
# re-run with --venv to select a different python if desired.
# Default: print a clear warning and proceed to launch with the repository venv
if args.no_verify:
print(
"Repository venv is missing required packages; proceeding without verification as requested ( --no-verify ). Client may fail to start."
)
else:
print(
"Warning: repository venv appears to be missing required packages. Proceeding to launch with repository venv; the client may fail to start. Use --install-deps to install requirements into the repo venv."
)
# Service install/uninstall requests
if args.install_service or args.uninstall_service:
@@ -892,10 +945,6 @@ def main(argv: Optional[List[str]] = None) -> int:
ok = uninstall_service_auto(args.service_name, repo_root, venv_py)
return 0 if ok else 7
# Prepare the command
client_args = args.client_args or []
cmd = [str(venv_py), str(client_path)] + client_args
# Determine headless vs GUI
if args.gui:
headless = False
@@ -905,6 +954,16 @@ def main(argv: Optional[List[str]] = None) -> int:
# Default to GUI for the client launcher
headless = False
# On Windows, if we are headless, use pythonw.exe for the client too to avoid a console.
if os.name == "nt" and headless:
pw = venv_py.parent / "pythonw.exe"
if pw.exists():
venv_py = pw
# Prepare the command
client_args = args.client_args or []
cmd = [str(venv_py), str(client_path)] + client_args
if not args.quiet and is_first_run(repo_root):
print("First run detected: defaulting to GUI unless --headless is specified.")
@@ -948,7 +1007,11 @@ def main(argv: Optional[List[str]] = None) -> int:
return 5
else:
try:
subprocess.run(cmd, cwd=str(cwd), env=env)
# If headless on Windows, ensure no window shows even for foreground run
kwargs = {}
if os.name == "nt" and headless:
kwargs["creationflags"] = 0x08000000
subprocess.run(cmd, cwd=str(cwd), env=env, **kwargs)
return 0
except subprocess.CalledProcessError as e:
print("hydrus client exited non-zero:", e)