From b3e7f3e27761a4c97cc4e96f6d4d9dcf5c773691 Mon Sep 17 00:00:00 2001 From: Nose Date: Thu, 22 Jan 2026 01:09:09 -0800 Subject: [PATCH] f --- scripts/run_client.py | 187 ++++++++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 62 deletions(-) diff --git a/scripts/run_client.py b/scripts/run_client.py index 23d85b6..80c4a60 100644 --- a/scripts/run_client.py +++ b/scripts/run_client.py @@ -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: 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)