feat: add Termux/Android prerequisite detection
Detects when running in Termux on Android and checks for required prerequisites before starting the server: - Termux:API package installed - Location services enabled and accessible - Wake lock available Exits with informative error message if prerequisites not met. Adds `rf-mapper check-termux` command for manual verification. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,9 @@ Note: Requires sudo for WiFi/Bluetooth scanning.
|
|||||||
web_parser.add_argument('--profile-requests', action='store_true', help='Enable profiling')
|
web_parser.add_argument('--profile-requests', action='store_true', help='Enable profiling')
|
||||||
web_parser.add_argument('--log-requests', action='store_true', help='Log requests')
|
web_parser.add_argument('--log-requests', action='store_true', help='Log requests')
|
||||||
|
|
||||||
|
# Check-termux command
|
||||||
|
subparsers.add_parser('check-termux', help='Check Termux/Android prerequisites')
|
||||||
|
|
||||||
# Config command
|
# Config command
|
||||||
config_parser = subparsers.add_parser('config', help='Show/edit configuration')
|
config_parser = subparsers.add_parser('config', help='Show/edit configuration')
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
@@ -209,6 +212,8 @@ Note: Requires sudo for WiFi/Bluetooth scanning.
|
|||||||
run_status(data_dir)
|
run_status(data_dir)
|
||||||
elif args.command == 'config':
|
elif args.command == 'config':
|
||||||
run_config(args, config)
|
run_config(args, config)
|
||||||
|
elif args.command == 'check-termux':
|
||||||
|
run_check_termux()
|
||||||
elif args.command == 'web':
|
elif args.command == 'web':
|
||||||
run_web_deprecated(args, config, data_dir)
|
run_web_deprecated(args, config, data_dir)
|
||||||
else:
|
else:
|
||||||
@@ -436,6 +441,19 @@ Home Assistant:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def run_check_termux():
|
||||||
|
"""Check Termux/Android prerequisites"""
|
||||||
|
from .termux import is_termux, check_termux_prerequisites
|
||||||
|
|
||||||
|
if not is_termux():
|
||||||
|
print("Not running in Termux/Android environment.")
|
||||||
|
print("This check is only relevant when running on Android via Termux.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
success = check_termux_prerequisites(verbose=True)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
def get_pid_file(data_dir: Path) -> Path:
|
def get_pid_file(data_dir: Path) -> Path:
|
||||||
"""Get path to PID file"""
|
"""Get path to PID file"""
|
||||||
return data_dir / "rf-mapper.pid"
|
return data_dir / "rf-mapper.pid"
|
||||||
@@ -534,6 +552,16 @@ def run_start(args, config: Config, data_dir: Path):
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# Check Termux prerequisites if running on Android
|
||||||
|
from .termux import is_termux, check_termux_prerequisites, setup_termux_signal_handlers
|
||||||
|
|
||||||
|
if is_termux():
|
||||||
|
if not check_termux_prerequisites(verbose=True):
|
||||||
|
print("Exiting due to missing prerequisites.")
|
||||||
|
sys.exit(1)
|
||||||
|
# Set up signal handlers for graceful shutdown
|
||||||
|
setup_termux_signal_handlers()
|
||||||
|
|
||||||
host = args.host or config.web.host
|
host = args.host or config.web.host
|
||||||
port = args.port or config.web.port
|
port = args.port or config.web.port
|
||||||
debug = getattr(args, 'debug', False)
|
debug = getattr(args, 'debug', False)
|
||||||
|
|||||||
226
src/rf_mapper/termux.py
Normal file
226
src/rf_mapper/termux.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
"""Termux/Android environment detection and prerequisite checks."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def is_termux() -> bool:
|
||||||
|
"""Detect if running in Termux on Android."""
|
||||||
|
# Check for Termux environment variable
|
||||||
|
if os.environ.get("TERMUX_VERSION"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for Termux-specific paths
|
||||||
|
termux_paths = [
|
||||||
|
"/data/data/com.termux",
|
||||||
|
Path.home() / ".termux",
|
||||||
|
Path("/data/data/com.termux/files/usr"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in termux_paths:
|
||||||
|
if Path(path).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check PREFIX environment variable (Termux sets this)
|
||||||
|
prefix = os.environ.get("PREFIX", "")
|
||||||
|
if "/com.termux/" in prefix:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_termux_api_installed() -> tuple[bool, str]:
|
||||||
|
"""Check if termux-api package and Termux:API app are installed."""
|
||||||
|
# Check for termux-api command
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["which", "termux-location"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, "termux-api package not installed. Run: pkg install termux-api"
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
return False, "termux-api package not installed. Run: pkg install termux-api"
|
||||||
|
|
||||||
|
# Test if Termux:API app is working (quick test with battery status)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["termux-battery-status"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode != 0 or "error" in result.stderr.lower():
|
||||||
|
return False, "Termux:API app not installed or not granted permissions"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Termux:API app not responding. Install from F-Droid and grant permissions"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "termux-api package not installed. Run: pkg install termux-api"
|
||||||
|
|
||||||
|
return True, "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def check_location_enabled() -> tuple[bool, str]:
|
||||||
|
"""Check if location services are accessible via termux-api."""
|
||||||
|
try:
|
||||||
|
# Use termux-location with a short timeout
|
||||||
|
result = subprocess.run(
|
||||||
|
["termux-location", "-p", "passive", "-r", "once"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result.stdout.strip()
|
||||||
|
|
||||||
|
# Check for common error patterns
|
||||||
|
if not output:
|
||||||
|
return False, "Location services not responding. Enable GPS/Location in Android settings"
|
||||||
|
|
||||||
|
if "null" in output.lower() and "latitude" not in output.lower():
|
||||||
|
return False, "Location unavailable. Enable GPS and grant Termux:API location permission"
|
||||||
|
|
||||||
|
# Try to parse as JSON to verify it's valid location data
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
data = json.loads(output)
|
||||||
|
if data.get("latitude") is not None:
|
||||||
|
return True, f"OK (lat: {data.get('latitude'):.4f})"
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Location might still be initializing
|
||||||
|
return True, "OK (location services accessible)"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Location request timed out. Enable GPS in Android settings"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "termux-location not found. Run: pkg install termux-api"
|
||||||
|
|
||||||
|
|
||||||
|
def check_wake_lock() -> tuple[bool, str]:
|
||||||
|
"""Check if wake lock can be acquired."""
|
||||||
|
try:
|
||||||
|
# Try to acquire wake lock
|
||||||
|
result = subprocess.run(
|
||||||
|
["termux-wake-lock"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, f"Failed to acquire wake lock: {result.stderr.strip()}"
|
||||||
|
|
||||||
|
return True, "OK (wake lock acquired)"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Wake lock request timed out"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "termux-wake-lock not found. Run: pkg install termux-api"
|
||||||
|
|
||||||
|
|
||||||
|
def check_termux_boot() -> tuple[bool, str]:
|
||||||
|
"""Check if Termux:Boot is available for auto-start capability."""
|
||||||
|
boot_dir = Path.home() / ".termux" / "boot"
|
||||||
|
|
||||||
|
if boot_dir.exists():
|
||||||
|
return True, "OK (boot directory exists)"
|
||||||
|
|
||||||
|
# Not critical, just informational
|
||||||
|
return True, "Termux:Boot not configured (optional for auto-start)"
|
||||||
|
|
||||||
|
|
||||||
|
def check_termux_prerequisites(verbose: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
Check all Termux prerequisites for RF Mapper.
|
||||||
|
|
||||||
|
Returns True if all required checks pass, False otherwise.
|
||||||
|
Prints status messages if verbose=True.
|
||||||
|
"""
|
||||||
|
if not is_termux():
|
||||||
|
# Not running in Termux, skip checks
|
||||||
|
return True
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("TERMUX ENVIRONMENT DETECTED")
|
||||||
|
print("Checking prerequisites...")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
checks = [
|
||||||
|
("Termux:API package", check_termux_api_installed, True), # Required
|
||||||
|
("Location services", check_location_enabled, True), # Required
|
||||||
|
("Wake lock", check_wake_lock, True), # Required
|
||||||
|
("Termux:Boot", check_termux_boot, False), # Optional
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, check_func, required in checks:
|
||||||
|
try:
|
||||||
|
ok, message = check_func()
|
||||||
|
status = "✓" if ok else ("✗" if required else "⚠")
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f" {status} {name}: {message}")
|
||||||
|
|
||||||
|
if not ok and required:
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if verbose:
|
||||||
|
print(f" ✗ {name}: Error - {e}")
|
||||||
|
if required:
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
print("All prerequisites met. Starting RF Mapper...")
|
||||||
|
else:
|
||||||
|
print("\nREQUIRED PREREQUISITES NOT MET")
|
||||||
|
print("\nTo fix:")
|
||||||
|
print(" 1. Install Termux:API from F-Droid")
|
||||||
|
print(" (NOT from Play Store - versions must match)")
|
||||||
|
print(" 2. Run: pkg install termux-api")
|
||||||
|
print(" 3. Enable Location in Android Settings")
|
||||||
|
print(" 4. Grant Termux:API location permission")
|
||||||
|
print(" 5. Run: termux-wake-lock")
|
||||||
|
print("\nFor auto-start on boot:")
|
||||||
|
print(" 1. Install Termux:Boot from F-Droid")
|
||||||
|
print(" 2. mkdir -p ~/.termux/boot")
|
||||||
|
print(" 3. Create boot script: ~/.termux/boot/start-rf-mapper.sh")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return all_ok
|
||||||
|
|
||||||
|
|
||||||
|
def setup_termux_signal_handlers():
|
||||||
|
"""Set up signal handlers for graceful shutdown in Termux."""
|
||||||
|
import signal
|
||||||
|
|
||||||
|
def handle_signal(signum, frame):
|
||||||
|
"""Release wake lock and exit gracefully."""
|
||||||
|
try:
|
||||||
|
subprocess.run(["termux-wake-unlock"], capture_output=True, timeout=5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, handle_signal)
|
||||||
|
signal.signal(signal.SIGINT, handle_signal)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Allow running as standalone check
|
||||||
|
if is_termux():
|
||||||
|
success = check_termux_prerequisites(verbose=True)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
else:
|
||||||
|
print("Not running in Termux environment")
|
||||||
|
sys.exit(0)
|
||||||
Reference in New Issue
Block a user