f
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user