f
This commit is contained in:
@@ -75,6 +75,11 @@ def install_requirements(
|
|||||||
reinstall: bool = False
|
reinstall: bool = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
try:
|
try:
|
||||||
|
# Suppression flag for Windows
|
||||||
|
kwargs = {}
|
||||||
|
if os.name == "nt":
|
||||||
|
kwargs["creationflags"] = 0x08000000
|
||||||
|
|
||||||
print(f"Installing {req_path} into venv ({venv_py})...")
|
print(f"Installing {req_path} into venv ({venv_py})...")
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[str(venv_py),
|
[str(venv_py),
|
||||||
@@ -83,7 +88,8 @@ def install_requirements(
|
|||||||
"install",
|
"install",
|
||||||
"--upgrade",
|
"--upgrade",
|
||||||
"pip"],
|
"pip"],
|
||||||
check=True
|
check=True,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
install_cmd = [str(venv_py), "-m", "pip", "install", "-r", str(req_path)]
|
install_cmd = [str(venv_py), "-m", "pip", "install", "-r", str(req_path)]
|
||||||
if reinstall:
|
if reinstall:
|
||||||
@@ -97,7 +103,7 @@ def install_requirements(
|
|||||||
"-r",
|
"-r",
|
||||||
str(req_path),
|
str(req_path),
|
||||||
]
|
]
|
||||||
subprocess.run(install_cmd, check=True)
|
subprocess.run(install_cmd, check=True, **kwargs)
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print("Failed to install requirements:", e)
|
print("Failed to install requirements:", e)
|
||||||
@@ -147,10 +153,18 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
|
|||||||
"mpv": "mpv",
|
"mpv": "mpv",
|
||||||
"pyside6": "PySide6",
|
"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 = []
|
missing = []
|
||||||
for pkg in packages:
|
for pkg in packages:
|
||||||
try:
|
try:
|
||||||
out = subprocess.run(
|
out = _run_silent(
|
||||||
[str(venv_py),
|
[str(venv_py),
|
||||||
"-m",
|
"-m",
|
||||||
"pip",
|
"pip",
|
||||||
@@ -174,7 +188,7 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
|
|||||||
|
|
||||||
import_name = import_map.get(pkg, pkg)
|
import_name = import_map.get(pkg, pkg)
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
_run_silent(
|
||||||
[str(venv_py),
|
[str(venv_py),
|
||||||
"-c",
|
"-c",
|
||||||
f"import {import_name}"],
|
f"import {import_name}"],
|
||||||
@@ -228,27 +242,42 @@ def install_service_windows(
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If we have a workspace root (Medios-Macina), we use it for the wrapper scripts
|
# Use the repository root for the service wrapper script
|
||||||
base_dir = workspace_root if workspace_root else repo_root
|
bat = repo_root / "run-client.bat"
|
||||||
bat = base_dir / "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
|
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():
|
if not bat.exists():
|
||||||
# Absolute paths ensure the service works regardless of where it starts
|
# The .bat remains using python.exe for manual/interactive runs
|
||||||
content = f'@echo off\n"{python_exe}" "{this_script}" %*\n'
|
content = f'@echo off\n"{python_exe}" "{target_script}" %*\n'
|
||||||
bat.write_text(content, encoding="utf-8")
|
bat.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
tr = str(bat)
|
|
||||||
sc = "ONLOGON" if start_on == "logon" else "ONSTART"
|
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 headless: task_args += "--headless "
|
||||||
if pull: task_args += "--pull "
|
if pull: task_args += "--pull "
|
||||||
# Force the correct repo root for the service
|
# Force the correct repo root for the service
|
||||||
task_args += f'--repo-root "{repo_root}" '
|
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 = [
|
cmd = [
|
||||||
schtasks,
|
schtasks,
|
||||||
"/Create",
|
"/Create",
|
||||||
@@ -257,7 +286,7 @@ def install_service_windows(
|
|||||||
"/TN",
|
"/TN",
|
||||||
service_name,
|
service_name,
|
||||||
"/TR",
|
"/TR",
|
||||||
f'{tr} {task_args}',
|
tr_command,
|
||||||
"/RL",
|
"/RL",
|
||||||
"LIMITED",
|
"LIMITED",
|
||||||
"/F",
|
"/F",
|
||||||
@@ -322,13 +351,18 @@ def install_service_systemd(
|
|||||||
unit_dir.mkdir(parents=True, exist_ok=True)
|
unit_dir.mkdir(parents=True, exist_ok=True)
|
||||||
unit_file = unit_dir / f"{service_name}.service"
|
unit_file = unit_dir / f"{service_name}.service"
|
||||||
|
|
||||||
this_script = Path(__file__).resolve()
|
# Prefer local helper if it exists
|
||||||
exec_args = f'"{venv_py}" "{this_script}" --detached '
|
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 headless: exec_args += "--headless "
|
||||||
if pull: exec_args += "--pull "
|
if pull: exec_args += "--pull "
|
||||||
exec_args += f'--repo-root "{repo_root}" '
|
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")
|
unit_file.write_text(content, encoding="utf-8")
|
||||||
subprocess.run([systemctl, "--user", "daemon-reload"], check=True)
|
subprocess.run([systemctl, "--user", "daemon-reload"], check=True)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
@@ -390,8 +424,13 @@ def install_service_cron(
|
|||||||
print("crontab not available; cannot install reboot cron job.")
|
print("crontab not available; cannot install reboot cron job.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
this_script = Path(__file__).resolve()
|
# Prefer local helper if it exists
|
||||||
exec_args = f'"{venv_py}" "{this_script}" --detached '
|
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 headless: exec_args += "--headless "
|
||||||
if pull: exec_args += "--pull "
|
if pull: exec_args += "--pull "
|
||||||
exec_args += f'--repo-root "{repo_root}" '
|
exec_args += f'--repo-root "{repo_root}" '
|
||||||
@@ -532,11 +571,11 @@ def print_activation_instructions(
|
|||||||
def detach_kwargs_for_platform():
|
def detach_kwargs_for_platform():
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
# Flags to ensure the process is detached and has NO console window
|
||||||
DETACHED_PROCESS = getattr(subprocess, "DETACHED_PROCESS", 0)
|
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
||||||
flags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
DETACHED_PROCESS = 0x00000008
|
||||||
if flags:
|
CREATE_NO_WINDOW = 0x08000000
|
||||||
kwargs["creationflags"] = flags
|
kwargs["creationflags"] = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_NO_WINDOW
|
||||||
else:
|
else:
|
||||||
kwargs["start_new_session"] = True
|
kwargs["start_new_session"] = True
|
||||||
return kwargs
|
return kwargs
|
||||||
@@ -710,13 +749,18 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
repo_root = workspace_root
|
repo_root = workspace_root
|
||||||
|
|
||||||
# Handle git pull update if requested
|
# 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 shutil.which("git"):
|
||||||
if (repo_root / ".git").exists():
|
if (repo_root / ".git").exists():
|
||||||
if not args.quiet:
|
if not args.quiet:
|
||||||
print(f"Updating repository via 'git pull' in {repo_root}...")
|
print(f"Updating repository via 'git pull' in {repo_root}...")
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
print(f"Warning: git pull failed: {e}")
|
print(f"Warning: git pull failed: {e}")
|
||||||
else:
|
else:
|
||||||
@@ -740,6 +784,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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
|
# 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
|
# and the user did not explicitly pass --venv. This matches the user's likely
|
||||||
# intent when they called: <venv_python> scripts/run_client.py ...
|
# 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,
|
# If current interpreter looks like a venv and can import required modules,
|
||||||
# prefer it immediately rather than forcing the repo venv.
|
# prefer it immediately rather than forcing the repo venv.
|
||||||
req = find_requirements(repo_root)
|
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"]
|
check_pkgs = pkgs if pkgs else ["pyyaml"]
|
||||||
|
|
||||||
|
ok_cur = False
|
||||||
|
if do_verify:
|
||||||
try:
|
try:
|
||||||
ok_cur = verify_imports(cur_py, check_pkgs)
|
ok_cur = verify_imports(cur_py, check_pkgs)
|
||||||
except Exception:
|
except Exception:
|
||||||
ok_cur = _python_can_import(cur_py, ["yaml"])
|
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:
|
if ok_cur:
|
||||||
venv_py = cur_py
|
venv_py = cur_py
|
||||||
if not args.quiet:
|
if not args.quiet:
|
||||||
@@ -762,7 +816,7 @@ 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
|
# 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
|
# packages listed in requirements.txt). If not, prefer the current Python
|
||||||
# interpreter when that interpreter looks more suitable (e.g. has deps installed).
|
# 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:
|
if not args.quiet:
|
||||||
print(f"Found venv python: {venv_py}")
|
print(f"Found venv python: {venv_py}")
|
||||||
req = find_requirements(repo_root)
|
req = find_requirements(repo_root)
|
||||||
@@ -772,6 +826,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
ok_venv = verify_imports(venv_py, check_pkgs)
|
ok_venv = verify_imports(venv_py, check_pkgs)
|
||||||
except Exception:
|
except Exception:
|
||||||
ok_venv = _python_can_import(venv_py, ["yaml"])
|
ok_venv = _python_can_import(venv_py, ["yaml"])
|
||||||
|
# ... logic continues below
|
||||||
|
|
||||||
if not ok_venv:
|
if not ok_venv:
|
||||||
try:
|
try:
|
||||||
@@ -839,6 +894,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If the venv appears to be missing required packages, offer to install them interactively
|
# If the venv appears to be missing required packages, offer to install them interactively
|
||||||
|
if do_verify:
|
||||||
req = find_requirements(repo_root)
|
req = find_requirements(repo_root)
|
||||||
pkgs = parse_requirements_file(req) if req else []
|
pkgs = parse_requirements_file(req) if req else []
|
||||||
check_pkgs = pkgs if pkgs else ["pyyaml"]
|
check_pkgs = pkgs if pkgs else ["pyyaml"]
|
||||||
@@ -864,9 +920,6 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
"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."
|
"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.
|
|
||||||
|
|
||||||
# Service install/uninstall requests
|
# Service install/uninstall requests
|
||||||
if args.install_service or args.uninstall_service:
|
if args.install_service or args.uninstall_service:
|
||||||
first_run = is_first_run(repo_root)
|
first_run = is_first_run(repo_root)
|
||||||
@@ -892,10 +945,6 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
ok = uninstall_service_auto(args.service_name, repo_root, venv_py)
|
ok = uninstall_service_auto(args.service_name, repo_root, venv_py)
|
||||||
return 0 if ok else 7
|
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
|
# Determine headless vs GUI
|
||||||
if args.gui:
|
if args.gui:
|
||||||
headless = False
|
headless = False
|
||||||
@@ -905,6 +954,16 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
# Default to GUI for the client launcher
|
# Default to GUI for the client launcher
|
||||||
headless = False
|
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):
|
if not args.quiet and is_first_run(repo_root):
|
||||||
print("First run detected: defaulting to GUI unless --headless is specified.")
|
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
|
return 5
|
||||||
else:
|
else:
|
||||||
try:
|
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
|
return 0
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print("hydrus client exited non-zero:", e)
|
print("hydrus client exited non-zero:", e)
|
||||||
|
|||||||
Reference in New Issue
Block a user