2025-12-29 17:05:03 -08:00
#!/usr/bin/env python3
""" Run the Hydrus client (top-level helper)
This standalone helper is intended to live in the project ' s top-level `scripts/`
folder so it remains available even if the Hydrus repository subfolder is not
present or its copy of this helper gets removed .
Features ( subset of the repo helper ) :
- Locate repository venv ( default : < workspace > / hydrusnetwork / . venv )
2025-12-31 22:05:25 -08:00
- Install or reinstall scripts / requirements . txt into the venv
2025-12-29 17:05:03 -08:00
- Verify key imports
- Launch hydrus_client . py ( foreground or detached )
- Install / uninstall simple user - level start - on - boot services ( schtasks / systemd / crontab )
Usage examples :
python scripts / run_client . py - - verify
python scripts / run_client . py - - detached - - headless
python scripts / run_client . py - - install - deps - - verify
"""
from __future__ import annotations
import argparse
import os
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from typing import List , Optional
def get_python_in_venv ( venv_dir : Path ) - > Optional [ Path ] :
try :
v = Path ( venv_dir )
# Windows
win_python = v / " Scripts " / " python.exe "
if win_python . exists ( ) :
return win_python
# Unix
unix_python = v / " bin " / " python "
if unix_python . exists ( ) :
return unix_python
unix_py3 = v / " bin " / " python3 "
if unix_py3 . exists ( ) :
return unix_py3
except Exception :
pass
return None
def find_requirements ( root : Path ) - > Optional [ Path ] :
2025-12-31 22:05:25 -08:00
candidates = [ root / " scripts " / " requirements.txt " , root / " requirements.txt " , root / " client " / " requirements.txt " ]
2025-12-29 17:05:03 -08:00
for c in candidates :
if c . exists ( ) :
return c
# shallow two-level search
try :
for p in root . iterdir ( ) :
if not p . is_dir ( ) :
continue
2025-12-29 18:42:02 -08:00
for child in ( p ,
) :
2025-12-29 17:05:03 -08:00
candidate = child / " requirements.txt "
if candidate . exists ( ) :
return candidate
except Exception :
pass
return None
2025-12-29 18:42:02 -08:00
def install_requirements (
venv_py : Path ,
req_path : Path ,
reinstall : bool = False
) - > bool :
2025-12-29 17:05:03 -08:00
try :
print ( f " Installing { req_path } into venv ( { venv_py } )... " )
2025-12-29 18:42:02 -08:00
subprocess . run (
[ str ( venv_py ) ,
" -m " ,
" pip " ,
" install " ,
" --upgrade " ,
" pip " ] ,
check = True
)
2025-12-29 17:05:03 -08:00
install_cmd = [ str ( venv_py ) , " -m " , " pip " , " install " , " -r " , str ( req_path ) ]
if reinstall :
install_cmd = [
str ( venv_py ) ,
" -m " ,
" pip " ,
" install " ,
" --upgrade " ,
" --force-reinstall " ,
" -r " ,
str ( req_path ) ,
]
subprocess . run ( install_cmd , check = True )
return True
except subprocess . CalledProcessError as e :
print ( " Failed to install requirements: " , e )
return False
def parse_requirements_file ( req_path : Path ) - > List [ str ] :
names : List [ str ] = [ ]
try :
with req_path . open ( " r " , encoding = " utf-8 " ) as fh :
for raw in fh :
line = raw . strip ( )
if not line or line . startswith ( " # " ) :
continue
if line . startswith ( " -e " ) or line . startswith ( " -- " ) :
continue
if " :// " in line or line . startswith ( " file: " ) :
continue
line = line . split ( " ; " ) [ 0 ] . strip ( )
line = line . split ( " [ " ) [ 0 ] . strip ( )
for sep in ( " == " , " >= " , " <= " , " ~= " , " != " , " > " , " < " , " === " ) :
if sep in line :
line = line . split ( sep ) [ 0 ] . strip ( )
if " @ " in line :
line = line . split ( " @ " ) [ 0 ] . strip ( )
if line :
names . append ( line . split ( ) [ 0 ] . strip ( ) . lower ( ) )
except Exception :
pass
return names
def verify_imports ( venv_py : Path , packages : List [ str ] ) - > bool :
# Map some package names to import names (handle common cases where package name differs from import name)
import_map = {
" pyyaml " : " yaml " ,
" pillow " : " PIL " ,
" python-dateutil " : " dateutil " ,
" beautifulsoup4 " : " bs4 " ,
" pillow-heif " : " pillow_heif " ,
" pillow-jxl-plugin " : " pillow_jxl_plugin " ,
" pyopenssl " : " OpenSSL " ,
" pysocks " : " socks " ,
" service-identity " : " service_identity " ,
" show-in-file-manager " : " show_in_file_manager " ,
" opencv-python-headless " : " cv2 " ,
" mpv " : " mpv " ,
" pyside6 " : " PySide6 " ,
}
missing = [ ]
for pkg in packages :
try :
out = subprocess . run (
2025-12-29 18:42:02 -08:00
[ str ( venv_py ) ,
" -m " ,
" pip " ,
" show " ,
pkg ] ,
2025-12-29 17:05:03 -08:00
stdout = subprocess . PIPE ,
stderr = subprocess . PIPE ,
text = True ,
timeout = 10 ,
)
except subprocess . TimeoutExpired :
missing . append ( pkg )
continue
except Exception :
missing . append ( pkg )
continue
if out . returncode != 0 or not out . stdout . strip ( ) :
missing . append ( pkg )
continue
import_name = import_map . get ( pkg , pkg )
try :
subprocess . run (
2025-12-29 18:42:02 -08:00
[ str ( venv_py ) ,
" -c " ,
f " import { import_name } " ] ,
2025-12-29 17:05:03 -08:00
check = True ,
stdout = subprocess . DEVNULL ,
stderr = subprocess . DEVNULL ,
timeout = 10 ,
)
except ( subprocess . CalledProcessError , subprocess . TimeoutExpired ) :
missing . append ( pkg )
if missing :
2025-12-29 18:42:02 -08:00
print (
" The following packages were not importable in the venv: " ,
" , " . join ( missing )
)
2025-12-29 17:05:03 -08:00
return False
return True
def is_first_run ( repo_root : Path ) - > bool :
try :
db_dir = repo_root / " db "
if db_dir . exists ( ) and any ( db_dir . iterdir ( ) ) :
return False
for f in repo_root . glob ( " *.db " ) :
if f . exists ( ) :
return False
except Exception :
return False
return True
# --- Service install/uninstall helpers -----------------------------------
def install_service_windows (
service_name : str ,
repo_root : Path ,
venv_py : Path ,
headless : bool = True ,
detached : bool = True ,
start_on : str = " logon " ,
) - > bool :
try :
schtasks = shutil . which ( " schtasks " )
if not schtasks :
2025-12-29 18:42:02 -08:00
print (
" schtasks not available on this system; cannot install Windows scheduled task. "
)
2025-12-29 17:05:03 -08:00
return False
bat = repo_root / " run-client.bat "
if not bat . exists ( ) :
# Use escaped backslashes to avoid Python "invalid escape sequence" warnings
content = ' @echo off \n " % ~dp0 \\ .venv \\ Scripts \\ python.exe " " % ~dp0hydrus_client.py " % * \n '
bat . write_text ( content , encoding = " utf-8 " )
tr = str ( bat )
sc = " ONLOGON " if start_on == " logon " else " ONSTART "
cmd = [
schtasks ,
" /Create " ,
" /SC " ,
sc ,
" /TN " ,
service_name ,
" /TR " ,
f ' " { tr } " ' ,
" /RL " ,
" LIMITED " ,
" /F " ,
]
subprocess . run ( cmd , check = True )
print ( f " Scheduled task ' { service_name } ' created ( { sc } ). " )
return True
except subprocess . CalledProcessError as e :
print ( " Failed to create scheduled task: " , e )
return False
except Exception as exc :
print ( " Windows install-service error: " , exc )
return False
def uninstall_service_windows ( service_name : str ) - > bool :
try :
schtasks = shutil . which ( " schtasks " )
if not schtasks :
2025-12-29 18:42:02 -08:00
print (
" schtasks not available on this system; cannot remove scheduled task. "
)
2025-12-29 17:05:03 -08:00
return False
cmd = [ schtasks , " /Delete " , " /TN " , service_name , " /F " ]
subprocess . run ( cmd , check = True )
print ( f " Scheduled task ' { service_name } ' removed. " )
return True
except subprocess . CalledProcessError as e :
print ( " Failed to delete scheduled task: " , e )
return False
except Exception as exc :
print ( " Windows uninstall-service error: " , exc )
return False
def install_service_systemd (
2025-12-29 18:42:02 -08:00
service_name : str ,
repo_root : Path ,
venv_py : Path ,
headless : bool = True ,
detached : bool = True
2025-12-29 17:05:03 -08:00
) - > bool :
try :
systemctl = shutil . which ( " systemctl " )
if not systemctl :
2025-12-29 18:42:02 -08:00
print (
" systemctl not available; falling back to crontab @reboot (if present). "
)
return install_service_cron (
service_name ,
repo_root ,
venv_py ,
headless ,
detached
)
2025-12-29 17:05:03 -08:00
unit_dir = Path . home ( ) / " .config " / " systemd " / " user "
unit_dir . mkdir ( parents = True , exist_ok = True )
unit_file = unit_dir / f " { service_name } .service "
exec_args = f ' " { venv_py } " " { str ( repo_root / " run_client.py " ) } " --detached '
exec_args + = " --headless " if headless else " --gui "
content = f " [Unit] \n Description=Hydrus Client (user) \n After=network.target \n \n [Service] \n Type=simple \n ExecStart= { exec_args } \n WorkingDirectory= { str ( repo_root ) } \n Restart=on-failure \n Environment=PYTHONUNBUFFERED=1 \n \n [Install] \n WantedBy=default.target \n "
unit_file . write_text ( content , encoding = " utf-8 " )
subprocess . run ( [ systemctl , " --user " , " daemon-reload " ] , check = True )
subprocess . run (
2025-12-29 18:42:02 -08:00
[ systemctl ,
" --user " ,
" enable " ,
" --now " ,
f " { service_name } .service " ] ,
check = True
2025-12-29 17:05:03 -08:00
)
print ( f " systemd user service ' { service_name } ' installed and started. " )
return True
except subprocess . CalledProcessError as e :
print ( " Failed to create systemd user service: " , e )
return False
except Exception as exc :
print ( " systemd install error: " , exc )
return False
def uninstall_service_systemd ( service_name : str ) - > bool :
try :
systemctl = shutil . which ( " systemctl " )
if not systemctl :
print ( " systemctl not available; cannot uninstall systemd service. " )
return False
subprocess . run (
2025-12-29 18:42:02 -08:00
[ systemctl ,
" --user " ,
" disable " ,
" --now " ,
f " { service_name } .service " ] ,
check = False
2025-12-29 17:05:03 -08:00
)
2025-12-29 18:42:02 -08:00
unit_file = Path . home (
) / " .config " / " systemd " / " user " / f " { service_name } .service "
2025-12-29 17:05:03 -08:00
if unit_file . exists ( ) :
unit_file . unlink ( )
subprocess . run ( [ systemctl , " --user " , " daemon-reload " ] , check = True )
print ( f " systemd user service ' { service_name } ' removed. " )
return True
except Exception as exc :
print ( " systemd uninstall error: " , exc )
return False
def install_service_cron (
2025-12-29 18:42:02 -08:00
service_name : str ,
repo_root : Path ,
venv_py : Path ,
headless : bool = True ,
detached : bool = True
2025-12-29 17:05:03 -08:00
) - > bool :
try :
crontab = shutil . which ( " crontab " )
if not crontab :
print ( " crontab not available; cannot install reboot cron job. " )
return False
entry = f " @reboot { venv_py } { str ( repo_root / ' run_client.py ' ) } --detached { ' --headless ' if headless else ' --gui ' } # { service_name } \n "
proc = subprocess . run (
2025-12-29 18:42:02 -08:00
[ crontab ,
" -l " ] ,
stdout = subprocess . PIPE ,
stderr = subprocess . PIPE ,
text = True
2025-12-29 17:05:03 -08:00
)
existing = proc . stdout if proc . returncode == 0 else " "
if entry . strip ( ) in existing :
print ( " Crontab entry already present; skipping. " )
return True
new = existing + " \n " + entry
subprocess . run ( [ crontab , " - " ] , input = new , text = True , check = True )
print ( f " Crontab @reboot entry added for ' { service_name } ' . " )
return True
except subprocess . CalledProcessError as e :
print ( " Failed to install crontab entry: " , e )
return False
except Exception as exc :
print ( " crontab install error: " , exc )
return False
def uninstall_service_cron ( service_name : str , repo_root : Path , venv_py : Path ) - > bool :
try :
crontab = shutil . which ( " crontab " )
if not crontab :
print ( " crontab not available; cannot remove reboot cron job. " )
return False
proc = subprocess . run (
2025-12-29 18:42:02 -08:00
[ crontab ,
" -l " ] ,
stdout = subprocess . PIPE ,
stderr = subprocess . PIPE ,
text = True
2025-12-29 17:05:03 -08:00
)
if proc . returncode != 0 :
print ( " No crontab found for user; nothing to remove. " )
return True
lines = [ l for l in proc . stdout . splitlines ( ) if f " # { service_name } " not in l ]
new = " \n " . join ( lines ) + " \n "
subprocess . run ( [ crontab , " - " ] , input = new , text = True , check = True )
print ( f " Crontab entry for ' { service_name } ' removed. " )
return True
except subprocess . CalledProcessError as e :
print ( " Failed to modify crontab: " , e )
return False
except Exception as exc :
print ( " crontab uninstall error: " , exc )
return False
def install_service_auto (
2025-12-29 18:42:02 -08:00
service_name : str ,
repo_root : Path ,
venv_py : Path ,
headless : bool = True ,
detached : bool = True
2025-12-29 17:05:03 -08:00
) - > bool :
try :
if os . name == " nt " :
return install_service_windows (
2025-12-29 18:42:02 -08:00
service_name ,
repo_root ,
venv_py ,
headless = headless ,
detached = detached
2025-12-29 17:05:03 -08:00
)
else :
if shutil . which ( " systemctl " ) :
return install_service_systemd (
2025-12-29 18:42:02 -08:00
service_name ,
repo_root ,
venv_py ,
headless = headless ,
detached = detached
2025-12-29 17:05:03 -08:00
)
else :
return install_service_cron (
2025-12-29 18:42:02 -08:00
service_name ,
repo_root ,
venv_py ,
headless = headless ,
detached = detached
2025-12-29 17:05:03 -08:00
)
except Exception as exc :
print ( " install_service_auto error: " , exc )
return False
def uninstall_service_auto ( service_name : str , repo_root : Path , venv_py : Path ) - > bool :
try :
if os . name == " nt " :
return uninstall_service_windows ( service_name )
else :
if shutil . which ( " systemctl " ) :
return uninstall_service_systemd ( service_name )
else :
return uninstall_service_cron ( service_name , repo_root , venv_py )
except Exception as exc :
print ( " uninstall_service_auto error: " , exc )
return False
2025-12-29 18:42:02 -08:00
def print_activation_instructions (
repo_root : Path ,
venv_dir : Path ,
venv_py : Path
) - > None :
2025-12-29 17:05:03 -08:00
print ( " \n Activation and run examples: " )
# PowerShell
print ( f " PowerShell: \n . { shlex . quote ( str ( venv_dir ) ) } \\ Scripts \\ Activate.ps1 " )
# CMD
print ( f " CMD: \n { str ( venv_dir ) } \\ Scripts \\ activate.bat " )
# Bash
print ( f " Bash (Linux/macOS/WSL): \n source { str ( venv_dir ) } /bin/activate " )
print (
f " \n Direct run without activating: \n { str ( venv_py ) } { str ( repo_root / ' hydrus_client.py ' ) } "
)
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
else :
kwargs [ " start_new_session " ] = True
return kwargs
2025-12-29 18:42:02 -08:00
def find_venv_python ( repo_root : Path ,
venv_arg : Optional [ str ] ,
venv_name : str ) - > Optional [ Path ] :
2025-12-29 17:05:03 -08:00
# venv_arg may be a python executable or a directory
if venv_arg :
p = Path ( venv_arg )
if p . exists ( ) :
if p . is_file ( ) :
return p
else :
found = get_python_in_venv ( p )
if found :
return found
# Try repo-local venv
dir_candidate = repo_root / venv_name
found = get_python_in_venv ( dir_candidate )
if found :
return found
# Fallback: if current interpreter is inside repo venv
try :
cur = Path ( sys . executable ) . resolve ( )
if repo_root in cur . parents :
return cur
except Exception :
pass
return None
def _python_can_import ( python_exe : Path , modules : List [ str ] ) - > bool :
""" Return True if the given python executable can import all modules in the list.
Uses a subprocess to avoid side - effects in the current interpreter .
"""
if not python_exe :
return False
try :
# Build a short import test string. Use semicolons to ensure any import error results in non-zero exit.
imports = " ; " . join ( [ f " import { m } " for m in modules ] )
out = subprocess . run (
2025-12-29 18:42:02 -08:00
[ str ( python_exe ) ,
" -c " ,
imports ] ,
2025-12-29 17:05:03 -08:00
stdout = subprocess . DEVNULL ,
stderr = subprocess . DEVNULL ,
timeout = 10 ,
)
return out . returncode == 0
except ( subprocess . TimeoutExpired , Exception ) :
return False
def main ( argv : Optional [ List [ str ] ] = None ) - > int :
p = argparse . ArgumentParser (
2025-12-29 18:42:02 -08:00
description =
" Run hydrus_client.py using the repo-local venv Python (top-level helper) "
)
p . add_argument (
" --venv " ,
help = " Path to venv dir or python executable (overrides default .venv) "
2025-12-29 17:05:03 -08:00
)
p . add_argument (
2025-12-29 18:42:02 -08:00
" --venv-name " ,
default = " .venv " ,
help = " Name of the venv folder to look for (default: .venv) "
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --client " ,
default = " hydrus_client.py " ,
help = " Path to hydrus_client.py relative to repo root " ,
)
p . add_argument (
" --repo-root " ,
default = None ,
help = " Path to the hydrus repository root (overrides auto-detection) " ,
)
p . add_argument (
" --install-deps " ,
action = " store_true " ,
help = " Install requirements.txt into the venv before running " ,
)
p . add_argument (
" --reinstall " ,
action = " store_true " ,
2025-12-29 18:42:02 -08:00
help =
" Force re-install dependencies from requirements.txt into the venv (uses --force-reinstall) " ,
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --verify " ,
action = " store_true " ,
2025-12-29 18:42:02 -08:00
help =
" Verify that packages from requirements.txt are importable in the venv (after install) " ,
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --no-verify " ,
action = " store_true " ,
2025-12-29 18:42:02 -08:00
help =
" Skip verification and do not prompt to install missing dependencies; proceed to run with the chosen Python " ,
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --headless " ,
action = " store_true " ,
2025-12-29 18:42:02 -08:00
help =
" Attempt to launch the client without showing the Qt GUI (best-effort). Default for subsequent runs; first run will show GUI unless --headless is supplied " ,
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --gui " ,
action = " store_true " ,
help = " Start the client with the GUI visible (overrides headless/default) " ,
)
p . add_argument (
2025-12-29 18:42:02 -08:00
" --detached " ,
action = " store_true " ,
help = " Start the client and do not wait (detached) "
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --install-service " ,
action = " store_true " ,
2025-12-29 18:42:02 -08:00
help =
" Install a user-level start-on-boot service/scheduled task for the hydrus client " ,
2025-12-29 17:05:03 -08:00
)
p . add_argument (
" --uninstall-service " ,
action = " store_true " ,
help = " Remove an installed start-on-boot service/scheduled task " ,
)
p . add_argument (
" --service-name " ,
default = " hydrus-client " ,
help = " Name of the service / scheduled task to install (default: hydrus-client) " ,
)
p . add_argument (
2025-12-29 18:42:02 -08:00
" --cwd " ,
default = None ,
help = " Working directory to start the client in (default: repo root) "
2025-12-29 17:05:03 -08:00
)
p . add_argument ( " --quiet " , action = " store_true " , help = " Reduce output " )
p . add_argument (
" client_args " ,
nargs = argparse . REMAINDER ,
help = " Arguments to pass to hydrus_client.py (prefix with --) " ,
)
args = p . parse_args ( argv )
workspace_root = Path ( __file__ ) . resolve ( ) . parent . parent
# Determine default repo root: prefer <workspace>/hydrusnetwork when present
if args . repo_root :
repo_root = Path ( args . repo_root ) . expanduser ( ) . resolve ( )
else :
candidate = workspace_root / " hydrusnetwork "
if candidate . exists ( ) :
repo_root = candidate
else :
repo_root = workspace_root
venv_py = find_venv_python ( repo_root , args . venv , args . venv_name )
def _is_running_in_virtualenv ( ) - > bool :
try :
2025-12-29 18:42:02 -08:00
return hasattr ( sys ,
" real_prefix " ) or getattr ( sys ,
" base_prefix " ,
None
) != getattr ( sys ,
" prefix " ,
None )
2025-12-29 17:05:03 -08:00
except Exception :
return False
# 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 ...
cur_py = Path ( sys . executable )
if args . venv is None and _is_running_in_virtualenv ( ) and cur_py :
# 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 [ ]
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 " ] )
if ok_cur :
venv_py = cur_py
if not args . quiet :
print ( f " Using current Python interpreter as venv: { cur_py } " )
# 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 not args . quiet :
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 " ]
try :
ok_venv = verify_imports ( venv_py , check_pkgs )
except Exception :
ok_venv = _python_can_import ( venv_py , [ " yaml " ] )
if not ok_venv :
try :
ok_cur = verify_imports ( cur_py , check_pkgs )
except Exception :
ok_cur = _python_can_import ( cur_py , [ " yaml " ] )
if ok_cur :
if not args . quiet :
print (
f " Repository venv ( { venv_py } ) is missing required packages; using current Python at { cur_py } instead. "
)
venv_py = cur_py
else :
print (
" Warning: repository venv appears to be missing required packages. If the client fails to start, run this helper with --install-deps to install requirements into the repo venv, or use --venv to point to a Python that has the deps. "
)
if not venv_py :
print ( " Could not locate a repository venv. " )
print (
" Create one with: python -m venv .venv (inside your hydrus repo) and then re-run this helper, or use the installer to create it for you. "
)
print_activation_instructions (
2025-12-29 18:42:02 -08:00
repo_root ,
repo_root / args . venv_name ,
repo_root / args . venv_name
2025-12-29 17:05:03 -08:00
)
return 2
client_path = ( repo_root / args . client ) . resolve ( )
if not client_path . exists ( ) :
print ( f " Client file not found: { client_path } " )
return 3
cwd = Path ( args . cwd ) . resolve ( ) if args . cwd else repo_root
# Optionally install dependencies
if args . install_deps or args . reinstall :
req = find_requirements ( repo_root )
if not req :
print ( " No requirements.txt found; skipping install " )
else :
ok = install_requirements ( venv_py , req , reinstall = args . reinstall )
if not ok :
print ( " Dependency installation failed; aborting " )
return 4
if args . verify :
pkgs = parse_requirements_file ( req )
if pkgs :
okv = verify_imports ( venv_py , pkgs )
if not okv :
2025-12-29 18:42:02 -08:00
print (
" Verification failed; see instructions above to re-run installation. "
)
2025-12-29 17:05:03 -08:00
# If not installing but user asked to verify, do verification only
if args . verify and not ( args . install_deps or args . reinstall ) :
req = find_requirements ( repo_root )
if req :
pkgs = parse_requirements_file ( req )
if pkgs and not verify_imports ( venv_py , pkgs ) :
print (
" Verification found missing packages. Use --install-deps to install into the venv. "
)
# 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 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.
# Service install/uninstall requests
if args . install_service or args . uninstall_service :
first_run = is_first_run ( repo_root )
if args . gui :
use_headless = False
elif args . headless :
use_headless = True
else :
use_headless = not first_run
if args . install_service :
ok = install_service_auto (
2025-12-29 18:42:02 -08:00
args . service_name ,
repo_root ,
venv_py ,
headless = use_headless ,
detached = True
2025-12-29 17:05:03 -08:00
)
return 0 if ok else 6
if args . uninstall_service :
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
first_run = is_first_run ( repo_root )
if args . gui :
headless = False
elif args . headless :
headless = True
else :
headless = not first_run
if not args . quiet and first_run :
print ( " First run detected: defaulting to GUI unless --headless is specified. " )
env = os . environ . copy ( )
if headless :
if os . name == " posix " and shutil . which ( " xvfb-run " ) :
2025-12-29 18:42:02 -08:00
xvfb_cmd = [
" xvfb-run " ,
" --auto-servernum " ,
" --server-args=-screen 0 1024x768x24 "
]
2025-12-29 17:05:03 -08:00
cmd = xvfb_cmd + cmd
if not args . quiet :
print ( " Headless: using xvfb-run to provide a virtual X server " )
else :
env [ " QT_QPA_PLATFORM " ] = " offscreen "
if not args . quiet :
print ( " Headless: setting QT_QPA_PLATFORM=offscreen (best-effort) " )
# Inform which Python will be used
if not args . quiet :
try :
print ( f " Launching Hydrus client with Python: { venv_py } " )
print ( f " Command: { ' ' . join ( shlex . quote ( str ( c ) ) for c in cmd ) } " )
except Exception :
pass
# Launch
if args . detached :
try :
kwargs = detach_kwargs_for_platform ( )
2025-12-29 18:42:02 -08:00
kwargs . update ( {
" cwd " : str ( cwd ) ,
" env " : env
} )
2025-12-29 17:05:03 -08:00
subprocess . Popen ( cmd , * * kwargs )
print ( " Hydrus client launched (detached). " )
return 0
except Exception as exc :
print ( " Failed to launch client detached: " , exc )
return 5
else :
try :
subprocess . run ( cmd , cwd = str ( cwd ) , env = env )
return 0
except subprocess . CalledProcessError as e :
print ( " hydrus client exited non-zero: " , e )
return 5
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )