#!/usr/bin/env python3 import json import logging import os import subprocess import sys from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse import psutil # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Allowlisted applications that can be launched ALLOWED_APPS = { 'firefox': 'firefox', 'kodi': 'kodi' } def is_app_running(app_name): """Check if an application is already running, returns (is_running, pid)""" command = ALLOWED_APPS.get(app_name) if not command: return False, None logger.debug(f"Looking for processes related to app '{app_name}' (command: '{command}')") for proc in psutil.process_iter(['name', 'cmdline', 'pid']): try: proc_name = proc.info['name'] cmdline = proc.info['cmdline'] or [] logger.debug(f"Checking process PID {proc.info['pid']}: name='{proc_name}', cmdline={cmdline}") # Check multiple patterns for the application: # 1. Process name exactly matches command # 2. Process name contains the command (e.g., "kodi.bin" contains "kodi") # 3. Command line starts with the command # 4. Command line contains the wrapped version (e.g., ".kodi-wrapped") # 5. Any command line argument ends with the command executable matches = False match_reason = "" if proc_name == command: matches = True match_reason = f"exact process name match: '{proc_name}'" elif command in proc_name: matches = True match_reason = f"process name contains command: '{proc_name}' contains '{command}'" elif cmdline and cmdline[0] == command: matches = True match_reason = f"exact cmdline match: '{cmdline[0]}'" elif cmdline and cmdline[0].endswith('/' + command): matches = True match_reason = f"cmdline path ends with command: '{cmdline[0]}'" elif cmdline and any(f'.{command}-wrapped' in arg for arg in cmdline): matches = True match_reason = f"wrapped command in cmdline: {cmdline}" elif cmdline and any(f'{command}.bin' in arg for arg in cmdline): matches = True match_reason = f"binary command in cmdline: {cmdline}" if matches: logger.info(f"Found running {app_name} process: PID {proc.info['pid']} ({match_reason})") return True, proc.info['pid'] except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): continue logger.debug(f"No running process found for {app_name}") return False, None class AppLauncherHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): logger.info(format % args) def do_GET(self): if self.path == '/': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = { 'status': 'running', 'available_apps': list(ALLOWED_APPS.keys()), 'usage': 'POST /launch/ to launch an application' } self.wfile.write(json.dumps(response, indent=2).encode()) else: self.send_error(404) def do_POST(self): parsed_path = urlparse(self.path) path_parts = parsed_path.path.strip('/').split('/') if len(path_parts) == 2 and path_parts[0] == 'launch': app_name = path_parts[1] self.launch_app(app_name) else: self.send_error(404, "Invalid endpoint. Use /launch/") def launch_app(self, app_name): if app_name not in ALLOWED_APPS: self.send_error(400, f"Application '{app_name}' not allowed. Available apps: {list(ALLOWED_APPS.keys())}") return command = ALLOWED_APPS[app_name] # Check if app is already running is_running, existing_pid = is_app_running(app_name) if is_running: logger.info(f"Application {app_name} is already running (PID: {existing_pid}), skipping launch") self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = { 'status': 'success', 'message': f'{app_name} is already running', 'pid': existing_pid, 'already_running': True } self.wfile.write(json.dumps(response).encode()) return try: # Launch the application in the background # Ensure we have the proper environment for GUI apps env = os.environ.copy() logger.info(f"Launching application: {command}") process = subprocess.Popen( [command], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True ) self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = { 'status': 'success', 'message': f'Successfully launched {app_name}', 'pid': process.pid, 'already_running': False } self.wfile.write(json.dumps(response).encode()) except FileNotFoundError: logger.error(f"Application not found: {command}") self.send_error(500, f"Application '{app_name}' not found on system") except Exception as e: logger.error(f"Error launching {command}: {e}") self.send_error(500, f"Failed to launch {app_name}: {str(e)}") def main(): port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081 server = HTTPServer(('0.0.0.0', port), AppLauncherHandler) logger.info(f"App launcher server starting on port {port}") logger.info(f"Available applications: {list(ALLOWED_APPS.keys())}") try: server.serve_forever() except KeyboardInterrupt: logger.info("Server shutting down...") server.server_close() if __name__ == '__main__': main()