From 4e63c9dd6a9f194480950785af7003592c0b98c6 Mon Sep 17 00:00:00 2001 From: Zayadul-huq-afnan Date: Wed, 18 Mar 2026 02:58:05 +0600 Subject: [PATCH 1/2] Added an new endpoint to download installer related logs --- Framework/install_handler/android/adb.py | 17 +- .../install_handler/android/android_sdk.py | 95 ++++----- Framework/install_handler/android/appium.py | 36 ++-- Framework/install_handler/android/emulator.py | 195 +++++++++--------- Framework/install_handler/android/java.py | 101 +++++---- Framework/install_handler/android/jdk.py | 134 ++++++------ .../install_handler/install_log_config.py | 84 ++++++++ Framework/install_handler/ios/simulator.py | 76 +++---- Framework/install_handler/ios/webdriver.py | 14 +- Framework/install_handler/ios/xcode.py | 7 +- Framework/install_handler/linux/atspi.py | 7 +- .../install_handler/long_poll_handler.py | 11 +- Framework/install_handler/macos/common.py | 7 +- Framework/install_handler/macos/xcode.py | 8 +- .../system_info/system_info.py | 28 +-- .../install_handler/web/chrome_for_testing.py | 25 ++- Framework/install_handler/web/edge.py | 104 +++++----- Framework/install_handler/web/mozilla.py | 120 ++++++----- .../install_handler/windows/inspector.py | 19 +- node_cli.py | 2 +- server/installers.py | 25 ++- 21 files changed, 624 insertions(+), 491 deletions(-) create mode 100644 Framework/install_handler/install_log_config.py diff --git a/Framework/install_handler/android/adb.py b/Framework/install_handler/android/adb.py index 2eef984a..080f9b2f 100644 --- a/Framework/install_handler/android/adb.py +++ b/Framework/install_handler/android/adb.py @@ -2,13 +2,16 @@ import asyncio import platform import os +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from Framework.install_handler.android.android_sdk import update_android_sdk_path +logger = get_logger() + async def check_status() -> bool: """Check if ADB (Android Debug Bridge) is installed.""" - print("[installer][android-adb] Checking status...") + logger.info("[installer][android-adb] Checking status...") try: @@ -26,7 +29,7 @@ async def check_status() -> bool: # If command succeeds (returncode = 0), ADB is installed if result.returncode == 0: version_output = (result.stdout or result.stderr).strip() - print(f"[installer][android-adb] Already installed") + logger.info("[installer][android-adb] Already installed") await send_response({ "action": "status", "data": { @@ -38,7 +41,7 @@ async def check_status() -> bool: }) return True else: - print("[installer][android-adb] Not installed") + logger.info("[installer][android-adb] Not installed") await send_response({ "action": "status", "data": { @@ -50,7 +53,7 @@ async def check_status() -> bool: }) return False except Exception as e: - print(f"[installer][android-adb] Error checking status: {e}") + logger.error("[installer][android-adb] Error checking status: %s", e) await send_response({ "action": "status", "data": { @@ -67,15 +70,15 @@ async def check_status() -> bool: async def install(): """Install ADB - checks if already installed, otherwise prompts to install Android SDK.""" - print("[installer][android-adb] Installing...") + logger.info("[installer][android-adb] Installing...") # Check if ADB is already installed if await check_status(): - print("[installer][android-adb] ADB is already installed") + logger.info("[installer][android-adb] ADB is already installed") return # ADB is not installed, send response to install Android SDK - print("[installer][android-adb] ADB is not installed. Install Android SDK to get ADB.") + logger.info("[installer][android-adb] ADB is not installed. Install Android SDK to get ADB.") await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/android/android_sdk.py b/Framework/install_handler/android/android_sdk.py index 28d11d1a..5ceba203 100644 --- a/Framework/install_handler/android/android_sdk.py +++ b/Framework/install_handler/android/android_sdk.py @@ -4,22 +4,26 @@ import shutil import zipfile import subprocess +import traceback from pathlib import Path import httpx +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from settings import ZEUZ_NODE_DOWNLOADS_DIR +logger = get_logger() + async def check_status() -> bool: """Check if Android SDK is installed in isolated directory (following Node.js installer pattern).""" - print("[installer][android-sdk] Checking status...") + logger.info("[installer][android-sdk] Checking status...") # Simple file existence check in isolated directory (like Node.js installer) adb_path = get_adb_path() if adb_path.exists(): sdk_root = _get_sdk_root() - print(f"[installer][android-sdk] Already installed at {sdk_root}") + logger.info("[installer][android-sdk] Already installed at %s", sdk_root) await send_response({ @@ -34,7 +38,7 @@ async def check_status() -> bool: return True # Not installed - print("[installer][android-sdk] Not installed") + logger.info("[installer][android-sdk] Not installed") await send_response({ "action": "status", "data": { @@ -73,13 +77,13 @@ def update_android_sdk_path(): # Check if SDK exists adb_path = get_adb_path() if not adb_path.exists(): - print("[installer][android-sdk] Warning: Android SDK not found for PATH update.") + logger.warning("[installer][android-sdk] Warning: Android SDK not found for PATH update.") return # Set ANDROID_HOME and ANDROID_SDK_ROOT for current process os.environ['ANDROID_HOME'] = str(sdk_root) os.environ['ANDROID_SDK_ROOT'] = str(sdk_root) - print(f"[installer][android-sdk] ANDROID_HOME set for current process: {sdk_root}") + logger.info("[installer][android-sdk] ANDROID_HOME set for current process: %s", sdk_root) # Add SDK paths to PATH for current process (prepend so they take precedence) sdk_paths = [ @@ -93,7 +97,7 @@ def update_android_sdk_path(): # Always prepend to ensure isolated SDK takes precedence (even if path already exists) os.environ['PATH'] = f"{sdk_path}{os.pathsep}{current_path}" current_path = os.environ['PATH'] - print(f"[installer][android-sdk] Prepended to current process PATH: {sdk_path}") + logger.info("[installer][android-sdk] Prepended to current process PATH: %s", sdk_path) def _get_cmdline_tools_url() -> str: @@ -117,7 +121,7 @@ async def _download_cmdline_tools(archive_path: Path) -> bool: archive_path.parent.mkdir(parents=True, exist_ok=True) - print(f"[installer][android-sdk] Downloading Android Command Line Tools to {archive_path}...") + logger.info("[installer][android-sdk] Downloading Android Command Line Tools to %s...", archive_path) await send_response({ "action": "status", "data": { @@ -136,19 +140,19 @@ async def _download_cmdline_tools(archive_path: Path) -> bool: total_size = int(response.headers.get("content-length", 0)) downloaded = 0 chunk = 8192 - counts = [] + last_pct = [-1] with open(archive_path, "wb") as f: async for data in response.aiter_bytes(chunk): f.write(data) downloaded += len(data) if total_size > 0: progress = (downloaded / total_size) * 100 - mb_d = downloaded / (1024 * 1024) - mb_t = total_size / (1024 * 1024) - print(f"\r[installer][android-sdk] Download {progress:.1f}% ({mb_d:.1f}/{mb_t:.1f} MB)", end='', flush=True) - p = round(mb_d/mb_t, 1) - if p not in counts: - counts.append(p) + pct = int(progress) + if pct != last_pct[0]: + last_pct[0] = pct + mb_d = downloaded / (1024 * 1024) + mb_t = total_size / (1024 * 1024) + logger.info("[installer][android-sdk] Download %d%% (%.1f/%.1f MB)", pct, mb_d, mb_t) await send_response({ "action": "status", "data": { @@ -158,11 +162,10 @@ async def _download_cmdline_tools(archive_path: Path) -> bool: "comment": f"Downloading Android Command Line Tools... {progress:.1f}% ({mb_d:.1f}/{mb_t:.1f} MB)", } }) - print() - print(f"[installer][android-sdk] Download complete: {archive_path}") + logger.info("[installer][android-sdk] Download complete: %s", archive_path) return True except Exception as e: - print(f"\n[installer][android-sdk] Download failed: {e}") + logger.error("[installer][android-sdk] Download failed: %s", e) await send_response({ "action": "status", "data": { @@ -206,7 +209,7 @@ async def _extract_cmdline_tools(archive_path: Path, sdk_root: Path) -> bool: # If already extracted, clean stale zip and exit success sdkmanager = _find_executable(latest_dir / "bin", "sdkmanager") if sdkmanager: - print("[installer][android-sdk] Command Line Tools already extracted") + logger.info("[installer][android-sdk] Command Line Tools already extracted") try: if archive_path.exists(): archive_path.unlink() @@ -215,7 +218,7 @@ async def _extract_cmdline_tools(archive_path: Path, sdk_root: Path) -> bool: return True - print("[installer][android-sdk] Extracting Android Command Line Tools...") + logger.info("[installer][android-sdk] Extracting Android Command Line Tools...") await send_response({ "action": "status", "data": { @@ -265,10 +268,10 @@ async def _extract_cmdline_tools(archive_path: Path, sdk_root: Path) -> bool: pass - print("[installer][android-sdk] Extraction complete") + logger.info("[installer][android-sdk] Extraction complete") return True except Exception as e: - print(f"[installer][android-sdk] Extraction failed: {e}") + logger.error("[installer][android-sdk] Extraction failed: %s", e) await send_response({ "action": "status", "data": { @@ -293,7 +296,7 @@ async def _run_sdkmanager(sdk_root: Path, args: list[str]) -> bool: try: sdkmanager = _find_sdkmanager(sdk_root) if not sdkmanager: - print("[installer][android-sdk] sdkmanager not found") + logger.info("[installer][android-sdk] sdkmanager not found") return False import asyncio @@ -309,8 +312,8 @@ async def _run_sdkmanager(sdk_root: Path, args: list[str]) -> bool: # Wrap each arg in single quotes to preserve semicolons in package names like "platforms;android-36" quoted_args = " ".join([f"'{arg}'" for arg in args]) shell_cmd = f'powershell -Command "{yes_responses} | &\\"{str(sdkmanager)}\\" --sdk_root={sdk_root} {quoted_args}"' - print(f"[installer][android-sdk] Running: sdkmanager {' '.join(args)}") - print(f"[installer][android-sdk] This may take 5-15 minutes to download ~450MB of components...") + logger.info("[installer][android-sdk] Running: sdkmanager %s", " ".join(args)) + logger.info("[installer][android-sdk] This may take 5-15 minutes to download ~450MB of components...") loop = asyncio.get_event_loop() @@ -333,10 +336,10 @@ def run_sdkmanager(): try: for line in iter(process.stdout.readline, ''): if line: - print(line.rstrip()) # Print immediately + logger.info("%s", line.rstrip()) output_lines.append(line.strip()) except Exception as e: - print(f"[installer][android-sdk] Output reading error: {e}") + logger.error("[installer][android-sdk] Output reading error: %s", e) process.stdout.close() returncode = process.wait(timeout=1800) @@ -353,7 +356,7 @@ class Result: elif system == "Linux": # Linux can execute directly cmd = [str(sdkmanager), f"--sdk_root={sdk_root}"] + args - print(f"[installer][android-sdk] Running: {' '.join(cmd)}") + logger.info("[installer][android-sdk] Running: %s", " ".join(cmd)) loop = asyncio.get_event_loop() result = await loop.run_in_executor( @@ -369,7 +372,7 @@ class Result: elif system == "Darwin": # macOS can execute directly cmd = [str(sdkmanager), f"--sdk_root={sdk_root}"] + args - print(f"[installer][android-sdk] Running: {' '.join(cmd)}") + logger.info("[installer][android-sdk] Running: %s", " ".join(cmd)) loop = asyncio.get_event_loop() result = await loop.run_in_executor( @@ -383,25 +386,24 @@ class Result: ) output = (result.stdout or "") + (result.stderr or "") else: - print(f"[installer][android-sdk] Unsupported platform: {system}") + logger.warning("[installer][android-sdk] Unsupported platform: %s", system) return False if result.returncode != 0: - print(f"[installer][android-sdk] sdkmanager failed (returncode={result.returncode})") + logger.error("[installer][android-sdk] sdkmanager failed (returncode=%s)", result.returncode) if output: - print(f"[installer][android-sdk] Last output:\n{output}") + logger.error("[installer][android-sdk] Last output:\n%s", output) return False - print(f"[installer][android-sdk] sdkmanager completed successfully") + logger.info("[installer][android-sdk] sdkmanager completed successfully") if output: - print(f"[installer][android-sdk] Final output:\n{output[-500:]}") # Last 500 chars + logger.info("[installer][android-sdk] Final output:\n%s", output[-500:]) return True except subprocess.TimeoutExpired: - print("[installer][android-sdk] sdkmanager timed out after 30 minutes") + logger.warning("[installer][android-sdk] sdkmanager timed out after 30 minutes") return False except Exception as e: - print(f"[installer][android-sdk] sdkmanager error: {e}") - import traceback + logger.exception("[installer][android-sdk] sdkmanager error: %s", e) traceback.print_exc() return False @@ -411,14 +413,14 @@ async def _accept_licenses(sdk_root: Path) -> bool: try: sdkmanager = _find_sdkmanager(sdk_root) if not sdkmanager: - print("[installer][android-sdk] sdkmanager not found") + logger.info("[installer][android-sdk] sdkmanager not found") return False import asyncio import subprocess cmd = [str(sdkmanager), f"--sdk_root={sdk_root}", "--licenses"] - print(f"[installer][android-sdk] Accepting licenses: {' '.join(cmd)}") + logger.info("[installer][android-sdk] Accepting licenses: %s", " ".join(cmd)) if platform.system() == "Windows": # On Windows, use PowerShell to pipe 'y' responses @@ -470,13 +472,12 @@ async def _accept_licenses(sdk_root: Path) -> bool: returncode = result.returncode if returncode != 0: - print(f"[installer][android-sdk] License acceptance failed: {output}") + logger.error("[installer][android-sdk] License acceptance failed: %s", output) return False - print("[installer][android-sdk] Licenses accepted successfully") + logger.info("[installer][android-sdk] Licenses accepted successfully") return True except Exception as e: - print(f"[installer][android-sdk] License acceptance error: {e}") - import traceback + logger.exception("[installer][android-sdk] License acceptance error: %s", e) traceback.print_exc() return False @@ -484,11 +485,11 @@ async def _accept_licenses(sdk_root: Path) -> bool: async def install() -> bool: - print("[installer][android-sdk] Installing...") + logger.info("[installer][android-sdk] Installing...") # Check if Android SDK is already installed if await check_status(): - print("[installer][android-sdk] Android SDK is already installed") + logger.info("[installer][android-sdk] Android SDK is already installed") return True sdk_root = _get_sdk_root() @@ -520,7 +521,7 @@ async def install() -> bool: } }) if not await _accept_licenses(sdk_root): - print("[installer][android-sdk] License acceptance failed") + logger.warning("[installer][android-sdk] License acceptance failed") # Continue; some environments prompt-less acceptance may not be required @@ -542,7 +543,7 @@ async def install() -> bool: } }) if not await _run_sdkmanager(sdk_root, core_components): - print("[installer][android-sdk] Failed installing one or more SDK components") + logger.error("[installer][android-sdk] Failed installing one or more SDK components") return False @@ -550,7 +551,7 @@ async def install() -> bool: update_android_sdk_path() - print(f"[installer][android-sdk] Installation successful at {sdk_root}") + logger.info("[installer][android-sdk] Installation successful at %s", sdk_root) await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/android/appium.py b/Framework/install_handler/android/appium.py index b704b10c..15f395f0 100644 --- a/Framework/install_handler/android/appium.py +++ b/Framework/install_handler/android/appium.py @@ -1,13 +1,16 @@ import subprocess import asyncio import platform +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from Framework.nodejs_appium_installer import check_installations, get_appium_path, get_node_dir +logger = get_logger() + async def check_status() -> bool: """Check if Appium is installed.""" - print("[installer][android-appium] Checking status...") + logger.info("[installer][android-appium] Checking status...") try: # Use the check function from nodejs_appium_installer @@ -32,13 +35,12 @@ async def check_status() -> bool: ) ) - print("appium result: ", result) version_output = (result.stdout or result.stderr or "").strip() version_info = f" (version: {version_output})" if version_output else "" - except: + except Exception: version_info = "" - print(f"[installer][android-appium] Already installed at {appium_location}") + logger.info("[installer][android-appium] Already installed at %s", appium_location) await send_response({ "action": "status", "data": { @@ -50,7 +52,7 @@ async def check_status() -> bool: }) return True else: - print("[installer][android-appium] Not installed") + logger.info("[installer][android-appium] Not installed") await send_response({ "action": "status", "data": { @@ -62,7 +64,7 @@ async def check_status() -> bool: }) return False except Exception as e: - print(f"[installer][android-appium] Error checking status: {e}") + logger.error("[installer][android-appium] Error checking status: %s", e) await send_response({ "action": "status", "data": { @@ -79,7 +81,7 @@ async def check_status() -> bool: async def install() -> bool: """Install Appium globally via npm.""" - print("[installer][android-appium] Installing...") + logger.info("[installer][android-appium] Installing...") await send_response({ "action": "status", @@ -101,7 +103,7 @@ async def install() -> bool: npm_cmd = "npm" cmd = [npm_cmd, "install", "-g", "appium"] - print(f"[installer][android-appium] Running on Windows: {' '.join(cmd)}") + logger.info("[installer][android-appium] Running on Windows: %s", " ".join(cmd)) result = await loop.run_in_executor( None, lambda: subprocess.run( @@ -118,7 +120,7 @@ async def install() -> bool: npm_cmd = "npm" cmd = [npm_cmd, "install", "-g", "appium"] - print(f"[installer][android-appium] Running on Linux: {' '.join(cmd)}") + logger.info("[installer][android-appium] Running on Linux: %s", " ".join(cmd)) result = await loop.run_in_executor( None, lambda: subprocess.run( @@ -134,7 +136,7 @@ async def install() -> bool: npm_cmd = "npm" cmd = [npm_cmd, "install", "-g", "appium"] - print(f"[installer][android-appium] Running on macOS: {' '.join(cmd)}") + logger.info("[installer][android-appium] Running on macOS: %s", " ".join(cmd)) result = await loop.run_in_executor( None, lambda: subprocess.run( @@ -146,7 +148,7 @@ async def install() -> bool: ) else: - print(f"[installer][android-appium] Unsupported platform: {system}") + logger.warning("[installer][android-appium] Unsupported platform: %s", system) await send_response({ "action": "status", "data": { @@ -162,8 +164,8 @@ async def install() -> bool: output = (result.stdout or "") + (result.stderr or "") if result.returncode != 0: - print(f"[installer][android-appium] Installation failed (returncode={result.returncode})") - print(f"[installer][android-appium] Output: {output[:500]}") + logger.error("[installer][android-appium] Installation failed (returncode=%s)", result.returncode) + logger.error("[installer][android-appium] Output: %s", output[:500]) await send_response({ "action": "status", @@ -176,9 +178,9 @@ async def install() -> bool: }) return False - print(f"[installer][android-appium] Installation successful") + logger.info("[installer][android-appium] Installation successful") if output: - print(f"[installer][android-appium] Output: {output[:300]}") + logger.info("[installer][android-appium] Output: %s", output[:300]) # Verify installation by checking status if await check_status(): @@ -205,7 +207,7 @@ async def install() -> bool: return False except subprocess.TimeoutExpired: - print("[installer][android-appium] Installation timed out after 10 minutes") + logger.warning("[installer][android-appium] Installation timed out after 10 minutes") await send_response({ "action": "status", "data": { @@ -218,7 +220,7 @@ async def install() -> bool: return False except Exception as e: - print(f"[installer][android-appium] Installation error: {e}") + logger.error("[installer][android-appium] Installation error: %s", e) await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/android/emulator.py b/Framework/install_handler/android/emulator.py index 690612e7..6744a405 100644 --- a/Framework/install_handler/android/emulator.py +++ b/Framework/install_handler/android/emulator.py @@ -4,10 +4,14 @@ import asyncio import re import random +import traceback from pathlib import Path from settings import ZEUZ_NODE_DOWNLOADS_DIR from Framework.install_handler.utils import send_response, debug from Framework.install_handler.android.android_sdk import _get_sdk_root +from Framework.install_handler.install_log_config import get_logger + +logger = get_logger() def _find_executable(base_path: Path, base_name: str) -> Path | None: @@ -63,7 +67,7 @@ def get_emulator_command(): sdk_root = _get_sdk_root() if debug: - print("Launch avd: ", sdk_root) + logger.debug("[installer][emulator] Launch avd: %s", sdk_root) if system == "Windows": return os.path.join(str(sdk_root), "emulator", "emulator.exe") @@ -89,14 +93,14 @@ async def get_available_avds() -> list[dict]: # Check if Android SDK is installed if sdk_root is None: if debug: - print("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + logger.debug("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") return [] avdmanager = _find_avdmanager(sdk_root) if not avdmanager: if debug: - print("[installer][emulator] avdmanager not found") + logger.debug("[installer][emulator] avdmanager not found") return [] # Run avdmanager list avd command using async executor @@ -113,7 +117,7 @@ async def get_available_avds() -> list[dict]: if result.returncode != 0: if debug: - print(f"[installer][emulator] avdmanager list avd failed: {result.stderr}") + logger.debug("[installer][emulator] avdmanager list avd failed: %s", result.stderr) return [] output = result.stdout @@ -180,12 +184,11 @@ async def get_available_avds() -> list[dict]: except subprocess.TimeoutExpired: if debug: - print("[installer][emulator] avdmanager list avd timed out") + logger.debug("[installer][emulator] avdmanager list avd timed out") return [] except Exception as e: if debug: - print(f"[installer][emulator] Error listing AVDs: {e}") - import traceback + logger.debug("[installer][emulator] Error listing AVDs: %s", e) traceback.print_exc() return [] @@ -208,7 +211,7 @@ async def launch_avd(avd_name: str) -> bool: start_new_session=True # Detach from parent process ) - print(f"[installer][emulator] Launching AVD: {avd_name}... (PID: {process.pid})") + logger.info("[installer][emulator] Launching AVD: %s... (PID: %s)", avd_name, process.pid) # Send success response to server await send_response({ @@ -224,8 +227,7 @@ async def launch_avd(avd_name: str) -> bool: except FileNotFoundError: error_msg = f"Emulator executable not found" - - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -239,8 +241,7 @@ async def launch_avd(avd_name: str) -> bool: except Exception as e: error_msg = f"Failed to launch AVD {avd_name}: {e}" - print(f"[installer][emulator] {error_msg}") - import traceback + logger.error("[installer][emulator] %s", error_msg) traceback.print_exc() await send_response({ "action": "status", @@ -299,8 +300,7 @@ async def get_filtered_avd_services(): except Exception as e: if debug: - print(f"[installer][emulator] Error getting filtered AVD services: {e}") - import traceback + logger.debug("[installer][emulator] Error getting filtered AVD services: %s", e) traceback.print_exc() return None @@ -343,22 +343,22 @@ def _run_sdkmanager_list(sdkmanager: Path, sdk_root: Path) -> str: return '\n'.join(filtered) else: if debug: - print(f"[installer][emulator] sdkmanager --list failed: {result.stderr}") + logger.debug("[installer][emulator] sdkmanager --list failed: %s", result.stderr) return "" if result.returncode == 0: return result.stdout else: if debug: - print(f"[installer][emulator] sdkmanager --list failed: {result.stderr}") + logger.debug("[installer][emulator] sdkmanager --list failed: %s", result.stderr) return "" except subprocess.TimeoutExpired: if debug: - print("[installer][emulator] sdkmanager --list timed out") + logger.debug("[installer][emulator] sdkmanager --list timed out") return "" except Exception as e: if debug: - print(f"[installer][emulator] Error running sdkmanager --list: {e}") + logger.debug("[installer][emulator] Error running sdkmanager --list: %s", e) return "" @@ -410,7 +410,7 @@ async def get_available_system_images() -> list[dict]: # Check if Android SDK is installed if sdk_root is None: if debug: - print("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + logger.debug("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") await send_response({ "action": "status", "data": { @@ -426,13 +426,13 @@ async def get_available_system_images() -> list[dict]: if not sdkmanager: if debug: - print("[installer][emulator] sdkmanager not found") + logger.debug("[installer][emulator] sdkmanager not found") return [] # Check platform support if not (_is_windows() or _is_linux() or _is_darwin()): if debug: - print(f"[installer][emulator] Unsupported platform: {platform.system()}") + logger.debug("[installer][emulator] Unsupported platform: %s", platform.system()) return [] # Run sdkmanager --list using async executor @@ -446,7 +446,7 @@ async def get_available_system_images() -> list[dict]: if not output: if debug: - print("[installer][emulator] sdkmanager --list returned empty output") + logger.debug("[installer][emulator] sdkmanager --list returned empty output") return [] # Parse system image details from output @@ -464,17 +464,16 @@ async def get_available_system_images() -> list[dict]: system_images = sorted(unique_images, key=lambda x: x["package"]) if debug: - print(f"[installer][emulator] Found {len(system_images)} available system images") + logger.debug("[installer][emulator] Found %s available system images", len(system_images)) return system_images except subprocess.TimeoutExpired: if debug: - print("[installer][emulator] sdkmanager --list timed out") + logger.debug("[installer][emulator] sdkmanager --list timed out") return [] except Exception as e: if debug: - print(f"[installer][emulator] Error getting available system images: {e}") - import traceback + logger.debug("[installer][emulator] Error getting available system images: %s", e) traceback.print_exc() return [] @@ -557,7 +556,7 @@ async def get_available_devices() -> list[dict]: # Check if Android SDK is installed if sdk_root is None: if debug: - print("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + logger.debug("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") await send_response({ "action": "status", "data": { @@ -573,7 +572,7 @@ async def get_available_devices() -> list[dict]: if not avdmanager: if debug: - print("[installer][emulator] avdmanager not found") + logger.debug("[installer][emulator] avdmanager not found") return [] # Run avdmanager list device using async executor @@ -590,7 +589,7 @@ async def get_available_devices() -> list[dict]: if result.returncode != 0: if debug: - print(f"[installer][emulator] avdmanager list device failed: {result.stderr}") + logger.debug("[installer][emulator] avdmanager list device failed: %s", result.stderr) return [] # Parse device details from output @@ -610,23 +609,21 @@ async def get_available_devices() -> list[dict]: if sanitized_name not in existing_avd_names: filtered_devices.append(device) elif debug: - print(f"[installer][emulator] Filtering out device '{device_name}' (AVD '{sanitized_name}' already exists)") + logger.debug("[installer][emulator] Filtering out device '%s' (AVD '%s' already exists)", device_name, sanitized_name) else: # If no device name, include it (shouldn't happen, but safe fallback) filtered_devices.append(device) if debug: - print(f"[installer][emulator] Found {len(devices)} available devices, {len(filtered_devices)} not yet installed") + logger.debug("[installer][emulator] Found %s available devices, %s not yet installed", len(devices), len(filtered_devices)) return filtered_devices except subprocess.TimeoutExpired: if debug: - print("[installer][emulator] avdmanager list device timed out") + logger.debug("[installer][emulator] avdmanager list device timed out") return [] except Exception as e: - if debug: - print(f"[installer][emulator] Error getting available devices: {e}") - import traceback + logger.exception("[installer][emulator] Error getting available devices: %s", e) traceback.print_exc() return [] @@ -654,14 +651,14 @@ async def android_emulator_install(): Returns list of available devices for emulator installation. """ if debug: - print("[installer][emulator] Getting available devices...") + logger.debug("[installer][emulator] Getting available devices...") try: # Check if Android SDK is installed first sdk_root = _get_sdk_root() if sdk_root is None: if debug: - print("[installer][emulator] Android SDK not found") + logger.debug("[installer][emulator] Android SDK not found") await send_response({ "action": "status", "data": { @@ -688,7 +685,7 @@ async def android_emulator_install(): # Get available devices devices = await get_available_devices() if debug: - print(f"[installer][emulator] Available devices: {devices}") + logger.debug("[installer][emulator] Available devices: %s", devices) await send_response({ @@ -704,9 +701,7 @@ async def android_emulator_install(): return True except Exception as e: - if debug: - print(f"[installer][emulator] Error getting devices: {e}") - import traceback + logger.exception("[installer][emulator] Error getting devices: %s", e) traceback.print_exc() await send_response({ "action": "status", @@ -815,8 +810,8 @@ def _run_sdkmanager_install_windows(sdkmanager: Path, sdk_root: Path, system_ima shell_cmd = f'powershell -Command "{yes_responses} | &\\"{str(sdkmanager)}\\" --sdk_root={sdk_root} {quoted_image}"' if debug: - print(f"[installer][emulator] Running: sdkmanager --sdk_root={sdk_root} {system_image}") - print(f"[installer][emulator] This may take 10-30 minutes to download system image...") + logger.debug("[installer][emulator] Running: sdkmanager --sdk_root=%s %s", sdk_root, system_image) + logger.debug("[installer][emulator] This may take 10-30 minutes to download system image...") process = subprocess.Popen( shell_cmd, @@ -847,7 +842,7 @@ def _run_sdkmanager_install_windows(sdkmanager: Path, sdk_root: Path, system_ima status = progress_match.group(2).strip() current_progress = f"{percent}% {status}" if current_progress != last_progress: - print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", current_progress) last_progress = current_progress if loop and device_id: @@ -868,13 +863,13 @@ def _run_sdkmanager_install_windows(sdkmanager: Path, sdk_root: Path, system_ima ) elif stripped and not stripped.startswith('[') and '%' not in stripped: # Print important non-progress messages on new line - print(f"\n[installer][emulator] {stripped}") + logger.info("[installer][emulator] %s", stripped) elif stripped.endswith('%'): # Handle lines that end with just percentage percent_match = re.search(r'(\d+)%', stripped) if percent_match: percent = int(percent_match.group(1)) - print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", stripped) if loop and device_id: rounded_percent = round(percent / 10) * 10 @@ -893,9 +888,9 @@ def _run_sdkmanager_install_windows(sdkmanager: Path, sdk_root: Path, system_ima loop ) except Exception as e: - print(f"\n[installer][emulator] Output reading error: {e}") + logger.error("[installer][emulator] Output reading error: %s", e) finally: - print() # New line after progress completes + pass # New line after progress completes process.stdout.close() returncode = process.wait(timeout=1800) # 30 minutes for large system image downloads @@ -915,8 +910,8 @@ def _run_sdkmanager_install_linux(sdkmanager: Path, sdk_root: Path, system_image """Install system image on Linux with real-time output""" try: if debug: - print(f"[installer][emulator] Running: sdkmanager --sdk_root={sdk_root} {system_image}") - print(f"[installer][emulator] This may take 10-30 minutes to download system image...") + logger.debug("[installer][emulator] Running: sdkmanager --sdk_root=%s %s", sdk_root, system_image) + logger.debug("[installer][emulator] This may take 10-30 minutes to download system image...") process = subprocess.Popen( [str(sdkmanager), f"--sdk_root={sdk_root}", system_image], @@ -943,7 +938,7 @@ def _run_sdkmanager_install_linux(sdkmanager: Path, sdk_root: Path, system_image status = progress_match.group(2).strip() current_progress = f"{percent}% {status}" if current_progress != last_progress: - print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", current_progress) last_progress = current_progress if loop and device_id: @@ -964,13 +959,13 @@ def _run_sdkmanager_install_linux(sdkmanager: Path, sdk_root: Path, system_image ) elif stripped and not stripped.startswith('[') and '%' not in stripped: # Print important non-progress messages on new line - print(f"\n[installer][emulator] {stripped}") + logger.info("[installer][emulator] %s", stripped) elif stripped.endswith('%'): # Handle lines that end with just percentage percent_match = re.search(r'(\d+)%', stripped) if percent_match: percent = int(percent_match.group(1)) - print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", stripped) if loop and device_id: rounded_percent = round(percent / 10) * 10 @@ -989,9 +984,9 @@ def _run_sdkmanager_install_linux(sdkmanager: Path, sdk_root: Path, system_image loop ) except Exception as e: - print(f"\n[installer][emulator] Output reading error: {e}") + logger.error("[installer][emulator] Output reading error: %s", e) finally: - print() # New line after progress completes + pass # New line after progress completes process.stdout.close() returncode = process.wait(timeout=1800) # 30 minutes for large system image downloads @@ -1011,8 +1006,8 @@ def _run_sdkmanager_install_darwin(sdkmanager: Path, sdk_root: Path, system_imag """Install system image on macOS with real-time output""" try: if debug: - print(f"[installer][emulator] Running: sdkmanager --sdk_root={sdk_root} {system_image}") - print(f"[installer][emulator] This may take 10-30 minutes to download system image...") + logger.debug("[installer][emulator] Running: sdkmanager --sdk_root=%s %s", sdk_root, system_image) + logger.debug("[installer][emulator] This may take 10-30 minutes to download system image...") process = subprocess.Popen( [str(sdkmanager), f"--sdk_root={sdk_root}", system_image], @@ -1039,7 +1034,7 @@ def _run_sdkmanager_install_darwin(sdkmanager: Path, sdk_root: Path, system_imag status = progress_match.group(2).strip() current_progress = f"{percent}% {status}" if current_progress != last_progress: - print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", current_progress) last_progress = current_progress if loop and device_id: @@ -1060,13 +1055,13 @@ def _run_sdkmanager_install_darwin(sdkmanager: Path, sdk_root: Path, system_imag ) elif stripped and not stripped.startswith('[') and '%' not in stripped: # Print important non-progress messages on new line - print(f"\n[installer][emulator] {stripped}") + logger.info("[installer][emulator] %s", stripped) elif stripped.endswith('%'): # Handle lines that end with just percentage percent_match = re.search(r'(\d+)%', stripped) if percent_match: percent = int(percent_match.group(1)) - print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", stripped) if loop and device_id: rounded_percent = round(percent / 10) * 10 @@ -1085,9 +1080,9 @@ def _run_sdkmanager_install_darwin(sdkmanager: Path, sdk_root: Path, system_imag loop ) except Exception as e: - print(f"\n[installer][emulator] Output reading error: {e}") + logger.error("[installer][emulator] Output reading error: %s", e) finally: - print() # New line after progress completes + pass # New line after progress completes process.stdout.close() returncode = process.wait(timeout=1800) # 30 minutes for large system image downloads @@ -1137,18 +1132,18 @@ def _run_avdmanager_create_windows(avdmanager: Path, sdk_root: Path, avd_name: s status = progress_match.group(2).strip() current_progress = f"{percent}% {status}" if current_progress != last_progress: - print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", current_progress) last_progress = current_progress elif stripped and not stripped.startswith('[') and '%' not in stripped: # Print important non-progress messages on new line - print(f"\n[installer][emulator] {stripped}") + logger.info("[installer][emulator] %s", stripped) elif stripped.endswith('%'): # Handle lines that end with just percentage - print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", stripped) except Exception as e: - print(f"\n[installer][emulator] Output reading error: {e}") + logger.error("[installer][emulator] Output reading error: %s", e) finally: - print() # New line after progress completes + pass # New line after progress completes process.stdout.close() returncode = process.wait(timeout=120) @@ -1198,18 +1193,18 @@ def _run_avdmanager_create_linux(avdmanager: Path, sdk_root: Path, avd_name: str status = progress_match.group(2).strip() current_progress = f"{percent}% {status}" if current_progress != last_progress: - print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", current_progress) last_progress = current_progress elif stripped and not stripped.startswith('[') and '%' not in stripped: # Print important non-progress messages on new line - print(f"\n[installer][emulator] {stripped}") + logger.info("[installer][emulator] %s", stripped) elif stripped.endswith('%'): # Handle lines that end with just percentage - print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", stripped) except Exception as e: - print(f"\n[installer][emulator] Output reading error: {e}") + logger.error("[installer][emulator] Output reading error: %s", e) finally: - print() # New line after progress completes + pass # New line after progress completes process.stdout.close() returncode = process.wait(timeout=120) @@ -1259,18 +1254,18 @@ def _run_avdmanager_create_darwin(avdmanager: Path, sdk_root: Path, avd_name: st status = progress_match.group(2).strip() current_progress = f"{percent}% {status}" if current_progress != last_progress: - print(f"\r[installer][emulator] Download progress: {current_progress}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", current_progress) last_progress = current_progress elif stripped and not stripped.startswith('[') and '%' not in stripped: # Print important non-progress messages on new line - print(f"\n[installer][emulator] {stripped}") + logger.info("[installer][emulator] %s", stripped) elif stripped.endswith('%'): # Handle lines that end with just percentage - print(f"\r[installer][emulator] Download progress: {stripped}", end='', flush=True) + logger.info("[installer][emulator] Download progress: %s", stripped) except Exception as e: - print(f"\n[installer][emulator] Output reading error: {e}") + logger.error("[installer][emulator] Output reading error: %s", e) finally: - print() # New line after progress completes + pass # New line after progress completes process.stdout.close() returncode = process.wait(timeout=120) @@ -1342,7 +1337,7 @@ def _configure_avd_hardware(avd_name: str) -> bool: config_path = Path(avd_home) / f"{avd_name}.avd" / "config.ini" if not config_path.exists(): - print(f"[installer][emulator] AVD config file not found: {config_path}") + logger.warning("[installer][emulator] AVD config file not found: %s", config_path) return False # Read current config @@ -1384,15 +1379,14 @@ def _configure_avd_hardware(avd_name: str) -> bool: if modified: with open(config_path, 'w', encoding='utf-8') as f: f.writelines(new_lines) - print(f"[installer][emulator] Configured hardware settings (hw.keyboard=yes) for AVD '{avd_name}'") + logger.info("[installer][emulator] Configured hardware settings (hw.keyboard=yes) for AVD '%s'", avd_name) return True else: - print(f"[installer][emulator] Hardware settings already configured for AVD '{avd_name}'") + logger.info("[installer][emulator] Hardware settings already configured for AVD '%s'", avd_name) return True except Exception as e: - print(f"[installer][emulator] Failed to configure hardware settings for AVD '{avd_name}': {e}") - import traceback + logger.exception("[installer][emulator] Failed to configure hardware settings for AVD '%s': %s", avd_name, e) traceback.print_exc() return False @@ -1478,7 +1472,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: parts = device_param.split(";") if len(parts) < 3: error_msg = f"Invalid device parameter format. Expected 'install device;device_id;device_name', got: {device_param}" - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) device_id = parts[1].strip() if len(parts) > 1 else "unknown" await send_response({ "action": "status", @@ -1497,12 +1491,12 @@ async def create_avd_from_system_image(device_param: str) -> bool: # Sanitize device name for AVD (AVD names can only contain: a-z A-Z 0-9 . _ -) avd_name = _sanitize_avd_name(device_name) - print(f"[installer][emulator] Creating AVD '{avd_name}' (from device name '{device_name}') with device ID '{device_id}'") + logger.info("[installer][emulator] Creating AVD '%s' (from device name '%s') with device ID '%s'", avd_name, device_name, device_id) # Check if Android SDK is installed sdk_root = _get_sdk_root() if sdk_root is None: - print("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + logger.warning("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") await send_response({ "action": "status", "data": { @@ -1520,7 +1514,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: if not sdkmanager: if debug: - print("[installer][emulator] sdkmanager not found") + logger.debug("[installer][emulator] sdkmanager not found") await send_response({ "action": "status", "data": { @@ -1534,7 +1528,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: if not avdmanager: if debug: - print("[installer][emulator] avdmanager not found") + logger.debug("[installer][emulator] avdmanager not found") await send_response({ "action": "status", "data": { @@ -1547,7 +1541,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: return False # Step 0: Get available system images and select highest API level - print(f"[installer][emulator] Getting available system images with Android Version 16") + logger.info("[installer][emulator] Getting available system images with Android Version 16") await send_response({ "action": "status", "data": { @@ -1561,7 +1555,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: system_images = await get_available_system_images() if not system_images: error_msg = "No system images found. Please install Android SDK components first." - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1577,7 +1571,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: system_image_name = _get_highest_api_system_image(system_images) if not system_image_name: error_msg = "Could not find a suitable system image." - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1589,11 +1583,11 @@ async def create_avd_from_system_image(device_param: str) -> bool: }) return False - print(f"[installer][emulator] Selected system image: {system_image_name}") - print(f"[installer][emulator] Creating AVD '{device_name}' with device ID '{device_id}' and system image '{system_image_name}'") + logger.info("[installer][emulator] Selected system image: %s", system_image_name) + logger.info("[installer][emulator] Creating AVD '%s' with device ID '%s' and system image '%s'", device_name, device_id, system_image_name) # Step 1: Install system image - print(f"[installer][emulator] Installing system image: {system_image_name}") + logger.info("[installer][emulator] Installing system image: %s", system_image_name) loop = asyncio.get_event_loop() if _is_windows(): @@ -1640,7 +1634,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: if not success: error_msg = f"Failed to install Android Version 16: {output}" - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1652,10 +1646,10 @@ async def create_avd_from_system_image(device_param: str) -> bool: }) return False - print(f"[installer][emulator] System image installed successfully") + logger.info("[installer][emulator] System image installed successfully") # Step 2: Create AVD with device_id and device_name - print(f"[installer][emulator] Creating AVD: {avd_name} with device ID: {device_id}") + logger.info("[installer][emulator] Creating AVD: %s with device ID: %s", avd_name, device_id) await send_response({ "action": "status", "data": { @@ -1710,7 +1704,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: if not success: error_msg = f"Failed to create AVD: {output}" - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1722,7 +1716,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: }) return False - print(f"[installer][emulator] AVD '{avd_name}' created successfully") + logger.info("[installer][emulator] AVD '%s' created successfully", avd_name) # Configure hardware settings so buttons work properly _configure_avd_hardware(avd_name) @@ -1745,7 +1739,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: except ValueError as e: error_msg = f"Invalid device parameter: {e}" - print(f"[installer][emulator] {error_msg}") + logger.error("[installer][emulator] %s", error_msg) device_name = device_param.split(";")[2].strip() if len(device_param.split(";")) > 2 else device_param await send_response({ "action": "status", @@ -1759,8 +1753,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: return False except Exception as e: error_msg = f"Error creating AVD: {e}" - print(f"[installer][emulator] {error_msg}") - import traceback + logger.exception("[installer][emulator] %s", error_msg) traceback.print_exc() device_name = device_param.split(";")[2].strip() if len(device_param.split(";")) > 2 else device_param await send_response({ diff --git a/Framework/install_handler/android/java.py b/Framework/install_handler/android/java.py index 10775df4..616b2352 100644 --- a/Framework/install_handler/android/java.py +++ b/Framework/install_handler/android/java.py @@ -9,10 +9,14 @@ import stat import httpx from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from Framework.install_handler.android.jdk import install as install_jdk from settings import ZEUZ_NODE_DOWNLOADS_DIR +logger = get_logger() + + def get_jdk_dir(): """Get JDK installation directory (in downloads directory, matching jdk.py extraction location).""" jdk_dir = ZEUZ_NODE_DOWNLOADS_DIR / "jdk" / "jdk-21" @@ -59,14 +63,14 @@ def get_java_path(): async def check_status() -> bool: """Check if Java 21 is installed (following Node.js installer pattern - simple file existence check).""" - print("[installer][android-java] Checking status...") + logger.info("[installer][android-java] Checking status...") # Simple file existence check in isolated directory (like Node.js installer) java_path = get_java_path() if java_path.exists(): - print("[installer][android-java] Already installed") + logger.info("[installer][android-java] Already installed") await send_response({ "action": "status", @@ -80,7 +84,7 @@ async def check_status() -> bool: return True # Not installed - print("[installer][android-java] Not installed") + logger.info("[installer][android-java] Not installed") await send_response({ "action": "status", "data": { @@ -97,10 +101,10 @@ def update_java_path(): """Add Java binaries to PATH and set JAVA_HOME for the current process (following Node.js pattern).""" java_path = get_java_path() - print("Updating java path for the current session") + logger.info("Updating java path for the current session") # Check if java exists if not java_path.exists(): - print("Java not found for PATH update.") + logger.warning("Java not found for PATH update.") return # Get JDK home directory (parent of bin directory) @@ -114,14 +118,14 @@ def update_java_path(): # Set JAVA_HOME for the current process os.environ['JAVA_HOME'] = str(jdk_home) - print(f"JAVA_HOME set for current process: {jdk_home}") + logger.info("JAVA_HOME set for current process: %s", jdk_home) # Add Java bin to PATH for the current process (always prepend so it takes precedence) java_bin_path = str(java_path.parent) current_path = os.environ.get('PATH', '') # Always prepend to ensure isolated Java takes precedence (even if path already exists) os.environ['PATH'] = f"{java_bin_path}{os.pathsep}{current_path}" - print(f"Java prepended to current process PATH: {java_bin_path}") + logger.info("Java prepended to current process PATH: %s", java_bin_path) async def _get_jdk_download_url(): @@ -156,7 +160,7 @@ async def _get_jdk_download_url(): arch = "s390x" else: # Default to x64 for unknown architectures - print(f"[installer][android-java] Warning: Unknown architecture '{machine}', defaulting to x64") + logger.warning("[installer][android-java] Warning: Unknown architecture '%s', defaulting to x64", machine) arch = "x64" # Use Temurin metadata API to get latest JDK 21 download URL @@ -171,18 +175,18 @@ async def _get_jdk_download_url(): # Extract download URL from API response if data and len(data) > 0: download_url = data[0]["binary"]["package"]["link"] - print(f"[installer][android-java] Found Temurin JDK 21 download URL: {download_url}") + logger.info("[installer][android-java] Found Temurin JDK 21 download URL: %s", download_url) return download_url else: raise Exception("No download URL found in API response") except Exception as e: - print(f"[installer][android-java] Error fetching Temurin download URL: {e}") + logger.error("[installer][android-java] Error fetching Temurin download URL: %s", e) raise Exception(f"Failed to get JDK download URL from Temurin API: {e}") async def _download_jdk(): """Download JDK 21 LTS with progress reporting""" - print("[installer][android-jdk] Downloading JDK 21 LTS...") + logger.info("[installer][android-jdk] Downloading JDK 21 LTS...") await send_response({ "action": "status", "data": { @@ -217,8 +221,7 @@ async def _download_jdk(): total_size = int(response.headers.get("content-length", 0)) chunk_size = 8192 downloaded = 0 - - count = [] + last_pct = [-1] with open(jdk_archive, "wb") as f: async for chunk in response.aiter_bytes(chunk_size): f.write(chunk) @@ -226,18 +229,15 @@ async def _download_jdk(): if total_size > 0: progress = (downloaded / total_size) * 100 - bar_length = 50 - filled_length = int(bar_length * downloaded // total_size) - bar = '█' * filled_length + '-' * (bar_length - filled_length) - - mb_downloaded = downloaded / (1024 * 1024) - mb_total = total_size / (1024 * 1024) - - print(f"\r[installer][android-jdk] |{bar}| {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end='', flush=True) - - p = round(mb_downloaded/mb_total, 1) - if p not in count: - count.append(p) + pct = int(progress) + if pct != last_pct[0]: + last_pct[0] = pct + bar_length = 50 + filled_length = int(bar_length * downloaded // total_size) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + mb_downloaded = downloaded / (1024 * 1024) + mb_total = total_size / (1024 * 1024) + logger.info("[installer][android-jdk] |%s| %d%% (%.1f/%.1f MB)", bar, pct, mb_downloaded, mb_total) asyncio.create_task(send_response({ "action": "status", "data": { @@ -248,11 +248,10 @@ async def _download_jdk(): } })) - print() - print(f"[installer][android-jdk] JDK download complete: {jdk_archive}") + logger.info("[installer][android-jdk] JDK download complete: %s", jdk_archive) return jdk_archive except Exception as e: - print(f"\n[installer][android-jdk] JDK download failed: {e}") + logger.error("[installer][android-jdk] JDK download failed: %s", e) await send_response({ "action": "status", "data": { @@ -270,7 +269,7 @@ async def _extract_jdk(jdk_archive): if not jdk_archive or not jdk_archive.exists(): return None - print("[installer][android-jdk] Extracting JDK...") + logger.info("[installer][android-jdk] Extracting JDK...") await send_response({ "action": "status", "data": { @@ -290,11 +289,11 @@ async def _extract_jdk(jdk_archive): jdk_dir.mkdir(parents=True, exist_ok=True) if system == "Windows": - print(f"[installer][android-jdk] Extracting JDK to {jdk_dir}") + logger.info("[installer][android-jdk] Extracting JDK to %s", jdk_dir) elif system == "Linux": - print(f"[installer][android-jdk] Extracting JDK to {jdk_dir}") + logger.info("[installer][android-jdk] Extracting JDK to %s", jdk_dir) elif system == "Darwin": - print(f"[installer][android-jdk] Extracting JDK to {jdk_dir}") + logger.info("[installer][android-jdk] Extracting JDK to %s", jdk_dir) try: if system == "Windows": @@ -341,7 +340,7 @@ async def _extract_jdk(jdk_archive): raise OSError(f"Unsupported platform: {system}") if not jdk_home: - print("[installer][android-jdk] Could not find JDK directory after extraction") + logger.error("[installer][android-jdk] Could not find JDK directory after extraction") await send_response({ "action": "status", "data": { @@ -353,10 +352,10 @@ async def _extract_jdk(jdk_archive): }) return None - print(f"[installer][android-jdk] JDK extracted to {jdk_home}") + logger.info("[installer][android-jdk] JDK extracted to %s", jdk_home) return jdk_home except Exception as e: - print(f"[installer][android-jdk] JDK extraction failed: {e}") + logger.error("[installer][android-jdk] JDK extraction failed: %s", e) await send_response({ "action": "status", "data": { @@ -371,7 +370,7 @@ async def _extract_jdk(jdk_archive): async def _verify_java_installation(jdk_home): """Verify that Java is properly installed and working""" - print("[installer][android-jdk] Verifying Java installation...") + logger.info("[installer][android-jdk] Verifying Java installation...") await send_response({ "action": "status", "data": { @@ -392,11 +391,11 @@ async def _verify_java_installation(jdk_home): elif system == "Darwin": java_exe = jdk_home / "bin" / "java" else: - print(f"[installer][android-jdk] Unsupported platform: {system}") + logger.warning("[installer][android-jdk] Unsupported platform: %s", system) return False if not java_exe.exists(): - print(f"[installer][android-jdk] Java executable not found at {java_exe}") + logger.error("[installer][android-jdk] Java executable not found at %s", java_exe) await send_response({ "action": "status", "data": { @@ -413,13 +412,13 @@ async def _verify_java_installation(jdk_home): try: java_exe.chmod(java_exe.stat().st_mode | stat.S_IEXEC) except Exception as e: - print(f"[installer][android-jdk] Failed to make Java executable: {e}") + logger.error("[installer][android-jdk] Failed to make Java executable: %s", e) return False elif system == "Darwin": try: java_exe.chmod(java_exe.stat().st_mode | stat.S_IEXEC) except Exception as e: - print(f"[installer][android-jdk] Failed to make Java executable: {e}") + logger.error("[installer][android-jdk] Failed to make Java executable: %s", e) return False # Test Java version (async) @@ -434,7 +433,7 @@ async def _verify_java_installation(jdk_home): ) ) if "version \"21" not in result.stderr: - print("[installer][android-jdk] Java version check failed") + logger.error("[installer][android-jdk] Java version check failed") await send_response({ "action": "status", "data": { @@ -445,7 +444,7 @@ async def _verify_java_installation(jdk_home): } }) return False - print("[installer][android-jdk] Java version verified") + logger.info("[installer][android-jdk] Java version verified") # Test Java compiler if system == "Windows": @@ -456,7 +455,7 @@ async def _verify_java_installation(jdk_home): javac_exe = jdk_home / "bin" / "javac" if not javac_exe.exists(): - print(f"[installer][android-jdk] Java compiler not found at {javac_exe}") + logger.error("[installer][android-jdk] Java compiler not found at %s", javac_exe) await send_response({ "action": "status", "data": { @@ -482,7 +481,7 @@ async def _verify_java_installation(jdk_home): ) ) if "javac 21" not in (result.stdout or result.stderr): - print("[installer][android-jdk] Java compiler version check failed") + logger.error("[installer][android-jdk] Java compiler version check failed") await send_response({ "action": "status", "data": { @@ -493,11 +492,11 @@ async def _verify_java_installation(jdk_home): } }) return False - print("[installer][android-jdk] Java compiler verified") + logger.info("[installer][android-jdk] Java compiler verified") return True except Exception as e: - print(f"[installer][android-jdk] Java verification failed: {e}") + logger.error("[installer][android-jdk] Java verification failed: %s", e) await send_response({ "action": "status", "data": { @@ -512,11 +511,11 @@ async def _verify_java_installation(jdk_home): async def install() -> bool: """Main function to setup JDK 21 LTS""" - print("[installer][android-java] Installing...") + logger.info("[installer][android-java] Installing...") # Check if JDK 21 is already installed if await check_status(): - print("[installer][android-java] JDK 21 is already installed") + logger.info("[installer][android-java] JDK 21 is already installed") return True jdk_home = None @@ -535,15 +534,15 @@ async def install() -> bool: # Clean up archive try: jdk_archive.unlink() - except: + except Exception: pass # Verify installation if not await _verify_java_installation(jdk_home): - print("[installer][android-jdk] Java installation verification failed") + logger.error("[installer][android-jdk] Java installation verification failed") return False - print("[installer][android-jdk] JDK 21 LTS setup complete") + logger.info("[installer][android-jdk] JDK 21 LTS setup complete") await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/android/jdk.py b/Framework/install_handler/android/jdk.py index e26f285f..4576b748 100644 --- a/Framework/install_handler/android/jdk.py +++ b/Framework/install_handler/android/jdk.py @@ -10,9 +10,12 @@ import tarfile import stat from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from settings import ZEUZ_NODE_DOWNLOADS_DIR +logger = get_logger() + async def _get_jdk_download_url(): """Get the appropriate JDK 21 LTS download URL based on platform""" @@ -33,7 +36,7 @@ async def _get_jdk_download_url(): return "https://download.oracle.com/java/21/latest/jdk-21_macos-aarch64_bin.tar.gz" else: return "https://download.oracle.com/java/21/latest/jdk-21_macos-x64_bin.tar.gz" - except: + except Exception: # Default to x64 if detection fails return "https://download.oracle.com/java/21/latest/jdk-21_macos-x64_bin.tar.gz" else: @@ -42,7 +45,7 @@ async def _get_jdk_download_url(): async def _download_jdk(): """Download JDK 21 LTS with progress reporting""" - print("[installer][android-jdk] Downloading JDK 21 LTS...") + logger.info("[installer][android-jdk] Downloading JDK 21 LTS...") await send_response({ "action": "status", "data": { @@ -77,8 +80,7 @@ async def _download_jdk(): total_size = int(response.headers.get("content-length", 0)) chunk_size = 8192 downloaded = 0 - - count = [] + last_pct = [-1] with open(jdk_archive, "wb") as f: async for chunk in response.aiter_bytes(chunk_size): f.write(chunk) @@ -86,18 +88,15 @@ async def _download_jdk(): if total_size > 0: progress = (downloaded / total_size) * 100 - bar_length = 50 - filled_length = int(bar_length * downloaded // total_size) - bar = '█' * filled_length + '-' * (bar_length - filled_length) - - mb_downloaded = downloaded / (1024 * 1024) - mb_total = total_size / (1024 * 1024) - - print(f"\r[installer][android-jdk] |{bar}| {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end='', flush=True) - - p = round(mb_downloaded/mb_total, 1) - if p not in count: - count.append(p) + pct = int(progress) + if pct != last_pct[0]: + last_pct[0] = pct + bar_length = 50 + filled_length = int(bar_length * downloaded // total_size) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + mb_downloaded = downloaded / (1024 * 1024) + mb_total = total_size / (1024 * 1024) + logger.info("[installer][android-jdk] |%s| %d%% (%.1f/%.1f MB)", bar, pct, mb_downloaded, mb_total) asyncio.create_task(send_response({ "action": "status", "data": { @@ -108,11 +107,10 @@ async def _download_jdk(): } })) - print() - print(f"[installer][android-jdk] JDK download complete: {jdk_archive}") + logger.info("[installer][android-jdk] JDK download complete: %s", jdk_archive) return jdk_archive except Exception as e: - print(f"\n[installer][android-jdk] JDK download failed: {e}") + logger.error("[installer][android-jdk] JDK download failed: %s", e) await send_response({ "action": "status", "data": { @@ -130,7 +128,7 @@ async def _extract_jdk(jdk_archive): if not jdk_archive or not jdk_archive.exists(): return None - print("[installer][android-jdk] Extracting JDK...") + logger.info("[installer][android-jdk] Extracting JDK...") await send_response({ "action": "status", "data": { @@ -150,11 +148,11 @@ async def _extract_jdk(jdk_archive): jdk_dir.mkdir(parents=True, exist_ok=True) if system == "Windows": - print(f"[installer][android-jdk] Extracting JDK to {jdk_dir}") + logger.info("[installer][android-jdk] Extracting JDK to %s", jdk_dir) elif system == "Linux": - print(f"[installer][android-jdk] Extracting JDK to {jdk_dir}") + logger.info("[installer][android-jdk] Extracting JDK to %s", jdk_dir) elif system == "Darwin": - print(f"[installer][android-jdk] Extracting JDK to {jdk_dir}") + logger.info("[installer][android-jdk] Extracting JDK to %s", jdk_dir) try: if system == "Windows": @@ -177,7 +175,7 @@ async def _extract_jdk(jdk_archive): break if not jdk_home: - print("[installer][android-jdk] Could not find JDK directory after extraction") + logger.error("[installer][android-jdk] Could not find JDK directory after extraction") await send_response({ "action": "status", "data": { @@ -189,10 +187,10 @@ async def _extract_jdk(jdk_archive): }) return None - print(f"[installer][android-jdk] JDK extracted to {jdk_home}") + logger.info("[installer][android-jdk] JDK extracted to %s", jdk_home) return jdk_home except Exception as e: - print(f"[installer][android-jdk] JDK extraction failed: {e}") + logger.error("[installer][android-jdk] JDK extraction failed: %s", e) await send_response({ "action": "status", "data": { @@ -210,7 +208,7 @@ async def _set_java_env_vars(jdk_home): if not jdk_home or not jdk_home.exists(): return False - print("[installer][android-jdk] Setting Java environment variables...") + logger.info("[installer][android-jdk] Setting Java environment variables...") await send_response({ "action": "status", "data": { @@ -231,7 +229,7 @@ async def _set_java_env_vars(jdk_home): r"Environment", 0, winreg.KEY_ALL_ACCESS) as key: winreg.SetValueEx(key, "JAVA_HOME", 0, winreg.REG_EXPAND_SZ, str(jdk_home)) - print("[installer][android-jdk] JAVA_HOME set in Windows user environment") + logger.info("[installer][android-jdk] JAVA_HOME set in Windows user environment") # Update PATH in user environment variables with winreg.OpenKey(winreg.HKEY_CURRENT_USER, @@ -250,7 +248,7 @@ async def _set_java_env_vars(jdk_home): path_parts.append(java_bin) new_path = ";".join(path_parts) winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path) - print("[installer][android-jdk] Java added to PATH in Windows user environment") + logger.info("[installer][android-jdk] Java added to PATH in Windows user environment") # CRITICAL: Update current process environment so subprocess can find Java immediately os.environ['JAVA_HOME'] = str(jdk_home) @@ -258,9 +256,9 @@ async def _set_java_env_vars(jdk_home): java_bin = str(jdk_home / "bin") if java_bin not in current_process_path: os.environ['PATH'] = f"{java_bin};{current_process_path}" - print("[installer][android-jdk] Java added to current process PATH") + logger.info("[installer][android-jdk] Java added to current process PATH") except Exception as e: - print(f"[installer][android-jdk] Failed to update Windows user environment: {e}") + logger.error("[installer][android-jdk] Failed to update Windows user environment: %s", e) return False elif system == "Linux": @@ -269,12 +267,12 @@ async def _set_java_env_vars(jdk_home): # Use current user's home directory user_home = Path.home() - print("[installer][android-jdk] Setting Java environment variables for current user") + logger.info("[installer][android-jdk] Setting Java environment variables for current user") if is_system_wide: - print("[installer][android-jdk] System-wide Java installation detected") + logger.info("[installer][android-jdk] System-wide Java installation detected") else: - print("[installer][android-jdk] User-specific Java installation detected") + logger.info("[installer][android-jdk] User-specific Java installation detected") shell_configs = [ user_home / ".bashrc", @@ -304,18 +302,18 @@ async def _set_java_env_vars(jdk_home): if needs_update: f.write("\n# Java environment variables\n" + "\n".join(export_lines) + "\n") - print(f"[installer][android-jdk] Updated {config_file} with Java paths") + logger.info("[installer][android-jdk] Updated %s with Java paths", config_file) updated = True except Exception as e: - print(f"[installer][android-jdk] Failed to update {config_file}: {e}") + logger.error("[installer][android-jdk] Failed to update %s: %s", config_file, e) if updated: - print("[!] Please restart your terminal or run 'source ~/.bashrc' (or your shell config)") + logger.info("[!] Please restart your terminal or run 'source ~/.bashrc' (or your shell config)") elif system == "Darwin": # macOS - user installation user_home = Path.home() - print("[installer][android-jdk] Setting Java environment variables for current user") + logger.info("[installer][android-jdk] Setting Java environment variables for current user") shell_configs = [ user_home / ".bash_profile", @@ -345,15 +343,15 @@ async def _set_java_env_vars(jdk_home): if needs_update: f.write("\n# Java environment variables\n" + "\n".join(export_lines) + "\n") - print(f"[installer][android-jdk] Updated {config_file} with Java paths") + logger.info("[installer][android-jdk] Updated %s with Java paths", config_file) updated = True except Exception as e: - print(f"[installer][android-jdk] Failed to update {config_file}: {e}") + logger.error("[installer][android-jdk] Failed to update %s: %s", config_file, e) if updated: - print("[!] Please restart your terminal or run 'source ~/.zshrc' (or your shell config)") + logger.info("[!] Please restart your terminal or run 'source ~/.zshrc' (or your shell config)") else: - print(f"[installer][android-jdk] Unsupported platform: {system}") + logger.warning("[installer][android-jdk] Unsupported platform: %s", system) return False return True @@ -361,7 +359,7 @@ async def _set_java_env_vars(jdk_home): async def _verify_java_installation(jdk_home): """Verify that Java is properly installed and working""" - print("[installer][android-jdk] Verifying Java installation...") + logger.info("[installer][android-jdk] Verifying Java installation...") await send_response({ "action": "status", "data": { @@ -382,11 +380,11 @@ async def _verify_java_installation(jdk_home): elif system == "Darwin": java_exe = jdk_home / "bin" / "java" else: - print(f"[installer][android-jdk] Unsupported platform: {system}") + logger.warning("[installer][android-jdk] Unsupported platform: %s", system) return False if not java_exe.exists(): - print(f"[installer][android-jdk] Java executable not found at {java_exe}") + logger.error("[installer][android-jdk] Java executable not found at %s", java_exe) await send_response({ "action": "status", "data": { @@ -403,13 +401,13 @@ async def _verify_java_installation(jdk_home): try: java_exe.chmod(java_exe.stat().st_mode | stat.S_IEXEC) except Exception as e: - print(f"[installer][android-jdk] Failed to make Java executable: {e}") + logger.error("[installer][android-jdk] Failed to make Java executable: %s", e) return False elif system == "Darwin": try: java_exe.chmod(java_exe.stat().st_mode | stat.S_IEXEC) except Exception as e: - print(f"[installer][android-jdk] Failed to make Java executable: {e}") + logger.error("[installer][android-jdk] Failed to make Java executable: %s", e) return False # Test Java version (async) @@ -424,7 +422,7 @@ async def _verify_java_installation(jdk_home): ) ) if "version \"21" not in result.stderr: - print("[installer][android-jdk] Java version check failed") + logger.error("[installer][android-jdk] Java version check failed") await send_response({ "action": "status", "data": { @@ -435,7 +433,7 @@ async def _verify_java_installation(jdk_home): } }) return False - print("[installer][android-jdk] Java version verified") + logger.info("[installer][android-jdk] Java version verified") # Test Java compiler if system == "Windows": @@ -446,7 +444,7 @@ async def _verify_java_installation(jdk_home): javac_exe = jdk_home / "bin" / "javac" if not javac_exe.exists(): - print(f"[installer][android-jdk] Java compiler not found at {javac_exe}") + logger.error("[installer][android-jdk] Java compiler not found at %s", javac_exe) await send_response({ "action": "status", "data": { @@ -472,7 +470,7 @@ async def _verify_java_installation(jdk_home): ) ) if "javac 21" not in (result.stdout or result.stderr): - print("[installer][android-jdk] Java compiler version check failed") + logger.error("[installer][android-jdk] Java compiler version check failed") await send_response({ "action": "status", "data": { @@ -483,11 +481,11 @@ async def _verify_java_installation(jdk_home): } }) return False - print("[installer][android-jdk] Java compiler verified") + logger.info("[installer][android-jdk] Java compiler verified") return True except Exception as e: - print(f"[installer][android-jdk] Java verification failed: {e}") + logger.error("[installer][android-jdk] Java verification failed: %s", e) await send_response({ "action": "status", "data": { @@ -502,7 +500,7 @@ async def _verify_java_installation(jdk_home): async def check_status() -> bool: """Check if JDK 21 is installed.""" - print("[installer][android-jdk] Checking status...") + logger.info("[installer][android-jdk] Checking status...") # Dynamically refresh JAVA_HOME and PATH from registry on Windows system = platform.system() @@ -519,11 +517,11 @@ async def check_status() -> bool: current_path = os.environ.get('PATH', '') if java_bin not in current_path: os.environ['PATH'] = f"{java_bin};{current_path}" - print(f"[installer][android-jdk] Refreshed JAVA_HOME from registry: {java_home_reg}") + logger.info("[installer][android-jdk] Refreshed JAVA_HOME from registry: %s", java_home_reg) except FileNotFoundError: pass except Exception as e: - print(f"[installer][android-jdk] Failed to refresh from registry: {e}") + logger.error("[installer][android-jdk] Failed to refresh from registry: %s", e) try: loop = asyncio.get_event_loop() @@ -538,7 +536,7 @@ async def check_status() -> bool: ) if result.returncode != 0: - print("[installer][android-jdk] Not installed") + logger.info("[installer][android-jdk] Not installed") await send_response({ "action": "status", "data": { @@ -553,7 +551,7 @@ async def check_status() -> bool: # javac -version prints to stderr typically version_text = (result.stderr or result.stdout).strip() if not version_text: - print("[installer][android-jdk] Not installed") + logger.info("[installer][android-jdk] Not installed") await send_response({ "action": "status", "data": { @@ -573,7 +571,7 @@ async def check_status() -> bool: # Check if it's JDK 21 if major_version == 21: - print(f"[installer][android-jdk] Already installed (version: {major_version}.{minor_version})") + logger.info("[installer][android-jdk] Already installed (version: %s.%s)", major_version, minor_version) await send_response({ "action": "status", "data": { @@ -586,7 +584,7 @@ async def check_status() -> bool: return True # Handle old versioning like "1.8.0" where major=1, minor=8 elif major_version == 1 and minor_version >= 8: - print(f"[installer][android-jdk] Wrong version installed (found: {major_version}.{minor_version})") + logger.warning("[installer][android-jdk] Wrong version installed (found: %s.%s)", major_version, minor_version) await send_response({ "action": "status", "data": { @@ -598,7 +596,7 @@ async def check_status() -> bool: }) return False - print(f"[installer][android-jdk] Not installed (found version: {version_text})") + logger.info("[installer][android-jdk] Not installed (found version: %s)", version_text) await send_response({ "action": "status", "data": { @@ -611,7 +609,7 @@ async def check_status() -> bool: return False except (FileNotFoundError, OSError): # javac command not found - JDK is not installed - print("[installer][android-jdk] Not installed (javac not found)") + logger.info("[installer][android-jdk] Not installed (javac not found)") await send_response({ "action": "status", "data": { @@ -623,7 +621,7 @@ async def check_status() -> bool: }) return False except Exception as e: - print(f"[installer][android-jdk] Error checking status: {e}") + logger.error("[installer][android-jdk] Error checking status: %s", e) await send_response({ "action": "status", "data": { @@ -640,11 +638,11 @@ async def check_status() -> bool: async def install() -> bool: """Main function to setup JDK 21 LTS""" - print("[installer][android-jdk] Installing...") + logger.info("[installer][android-jdk] Installing...") # Check if JDK 21 is already installed if await check_status(): - print("[installer][android-jdk] JDK 21 is already installed") + logger.info("[installer][android-jdk] JDK 21 is already installed") return True jdk_home = None @@ -663,19 +661,19 @@ async def install() -> bool: # Clean up archive try: jdk_archive.unlink() - except: + except Exception: pass # Verify installation if not await _verify_java_installation(jdk_home): - print("[installer][android-jdk] Java installation verification failed") + logger.error("[installer][android-jdk] Java installation verification failed") return False # Set environment variables if not await _set_java_env_vars(jdk_home): return False - print("[installer][android-jdk] JDK 21 LTS setup complete") + logger.info("[installer][android-jdk] JDK 21 LTS setup complete") await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/install_log_config.py b/Framework/install_handler/install_log_config.py new file mode 100644 index 00000000..925dc3fa --- /dev/null +++ b/Framework/install_handler/install_log_config.py @@ -0,0 +1,84 @@ +""" +Shared config and logger for the install handler. +Used by installer modules for console + (when set up) rotating file logging. +""" +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + +INSTALLER_LOG_DIR: Path | None = None +INSTALLER_LOGGER_NAME = "Framework.install_handler" +INSTALLER_LOG_FILENAME = "installer.log" +LOG_FORMAT = "{message}" +LOG_DATE_FMT = "%Y-%m-%d %H:%M" +ROTATING_MAX_BYTES = 5 * 1024 * 1024 # 5 MB +ROTATING_BACKUP_COUNT = 3 + + +def get_installer_log_dir() -> Path: + """Return the directory for installer log files. Uses default if not yet set.""" + if INSTALLER_LOG_DIR is not None: + return INSTALLER_LOG_DIR + return Path.home() / ".zeuz" / "logs" + + +def get_logger() -> logging.Logger: + """Return the shared installer logger. Handlers are attached by setup_installer_logging().""" + return logging.getLogger(INSTALLER_LOGGER_NAME) + + +def setup_installer_logging(log_dir: Path | None = None) -> None: + """ + Attach console and (optionally) rotating file handler to the installer logger. + Safe to call multiple times; avoids duplicate handlers. + + Under no circumstances does this function raise. If anything fails (e.g. log + dir creation, file open, permissions), we continue without the file handler + or, in the worst case, without any handlers—the node and installer must + never be interrupted by logging setup. + """ + try: + global INSTALLER_LOG_DIR + if log_dir is not None: + INSTALLER_LOG_DIR = log_dir + effective_dir = get_installer_log_dir() + + logger = logging.getLogger(INSTALLER_LOGGER_NAME) + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter(LOG_FORMAT, style="{", datefmt=LOG_DATE_FMT) + + # Avoid duplicate handlers: remove existing ones from this logger + for h in list(logger.handlers): + logger.removeHandler(h) + + # Console (always) + console = logging.StreamHandler(sys.stderr) + console.setFormatter(formatter) + logger.addHandler(console) + + # Rotating file: only if we can create the dir and open the file. + # On any failure we keep console-only; never re-raise. + try: + effective_dir.mkdir(parents=True, exist_ok=True) + log_file = effective_dir / INSTALLER_LOG_FILENAME + # Truncate so this process only has current-session logs; then append + try: + log_file.open("w", encoding="utf-8").close() + except Exception: + pass + file_handler = RotatingFileHandler( + log_file, + mode="a", + maxBytes=ROTATING_MAX_BYTES, + backupCount=ROTATING_BACKUP_COUNT, + encoding="utf-8", + ) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + except Exception: + # File logging unavailable (permissions, read-only, disk full, etc.) + logger.warning("Installer file logging disabled (could not create log file)") + except Exception: + # Do not re-raise. Node and installer must never be interrupted. + print("[installer] Logging setup failed; continuing without installer log.", file=sys.stderr) diff --git a/Framework/install_handler/ios/simulator.py b/Framework/install_handler/ios/simulator.py index 2120e1c6..f67f99cd 100644 --- a/Framework/install_handler/ios/simulator.py +++ b/Framework/install_handler/ios/simulator.py @@ -6,9 +6,14 @@ import json import re import tempfile +import traceback from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response +logger = get_logger() + + async def _send_status(status: str, comment: str): """Helper to send status responses.""" await send_response( @@ -134,7 +139,7 @@ def fallback_is_xcode_installed() -> bool: async def check_status() -> bool: """Check if iOS Simulator is installed and available.""" - print("[simulator] Checking status...") + logger.info("[simulator] Checking status...") if platform.system().lower() != "darwin": await _send_status("error", "Unsupported OS. iOS Simulator is only available on macOS.") @@ -307,7 +312,7 @@ async def get_available_simulators() -> list[dict]: return simulators except Exception as e: - print(f"[simulator] Error listing simulators: {e}") + logger.error("[simulator] Error listing simulators: %s", e) return [] @@ -371,7 +376,7 @@ async def get_available_device_types() -> list[dict]: return device_types except Exception as e: - print(f"[simulator] Error listing device types: {e}") + logger.error("[simulator] Error listing device types: %s", e) return [] @@ -416,7 +421,7 @@ async def get_available_runtimes() -> list[dict]: return runtimes except Exception as e: - print(f"[simulator] Error listing runtimes: {e}") + logger.error("[simulator] Error listing runtimes: %s", e) return [] @@ -443,7 +448,7 @@ async def ios_simulator_install(): Get available device types when install button is clicked. Returns list of available iOS device types for simulator creation. """ - print("[simulator] Getting available device types...") + logger.info("[simulator] Getting available device types...") try: # Check if macOS @@ -487,7 +492,7 @@ async def ios_simulator_install(): # Get available device types device_types = await get_available_device_types() - print(f"[simulator] Available device types: {device_types}") + logger.info("[simulator] Available device types: %s", device_types) await send_response({ "action": "status", @@ -499,7 +504,7 @@ async def ios_simulator_install(): return True except Exception as e: - print(f"[simulator] Error getting device types: {e}") + logger.error("[simulator] Error getting device types: %s", e) await send_response({ "action": "status", "data": { @@ -557,7 +562,7 @@ async def get_filtered_simulator_services(): } except Exception as e: - print(f"[simulator] Error getting filtered simulators: {e}") + logger.error("[simulator] Error getting filtered simulators: %s", e) return None @@ -579,7 +584,7 @@ async def delete_simulator(udid: str) -> bool: if not simulator_info: error_msg = f"Simulator {udid} not found" - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -592,7 +597,7 @@ async def delete_simulator(udid: str) -> bool: return False simulator_name = simulator_info["name"] - print(f"[simulator] Deleting simulator: {simulator_name} ({udid})") + logger.info("[simulator] Deleting simulator: %s (%s)", simulator_name, udid) # Send deleting status await send_response({ @@ -615,7 +620,7 @@ async def delete_simulator(udid: str) -> bool: if result.returncode != 0: error_msg = f"Failed to delete simulator: {result.stderr.strip()}" - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -627,7 +632,7 @@ async def delete_simulator(udid: str) -> bool: }) return False - print(f"[simulator] Simulator deleted successfully: {simulator_name} ({udid})") + logger.info("[simulator] Simulator deleted successfully: %s (%s)", simulator_name, udid) # Send success response await send_response({ @@ -643,7 +648,7 @@ async def delete_simulator(udid: str) -> bool: except subprocess.TimeoutExpired: error_msg = "Simulator deletion timed out" - print(f"[simulator] {error_msg}") + logger.warning("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -656,8 +661,7 @@ async def delete_simulator(udid: str) -> bool: return False except Exception as e: error_msg = f"Error deleting simulator: {e}" - print(f"[simulator] {error_msg}") - import traceback + logger.exception("[simulator] %s", error_msg) traceback.print_exc() await send_response({ "action": "status", @@ -686,7 +690,7 @@ async def launch_simulator(udid: str) -> bool: if not simulator_info: error_msg = f"Simulator {udid} not found" - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -723,7 +727,7 @@ async def launch_simulator(udid: str) -> bool: if result.returncode != 0: error_msg = f"Failed to boot simulator: {result.stderr.strip()}" - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -825,7 +829,7 @@ async def launch_simulator(udid: str) -> bool: # Copy to standard location before temp directory is deleted standard_build_path.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(app_path, standard_build_path, dirs_exist_ok=True) - print(f"[simulator] Copied built app to {standard_build_path}") + logger.info("[simulator] Copied built app to %s", standard_build_path) app_path_to_install = standard_build_path else: await _send_status_emulator(udid, "installed", f"WebDriverAgent app not found at {app_path}") @@ -842,12 +846,12 @@ async def launch_simulator(udid: str) -> bool: ) if install_result.returncode == 0: - print(f"[simulator] WebDriverAgent installed successfully on {simulator_name}") + logger.info("[simulator] WebDriverAgent installed successfully on %s", simulator_name) wda_installed = True else: - print(f"[simulator] Failed to install WebDriverAgent: {install_result.stderr}") + logger.error("[simulator] Failed to install WebDriverAgent: %s", install_result.stderr) else: - print(f"[simulator] WebDriverAgent already installed on {simulator_name}") + logger.info("[simulator] WebDriverAgent already installed on %s", simulator_name) # Launch WebDriverAgent if installed if wda_installed: @@ -870,7 +874,7 @@ async def launch_simulator(udid: str) -> bool: ) if launch_wda.returncode == 0: - print(f"[simulator] WebDriverAgent launched successfully on {simulator_name}") + logger.info("[simulator] WebDriverAgent launched successfully on %s", simulator_name) await send_response({ "action": "status", "data": { @@ -881,7 +885,7 @@ async def launch_simulator(udid: str) -> bool: } }) else: - print(f"[simulator] Failed to launch WebDriverAgent: {launch_wda.stderr}") + logger.error("[simulator] Failed to launch WebDriverAgent: %s", launch_wda.stderr) await send_response({ "action": "status", "data": { @@ -907,7 +911,7 @@ async def launch_simulator(udid: str) -> bool: except subprocess.TimeoutExpired: error_msg = "Operation timed out" - print(f"[simulator] {error_msg}") + logger.warning("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -920,8 +924,7 @@ async def launch_simulator(udid: str) -> bool: return False except Exception as e: error_msg = f"Failed to launch simulator: {e}" - print(f"[simulator] {error_msg}") - import traceback + logger.exception("[simulator] %s", error_msg) traceback.print_exc() await send_response({ "action": "status", @@ -952,7 +955,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: parts = device_param.split(";") if len(parts) < 3: error_msg = f"Invalid device parameter format. Expected 'install device;device_type_id;device_name', got: {device_param}" - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -967,13 +970,13 @@ async def create_simulator_from_device_type(device_param: str) -> bool: device_type_id = parts[1].strip() device_name = parts[2].strip() - print(f"[simulator] Creating simulator '{device_name}' with device type '{device_type_id}'") + logger.info("[simulator] Creating simulator '%s' with device type '%s'", device_name, device_type_id) # Get available runtimes runtimes = await get_available_runtimes() if not runtimes: error_msg = "No iOS runtimes found. Please install Xcode and iOS runtime first." - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -989,7 +992,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: available_runtimes = [r for r in runtimes if r.get("isAvailable", False)] if not available_runtimes: error_msg = "No available iOS runtimes found." - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1006,7 +1009,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: runtime_id = runtime["identifier"] runtime_name = runtime["name"] - print(f"[simulator] Using runtime: {runtime_name} ({runtime_id})") + logger.info("[simulator] Using runtime: %s (%s)", runtime_name, runtime_id) # Generate a unique simulator name existing_sims = await get_available_simulators() @@ -1040,7 +1043,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: if result.returncode != 0: error_msg = f"Failed to create simulator: {result.stderr.strip()}" - print(f"[simulator] {error_msg}") + logger.error("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1053,7 +1056,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: return False new_udid = result.stdout.strip() - print(f"[simulator] Simulator created successfully: {simulator_name} ({new_udid})") + logger.info("[simulator] Simulator created successfully: %s (%s)", simulator_name, new_udid) # Send success response await send_response({ @@ -1070,7 +1073,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: except subprocess.TimeoutExpired: error_msg = "Simulator creation timed out" - print(f"[simulator] {error_msg}") + logger.warning("[simulator] %s", error_msg) await send_response({ "action": "status", "data": { @@ -1083,8 +1086,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: return False except Exception as e: error_msg = f"Error creating simulator: {e}" - print(f"[simulator] {error_msg}") - import traceback + logger.exception("[simulator] %s", error_msg) traceback.print_exc() await send_response({ "action": "status", @@ -1100,7 +1102,7 @@ async def create_simulator_from_device_type(device_param: str) -> bool: async def install(user_password: str = "") -> bool: """Main install entry point.""" - print("[simulator] Installing...") + logger.info("[simulator] Installing...") if platform.system().lower() != "darwin": await _send_status("error", "iOS Simulator is only available on macOS.") diff --git a/Framework/install_handler/ios/webdriver.py b/Framework/install_handler/ios/webdriver.py index d66d88bb..c60253da 100644 --- a/Framework/install_handler/ios/webdriver.py +++ b/Framework/install_handler/ios/webdriver.py @@ -6,11 +6,15 @@ import tempfile import asyncio from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response +logger = get_logger() + + async def _send_status(status: str, comment: str): """Helper to send status responses.""" - print(f"[{status}] {comment}") + logger.info("[%s] %s", status, comment) await send_response( { "action": "status", @@ -74,7 +78,7 @@ async def _get_best_simulator() -> tuple[str, str] | None: return candidates[0][1], candidates[0][2] return None except Exception as e: - print(f"Error listing simulators: {e}") + logger.error("Error listing simulators: %s", e) return None async def _boot_simulator_if_needed(device_uuid: str) -> bool: @@ -110,14 +114,14 @@ async def _boot_simulator_if_needed(device_uuid: str) -> bool: ) return res.returncode == 0 except Exception as e: - print(f"Boot exception: {e}") + logger.warning("Boot exception: %s", e) # We return True here to attempt the next step anyway, # as sometimes bootstatus fails even if the device works. return True async def check_status() -> bool: """Checks if WebDriverAgent is installed (Ensures Simulator is ON).""" - print("[webdriver] Checking status...") + logger.info("[webdriver] Checking status...") if platform.system().lower() != "darwin": await _send_status("error", "Unsupported OS.") @@ -239,7 +243,7 @@ async def _bootstrap_webdriver(webdriver_path: Path): except: pass async def install() -> bool: - print("[webdriver] Starting installation...") + logger.info("[webdriver] Starting installation...") if await check_status(): return True diff --git a/Framework/install_handler/ios/xcode.py b/Framework/install_handler/ios/xcode.py index 08ebddee..5a3749d9 100644 --- a/Framework/install_handler/ios/xcode.py +++ b/Framework/install_handler/ios/xcode.py @@ -1,15 +1,18 @@ +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.macos.common import xcode_check_status, xcode_install +logger = get_logger() + async def check_status() -> bool: """Check if Xcode is installed and license is accepted.""" - print("[xcode] Checking status...") + logger.info("[xcode] Checking status...") return await xcode_check_status("iOS") async def install(user_password: str = "") -> bool: """Install Xcode via App Store and accept license.""" - print("[xcode] Installing...") + logger.info("[xcode] Installing...") return await xcode_install("iOS", user_password) diff --git a/Framework/install_handler/linux/atspi.py b/Framework/install_handler/linux/atspi.py index 9df1493d..8229c499 100644 --- a/Framework/install_handler/linux/atspi.py +++ b/Framework/install_handler/linux/atspi.py @@ -1,4 +1,5 @@ import os +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from .linux_utils import ( detect_package_manager, @@ -6,6 +7,8 @@ install_packages, ) +logger = get_logger() + # Package definitions for different package managers PACKAGES = { @@ -40,7 +43,7 @@ async def check_status(): """Checks if AT-SPI development packages are installed.""" - print("Checking AT-SPI development packages status...") + logger.info("Checking AT-SPI development packages status...") # Check if session type is X11 session_type = os.environ.get("XDG_SESSION_TYPE", "unknown") @@ -105,7 +108,7 @@ async def check_status(): async def install(user_password: str = ""): """Install AT-SPI development packages using the system package manager.""" - print("Installing AT-SPI development packages...") + logger.info("Installing AT-SPI development packages...") # Check if session type is X11 session_type = os.environ.get("XDG_SESSION_TYPE", "unknown") diff --git a/Framework/install_handler/long_poll_handler.py b/Framework/install_handler/long_poll_handler.py index 87882fa0..85c395dd 100644 --- a/Framework/install_handler/long_poll_handler.py +++ b/Framework/install_handler/long_poll_handler.py @@ -3,7 +3,10 @@ import random import httpx import inspect +from pathlib import Path + from colorama import Fore +from Framework.install_handler import install_log_config from Framework.install_handler.route import Response, services from Framework.install_handler.utils import ( debug, @@ -34,10 +37,16 @@ class InstallHandler: - def __init__(self): + def __init__(self, log_dir: Path | None = None): self.cancel_ = False self.running = False self.client = None + installer_log_dir = log_dir if log_dir is not None else Path.home() / ".zeuz" / "logs" + install_log_config.INSTALLER_LOG_DIR = installer_log_dir + try: + install_log_config.setup_installer_logging(installer_log_dir) + except Exception: + pass async def on_message(self, message: Response) -> None: try: diff --git a/Framework/install_handler/macos/common.py b/Framework/install_handler/macos/common.py index 771b9a84..aff92b28 100644 --- a/Framework/install_handler/macos/common.py +++ b/Framework/install_handler/macos/common.py @@ -3,8 +3,11 @@ import os import shutil import subprocess +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response +logger = get_logger() + async def _send_status(category, status: str, comment: str): """Helper to send status responses.""" @@ -37,7 +40,7 @@ def fallback_is_xcode_installed() -> bool: async def xcode_check_status(category) -> bool: """Check if Xcode is installed and license is accepted.""" - print("[xcode] Checking status...") + logger.info("[xcode] Checking status...") if platform.system().lower() != "darwin": await _send_status( @@ -126,7 +129,7 @@ async def _accept_license(category, user_password: str) -> bool: async def xcode_install(category, user_password: str = "") -> bool: """Install Xcode via App Store and accept license.""" - print("[xcode] Installing...") + logger.info("[xcode] Installing...") if platform.system().lower() != "darwin": await _send_status( diff --git a/Framework/install_handler/macos/xcode.py b/Framework/install_handler/macos/xcode.py index a940e083..bcd61d7d 100644 --- a/Framework/install_handler/macos/xcode.py +++ b/Framework/install_handler/macos/xcode.py @@ -1,16 +1,18 @@ +from Framework.install_handler.install_log_config import get_logger from .common import xcode_check_status, xcode_install +logger = get_logger() + async def check_status() -> bool: """Check if Xcode is installed and license is accepted.""" - print("[xcode] Checking status...") + logger.info("[xcode] Checking status...") return await xcode_check_status("MacOS") - async def install(user_password: str = "") -> bool: """Install Xcode via App Store and accept license.""" - print("[xcode] Installing...") + logger.info("[xcode] Installing...") return await xcode_install("MacOS", user_password) diff --git a/Framework/install_handler/system_info/system_info.py b/Framework/install_handler/system_info/system_info.py index 4f62f93f..1d4e11c0 100644 --- a/Framework/install_handler/system_info/system_info.py +++ b/Framework/install_handler/system_info/system_info.py @@ -9,6 +9,10 @@ except ImportError: psutil = None +from Framework.install_handler.install_log_config import get_logger + +logger = get_logger() + def get_system_uptime() -> Dict[str, Any]: """ @@ -107,7 +111,7 @@ def get_system_uptime() -> Dict[str, Any]: "uptime_human": "Unknown" } except Exception as e: - print(f"[installer][system-info] Error getting uptime: {e}") + logger.error("[installer][system-info] Error getting uptime: %s", e) return { "uptime_seconds": None, "uptime_human": "Unknown" @@ -184,7 +188,7 @@ def get_device_model() -> str: return "Unknown" except Exception as e: - print(f"[installer][system-info] Error getting device model: {e}") + logger.error("[installer][system-info] Error getting device model: %s", e) return "Unknown" @@ -261,7 +265,7 @@ def get_os_info() -> Dict[str, str]: "os_release": release } except Exception as e: - print(f"[installer][system-info] Error getting OS info: {e}") + logger.error("[installer][system-info] Error getting OS info: %s", e) return { "os_name": platform.system(), "os_version": platform.version(), @@ -371,7 +375,7 @@ def get_cpu_info() -> Dict[str, Any]: "cpu_temperature": cpu_temperature } except Exception as e: - print(f"[installer][system-info] Error getting CPU info: {e}") + logger.error("[installer][system-info] Error getting CPU info: %s", e) return { "cpu_cores": None, "cpu_physical_cores": None, @@ -684,7 +688,7 @@ def parse_size(value: str, unit: str) -> int: "swap_percent": 0 } except Exception as e: - print(f"[installer][system-info] Error getting memory info: {e}") + logger.error("[installer][system-info] Error getting memory info: %s", e) return { "total_ram_bytes": None, "total_ram_gb": None, @@ -776,7 +780,7 @@ def get_disk_info() -> Dict[str, Any]: # Skip partitions we don't have permission to access continue except Exception as e: - print(f"[installer][system-info] Error getting usage for {partition.mountpoint}: {e}") + logger.error("[installer][system-info] Error getting usage for %s: %s", partition.mountpoint, e) continue total_disk_space_gb = total_disk_space_bytes / (1024 ** 3) @@ -891,7 +895,7 @@ def get_disk_info() -> Dict[str, Any]: "mount_points": mount_points } except Exception as e: - print(f"[installer][system-info] Error getting disk info (Linux fallback): {e}") + logger.error("[installer][system-info] Error getting disk info (Linux fallback): %s", e) elif system == "Darwin": try: result = subprocess.run( @@ -930,7 +934,7 @@ def get_disk_info() -> Dict[str, Any]: "mount_points": mount_points } except Exception as e: - print(f"[installer][system-info] Error getting disk info (macOS fallback): {e}") + logger.error("[installer][system-info] Error getting disk info (macOS fallback): %s", e) elif system == "Windows": try: result = subprocess.run( @@ -990,7 +994,7 @@ def get_disk_info() -> Dict[str, Any]: "mount_points": mount_points } except Exception as e: - print(f"[installer][system-info] Error getting disk info (Windows fallback): {e}") + logger.error("[installer][system-info] Error getting disk info (Windows fallback): %s", e) return { "total_disk_space_bytes": None, @@ -1007,7 +1011,7 @@ def get_disk_info() -> Dict[str, Any]: "mount_points": [] } except Exception as e: - print(f"[installer][system-info] Error getting disk info: {e}") + logger.error("[installer][system-info] Error getting disk info: %s", e) return { "total_disk_space_bytes": None, "total_disk_space_gb": None, @@ -1045,7 +1049,7 @@ async def get_all_system_info() -> Dict[str, Any]: "disk": disk_info } except Exception as e: - print(f"[installer][system-info] Error getting all system info: {e}") + logger.error("[installer][system-info] Error getting all system info: %s", e) return { "os": get_os_info(), "cpu": get_cpu_info(), @@ -1211,7 +1215,7 @@ async def get_formatted_system_info() -> Dict[str, Any]: "disk": disk_data } except Exception as e: - print(f"[installer][system-info] Error formatting system info: {e}") + logger.error("[installer][system-info] Error formatting system info: %s", e) # Return minimal error response return { "os": {"name": "Unknown", "version": "Unknown", "release": "Unknown"}, diff --git a/Framework/install_handler/web/chrome_for_testing.py b/Framework/install_handler/web/chrome_for_testing.py index 9d54348f..a8bea0bb 100644 --- a/Framework/install_handler/web/chrome_for_testing.py +++ b/Framework/install_handler/web/chrome_for_testing.py @@ -1,11 +1,14 @@ import asyncio from Framework.Built_In_Automation.Web.Selenium.utils import ChromeForTesting +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response +logger = get_logger() + async def check_status() -> bool: """Check if Chrome for Testing is installed.""" - print("[installer][web-chrome_for_testing] Checking status...") + logger.info("[installer][web-chrome_for_testing] Checking status...") try: cft = ChromeForTesting() @@ -14,7 +17,7 @@ async def check_status() -> bool: latest_version = cft.get_latest_version(channel="Stable", force_check=False) if not latest_version: - print("[installer][web-chrome_for_testing] Not installed (no version available)") + logger.info("[installer][web-chrome_for_testing] Not installed (no version available)") await send_response({ "action": "status", "data": { @@ -30,7 +33,7 @@ async def check_status() -> bool: is_installed = cft.is_version_installed(latest_version) if is_installed: - print(f"[installer][web-chrome_for_testing] Already installed (version: {latest_version})") + logger.info("[installer][web-chrome_for_testing] Already installed (version: %s)", latest_version) await send_response({ "action": "status", "data": { @@ -42,7 +45,7 @@ async def check_status() -> bool: }) return True else: - print("[installer][web-chrome_for_testing] Not installed") + logger.info("[installer][web-chrome_for_testing] Not installed") await send_response({ "action": "status", "data": { @@ -54,7 +57,7 @@ async def check_status() -> bool: }) return False except Exception as e: - print(f"[installer][web-chrome_for_testing] Error checking status: {e}") + logger.error("[installer][web-chrome_for_testing] Error checking status: %s", e) await send_response({ "action": "status", "data": { @@ -69,11 +72,11 @@ async def check_status() -> bool: async def install() -> bool: """Install Chrome for Testing.""" - print("[installer][web-chrome_for_testing] Installing...") + logger.info("[installer][web-chrome_for_testing] Installing...") # Check if already installed if await check_status(): - print("[installer][web-chrome_for_testing] Chrome for Testing is already installed") + logger.info("[installer][web-chrome_for_testing] Chrome for Testing is already installed") return True try: @@ -98,7 +101,7 @@ async def install() -> bool: ) if chrome_bin and driver_bin: - print("[installer][web-chrome_for_testing] Chrome for Testing installation successful") + logger.info("[installer][web-chrome_for_testing] Chrome for Testing installation successful") await send_response({ "action": "status", "data": { @@ -110,7 +113,7 @@ async def install() -> bool: }) return True else: - print("[installer][web-chrome_for_testing] Chrome for Testing installation failed") + logger.error("[installer][web-chrome_for_testing] Chrome for Testing installation failed") await send_response({ "action": "status", "data": { @@ -122,7 +125,7 @@ async def install() -> bool: }) return False except FileNotFoundError as e: - print(f"[installer][web-chrome_for_testing] Error installing: {e}") + logger.error("[installer][web-chrome_for_testing] Error installing: %s", e) await send_response({ "action": "status", "data": { @@ -134,7 +137,7 @@ async def install() -> bool: }) return False except Exception as e: - print(f"[installer][web-chrome_for_testing] Error installing: {e}") + logger.error("[installer][web-chrome_for_testing] Error installing: %s", e) await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/web/edge.py b/Framework/install_handler/web/edge.py index 2e8589f6..e9926e4a 100644 --- a/Framework/install_handler/web/edge.py +++ b/Framework/install_handler/web/edge.py @@ -6,9 +6,12 @@ import shutil import json from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from settings import ZEUZ_NODE_DOWNLOADS_DIR +logger = get_logger() + def _is_windows(): """Check if running on Windows""" @@ -48,7 +51,7 @@ def _get_linux_package_manager(): async def check_status() -> bool: """Check if Microsoft Edge is installed.""" - print("[installer][web-edge] Checking status...") + logger.info("[installer][web-edge] Checking status...") try: result = None @@ -120,7 +123,7 @@ async def check_status() -> bool: ) if result.returncode != 0: - print("[installer][web-edge] Not installed") + logger.info("[installer][web-edge] Not installed") await send_response({ "action": "status", "data": { @@ -135,7 +138,7 @@ async def check_status() -> bool: # Edge version output is typically in stdout or stderr version_text = (result.stdout or result.stderr).strip() if not version_text: - print("[installer][web-edge] Not installed") + logger.info("[installer][web-edge] Not installed") await send_response({ "action": "status", "data": { @@ -147,7 +150,7 @@ async def check_status() -> bool: }) return False - print("[installer][web-edge] Already installed") + logger.info("[installer][web-edge] Already installed") await send_response({ "action": "status", "data": { @@ -160,7 +163,7 @@ async def check_status() -> bool: return True except (FileNotFoundError, OSError): # Edge command not found - Edge is not installed - print("[installer][web-edge] Not installed (msedge not found)") + logger.info("[installer][web-edge] Not installed (msedge not found)") await send_response({ "action": "status", "data": { @@ -172,7 +175,7 @@ async def check_status() -> bool: }) return False except Exception as e: - print(f"[installer][web-edge] Error checking status: {e}") + logger.error("[installer][web-edge] Error checking status: %s", e) await send_response({ "action": "status", "data": { @@ -187,7 +190,7 @@ async def check_status() -> bool: async def _download_edge_installer(): """Download Edge installer based on platform""" - print("[installer][web-edge] Downloading Microsoft Edge installer...") + logger.info("[installer][web-edge] Downloading Microsoft Edge installer...") await send_response({ "action": "status", "data": { @@ -235,8 +238,7 @@ async def _download_edge_installer(): total_size = int(response.headers.get("content-length", 0)) chunk_size = 8192 downloaded = 0 - - count = [] + last_pct = [-1] with open(installer_path, "wb") as f: async for chunk in response.aiter_bytes(chunk_size): f.write(chunk) @@ -244,18 +246,15 @@ async def _download_edge_installer(): if total_size > 0: progress = (downloaded / total_size) * 100 - bar_length = 50 - filled_length = int(bar_length * downloaded // total_size) - bar = '█' * filled_length + '-' * (bar_length - filled_length) - - mb_downloaded = downloaded / (1024 * 1024) - mb_total = total_size / (1024 * 1024) - - print(f"\r[installer][web-edge] |{bar}| {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end='', flush=True) - - p = round(mb_downloaded/mb_total, 1) - if p not in count: - count.append(p) + pct = int(progress) + if pct != last_pct[0]: + last_pct[0] = pct + bar_length = 50 + filled_length = int(bar_length * downloaded // total_size) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + mb_downloaded = downloaded / (1024 * 1024) + mb_total = total_size / (1024 * 1024) + logger.info("[installer][web-edge] |%s| %d%% (%.1f/%.1f MB)", bar, pct, mb_downloaded, mb_total) asyncio.create_task(send_response({ "action": "status", "data": { @@ -266,11 +265,10 @@ async def _download_edge_installer(): } })) - print() - print(f"[installer][web-edge] Download complete: {installer_path}") + logger.info("[installer][web-edge] Download complete: %s", installer_path) return installer_path except Exception as e: - print(f"\n[installer][web-edge] Download failed: {e}") + logger.error("[installer][web-edge] Download failed: %s", e) await send_response({ "action": "status", "data": { @@ -285,7 +283,7 @@ async def _download_edge_installer(): async def _install_edge_windows(installer_path, user_password: str = ""): """Install Edge on Windows""" - print("[installer][web-edge] Installing Microsoft Edge on Windows...") + logger.info("[installer][web-edge] Installing Microsoft Edge on Windows...") await send_response({ "action": "status", "data": { @@ -345,7 +343,7 @@ def run_elevated(cmd_list): winget_result = run_elevated(["winget", "install", "--id", "Microsoft.Edge", "--silent", "--accept-package-agreements", "--accept-source-agreements"]) if winget_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via winget") + logger.info("[installer][web-edge] Microsoft Edge installed via winget") return True # Fallback to MSI installer @@ -353,10 +351,10 @@ def run_elevated(cmd_list): msi_result = run_elevated(["msiexec", "/i", str(installer_path), "/quiet", "/norestart"]) if msi_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via MSI") + logger.info("[installer][web-edge] Microsoft Edge installed via MSI") return True else: - print(f"[installer][web-edge] Installation failed. Error: {msi_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", msi_result.stderr) await send_response({ "action": "status", "data": { @@ -368,7 +366,7 @@ def run_elevated(cmd_list): }) return False else: - print("[installer][web-edge] Installer not found, trying direct download") + logger.warning("[installer][web-edge] Installer not found, trying direct download") # Try direct download URL (this will prompt for elevation if needed) if user_password: import getpass @@ -395,7 +393,7 @@ def run_elevated(cmd_list): if download_result.returncode == 0: return True else: - print(f"[installer][web-edge] Installation failed. Error: {download_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", download_result.stderr) await send_response({ "action": "status", "data": { @@ -408,7 +406,7 @@ def run_elevated(cmd_list): return False # All installation methods failed - print(f"[installer][web-edge] Installation failed. Error: {winget_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", winget_result.stderr) await send_response({ "action": "status", "data": { @@ -420,7 +418,7 @@ def run_elevated(cmd_list): }) return False except Exception as e: - print(f"[installer][web-edge] Windows installation failed: {e}") + logger.exception("[installer][web-edge] Windows installation failed: %s", e) await send_response({ "action": "status", "data": { @@ -435,7 +433,7 @@ def run_elevated(cmd_list): async def _install_edge_linux(installer_path, user_password: str = ""): """Install Edge on Linux""" - print("[installer][web-edge] Installing Microsoft Edge on Linux...") + logger.info("[installer][web-edge] Installing Microsoft Edge on Linux...") await send_response({ "action": "status", "data": { @@ -465,7 +463,7 @@ def run_sudo(cmd_list): apt_install_result = run_sudo(["sudo", "apt-get", "install", "-y", "microsoft-edge-stable"]) if apt_install_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via apt") + logger.info("[installer][web-edge] Microsoft Edge installed via apt") return True @@ -479,11 +477,11 @@ def run_sudo(cmd_list): deb_result = run_sudo(["sudo", "dpkg", "-i", str(installer_path)]) if deb_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via .deb package") + logger.info("[installer][web-edge] Microsoft Edge installed via .deb package") return True # .deb installation failed - print(f"[installer][web-edge] Installation failed. Error: {deb_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", deb_result.stderr) await send_response({ "action": "status", "data": { @@ -496,7 +494,7 @@ def run_sudo(cmd_list): return False else: # No .deb package available and apt failed - print(f"[installer][web-edge] Installation failed. Error: {apt_install_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", apt_install_result.stderr) await send_response({ "action": "status", "data": { @@ -513,11 +511,11 @@ def run_sudo(cmd_list): yum_result = run_sudo(["sudo", "yum", "install", "-y", "microsoft-edge-stable"]) if yum_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via yum") + logger.info("[installer][web-edge] Microsoft Edge installed via yum") return True # Installation failed - print(f"[installer][web-edge] Installation failed. Error: {yum_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", yum_result.stderr) await send_response({ "action": "status", "data": { @@ -534,11 +532,11 @@ def run_sudo(cmd_list): dnf_result = run_sudo(["sudo", "dnf", "install", "-y", "microsoft-edge-stable"]) if dnf_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via dnf") + logger.info("[installer][web-edge] Microsoft Edge installed via dnf") return True # Installation failed - print(f"[installer][web-edge] Installation failed. Error: {dnf_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", dnf_result.stderr) await send_response({ "action": "status", "data": { @@ -561,7 +559,7 @@ def run_sudo(cmd_list): }) return False except Exception as e: - print(f"[installer][web-edge] Linux installation failed: {e}") + logger.exception("[installer][web-edge] Linux installation failed: %s", e) await send_response({ "action": "status", "data": { @@ -576,7 +574,7 @@ def run_sudo(cmd_list): async def _install_edge_darwin(installer_path, user_password: str = ""): """Install Edge on macOS""" - print("[installer][web-edge] Installing Microsoft Edge on macOS...") + logger.info("[installer][web-edge] Installing Microsoft Edge on macOS...") await send_response({ "action": "status", "data": { @@ -597,7 +595,7 @@ async def _install_edge_darwin(installer_path, user_password: str = ""): ) if brew_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via homebrew") + logger.info("[installer][web-edge] Microsoft Edge installed via homebrew") return True # Fallback to .pkg installer (needs sudo) @@ -615,10 +613,10 @@ async def _install_edge_darwin(installer_path, user_password: str = ""): ) if pkg_result.returncode == 0: - print("[installer][web-edge] Microsoft Edge installed via .pkg") + logger.info("[installer][web-edge] Microsoft Edge installed via .pkg") return True else: - print(f"[installer][web-edge] Installation failed. Error: {pkg_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", pkg_result.stderr) await send_response({ "action": "status", "data": { @@ -632,7 +630,7 @@ async def _install_edge_darwin(installer_path, user_password: str = ""): # All installation methods failed if brew_result.returncode != 0: - print(f"[installer][web-edge] Installation failed. Error: {brew_result.stderr}") + logger.error("[installer][web-edge] Installation failed. Error: %s", brew_result.stderr) await send_response({ "action": "status", @@ -645,7 +643,7 @@ async def _install_edge_darwin(installer_path, user_password: str = ""): }) return False except Exception as e: - print(f"[installer][web-edge] macOS installation failed: {e}") + logger.exception("[installer][web-edge] macOS installation failed: %s", e) await send_response({ "action": "status", "data": { @@ -660,7 +658,7 @@ async def _install_edge_darwin(installer_path, user_password: str = ""): async def _verify_edge_installation(): """Verify that Edge is properly installed""" - print("[installer][web-edge] Verifying Microsoft Edge installation...") + logger.info("[installer][web-edge] Verifying Microsoft Edge installation...") await send_response({ "action": "status", "data": { @@ -681,11 +679,11 @@ async def _verify_edge_installation(): async def install(user_password: str = "") -> bool: """Main function to install Microsoft Edge""" - print("[installer][web-edge] Installing Microsoft Edge...") + logger.info("[installer][web-edge] Installing Microsoft Edge...") # Check if Edge is already installed if await check_status(): - print("[installer][web-edge] Microsoft Edge is already installed") + logger.info("[installer][web-edge] Microsoft Edge is already installed") return True installer_path = None @@ -721,10 +719,10 @@ async def install(user_password: str = "") -> bool: # Verify installation if not await _verify_edge_installation(): - print("[installer][web-edge] Microsoft Edge installation verification failed") + logger.error("[installer][web-edge] Microsoft Edge installation verification failed") return False - print("[installer][web-edge] Microsoft Edge installation complete") + logger.info("[installer][web-edge] Microsoft Edge installation complete") await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/web/mozilla.py b/Framework/install_handler/web/mozilla.py index fedfdc8a..a5853ec3 100644 --- a/Framework/install_handler/web/mozilla.py +++ b/Framework/install_handler/web/mozilla.py @@ -8,9 +8,12 @@ import tarfile import re from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response from settings import ZEUZ_NODE_DOWNLOADS_DIR +logger = get_logger() + def _is_windows(): """Check if running on Windows""" @@ -50,7 +53,7 @@ def _get_linux_package_manager(): async def check_status() -> bool: """Check if Mozilla Firefox is installed.""" - print("[installer][web-mozilla] Checking status...") + logger.info("[installer][web-mozilla] Checking status...") try: result = None @@ -165,7 +168,7 @@ async def check_status() -> bool: ) if result.returncode != 0: - print("[installer][web-mozilla] Not installed") + logger.info("[installer][web-mozilla] Not installed") await send_response({ "action": "status", "data": { @@ -180,7 +183,7 @@ async def check_status() -> bool: # Firefox version output is typically in stdout or stderr version_text = (result.stdout or result.stderr).strip() if not version_text: - print("[installer][web-mozilla] Not installed") + logger.info("[installer][web-mozilla] Not installed") await send_response({ "action": "status", "data": { @@ -192,7 +195,7 @@ async def check_status() -> bool: }) return False - print("[installer][web-mozilla] Already installed") + logger.info("[installer][web-mozilla] Already installed") await send_response({ "action": "status", "data": { @@ -205,7 +208,7 @@ async def check_status() -> bool: return True except (FileNotFoundError, OSError): # Firefox command not found - Firefox is not installed - print("[installer][web-mozilla] Not installed (firefox not found)") + logger.info("[installer][web-mozilla] Not installed (firefox not found)") await send_response({ "action": "status", "data": { @@ -217,7 +220,7 @@ async def check_status() -> bool: }) return False except Exception as e: - print(f"[installer][web-mozilla] Error checking status: {e}") + logger.error("[installer][web-mozilla] Error checking status: %s", e) await send_response({ "action": "status", "data": { @@ -232,7 +235,7 @@ async def check_status() -> bool: async def _download_firefox_installer(): """Download Firefox installer based on platform""" - print("[installer][web-mozilla] Downloading Mozilla Firefox installer...") + logger.info("[installer][web-mozilla] Downloading Mozilla Firefox installer...") await send_response({ "action": "status", "data": { @@ -297,8 +300,7 @@ async def _download_firefox_installer(): total_size = int(response.headers.get("content-length", 0)) chunk_size = 8192 downloaded = 0 - - count = [] + last_pct = [-1] with open(installer_path, "wb") as f: async for chunk in response.aiter_bytes(chunk_size): f.write(chunk) @@ -306,18 +308,15 @@ async def _download_firefox_installer(): if total_size > 0: progress = (downloaded / total_size) * 100 - bar_length = 50 - filled_length = int(bar_length * downloaded // total_size) - bar = '█' * filled_length + '-' * (bar_length - filled_length) - - mb_downloaded = downloaded / (1024 * 1024) - mb_total = total_size / (1024 * 1024) - - print(f"\r[installer][web-mozilla] |{bar}| {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end='', flush=True) - - p = round(mb_downloaded/mb_total, 1) - if p not in count: - count.append(p) + pct = int(progress) + if pct != last_pct[0]: + last_pct[0] = pct + bar_length = 50 + filled_length = int(bar_length * downloaded // total_size) + bar = '█' * filled_length + '-' * (bar_length - filled_length) + mb_downloaded = downloaded / (1024 * 1024) + mb_total = total_size / (1024 * 1024) + logger.info("[installer][web-mozilla] |%s| %d%% (%.1f/%.1f MB)", bar, pct, mb_downloaded, mb_total) asyncio.create_task(send_response({ "action": "status", "data": { @@ -334,13 +333,12 @@ async def _download_firefox_installer(): exe_files = list(download_dir.glob("*.exe")) if exe_files: installer_path = exe_files[0] # Use the first .exe file found - print(f"[installer][web-mozilla] Found installer file: {installer_path.name}") + logger.info("[installer][web-mozilla] Found installer file: %s", installer_path.name) - print() - print(f"[installer][web-mozilla] Download complete: {installer_path}") + logger.info("[installer][web-mozilla] Download complete: %s", installer_path) return installer_path except Exception as e: - print(f"\n[installer][web-mozilla] Download failed: {e}") + logger.error("[installer][web-mozilla] Download failed: %s", e) await send_response({ "action": "status", "data": { @@ -355,7 +353,7 @@ async def _download_firefox_installer(): async def _install_firefox_windows(installer_path, user_password: str = ""): """Install Firefox on Windows""" - print("[installer][web-mozilla] Installing Mozilla Firefox on Windows...") + logger.info("[installer][web-mozilla] Installing Mozilla Firefox on Windows...") await send_response({ "action": "status", "data": { @@ -379,11 +377,11 @@ async def _install_firefox_windows(installer_path, user_password: str = ""): exe_files = list(download_dir.glob("*.exe")) if exe_files: installer_exe = exe_files[0] - print(f"[installer][web-mozilla] Found installer: {installer_exe.name}") + logger.info("[installer][web-mozilla] Found installer: %s", installer_exe.name) if not installer_exe or not installer_exe.exists(): error_msg = "Firefox installer not found in download directory" - print(f"[installer][web-mozilla] {error_msg}") + logger.error("[installer][web-mozilla] %s", error_msg) await send_response({ "action": "status", "data": { @@ -400,8 +398,8 @@ async def _install_firefox_windows(installer_path, user_password: str = ""): installer_path_str = str(installer_exe).replace('/', '\\') ps_command = f'Start-Process "{installer_path_str}" -ArgumentList "/S" -Wait' - print(f"[installer][web-mozilla] Running installer: {installer_exe.name}") - print(f"[installer][web-mozilla] Command: {ps_command}") + logger.info("[installer][web-mozilla] Running installer: %s", installer_exe.name) + logger.info("[installer][web-mozilla] Command: %s", ps_command) # Run the command (UAC prompt will appear if needed for elevation) result = subprocess.run( @@ -412,11 +410,11 @@ async def _install_firefox_windows(installer_path, user_password: str = ""): ) if result.returncode == 0: - print("[installer][web-mozilla] Mozilla Firefox installed successfully") + logger.info("[installer][web-mozilla] Mozilla Firefox installed successfully") return True else: error_msg = f"Installation failed: {result.stderr or result.stdout}" - print(f"[installer][web-mozilla] {error_msg}") + logger.error("[installer][web-mozilla] %s", error_msg) await send_response({ "action": "status", "data": { @@ -428,7 +426,7 @@ async def _install_firefox_windows(installer_path, user_password: str = ""): }) return False except Exception as e: - print(f"[installer][web-mozilla] Windows installation failed: {e}") + logger.exception("[installer][web-mozilla] Windows installation failed: %s", e) import traceback traceback.print_exc() await send_response({ @@ -445,7 +443,7 @@ async def _install_firefox_windows(installer_path, user_password: str = ""): async def _install_firefox_linux(installer_path, user_password: str = ""): """Install Firefox on Linux""" - print("[installer][web-mozilla] Installing Mozilla Firefox on Linux...") + logger.info("[installer][web-mozilla] Installing Mozilla Firefox on Linux...") await send_response({ "action": "status", "data": { @@ -480,10 +478,10 @@ def run_sudo(cmd_list): tar_files = list(download_dir.glob("*.tar.xz")) if tar_files: tar_file = tar_files[0] # Use the first .tar.xz file found - print(f"[installer][web-mozilla] Found Firefox installer: {tar_file.name}") + logger.info("[installer][web-mozilla] Found Firefox installer: %s", tar_file.name) if tar_file and tar_file.exists(): - print("[installer][web-mozilla] Attempting to install from downloaded .tar.xz file...") + logger.info("[installer][web-mozilla] Attempting to install from downloaded .tar.xz file...") await send_response({ "action": "status", "data": { @@ -498,7 +496,7 @@ def run_sudo(cmd_list): extract_dir = ZEUZ_NODE_DOWNLOADS_DIR / "firefox" extract_dir.mkdir(parents=True, exist_ok=True) - print("[installer][web-mozilla] Extracting Firefox from archive...") + logger.info("[installer][web-mozilla] Extracting Firefox from archive...") await send_response({ "action": "status", "data": { @@ -525,7 +523,7 @@ def run_sudo(cmd_list): # Binary path: ~/.zeuz/zeuz_node_downloads/firefox/firefox/firefox firefox_binary_path = firefox_dir / "firefox" - print(f"[installer][web-mozilla] Creating symlink to Firefox binary...") + logger.info("[installer][web-mozilla] Creating symlink to Firefox binary...") await send_response({ "action": "status", "data": { @@ -548,7 +546,7 @@ def run_sudo(cmd_list): if symlink_path.exists() or symlink_path.is_symlink(): remove_result = run_sudo(["sudo", "rm", "-f", str(symlink_path)]) if remove_result.returncode != 0: - print(f"[installer][web-mozilla] Warning: Failed to remove existing {symlink_path}: {remove_result.stderr}") + logger.warning("[installer][web-mozilla] Warning: Failed to remove existing %s: %s", symlink_path, remove_result.stderr) # Create new symlink pointing directly to the binary in download directory symlink_result = run_sudo([ @@ -556,15 +554,15 @@ def run_sudo(cmd_list): ]) if symlink_result.returncode == 0: - print(f"[installer][web-mozilla] Created symlink: {symlink_path} -> {firefox_binary_path}") + logger.info("[installer][web-mozilla] Created symlink: %s -> %s", symlink_path, firefox_binary_path) run_sudo(["sudo", "chmod", "+x", str(symlink_path)]) symlink_created = True break else: - print(f"[installer][web-mozilla] Failed to create symlink at {symlink_path}: {symlink_result.stderr}") + logger.error("[installer][web-mozilla] Failed to create symlink at %s: %s", symlink_path, symlink_result.stderr) if not symlink_created: - print("[installer][web-mozilla] Failed to create symlink in any location") + logger.error("[installer][web-mozilla] Failed to create symlink in any location") else: # Create .desktop file to appear in application menu desktop_dir = Path.home() / ".local" / "share" / "applications" @@ -599,9 +597,9 @@ def run_sudo(cmd_list): f.write(desktop_content) # Make .desktop file executable os.chmod(desktop_file, 0o755) - print(f"[installer][web-mozilla] Created .desktop file: {desktop_file}") + logger.info("[installer][web-mozilla] Created .desktop file: %s", desktop_file) except Exception as e: - print(f"[installer][web-mozilla] Warning: Failed to create .desktop file: {e}") + logger.warning("[installer][web-mozilla] Warning: Failed to create .desktop file: %s", e) # Verify installation by testing firefox command (uses symlink) test_result = subprocess.run( @@ -611,13 +609,13 @@ def run_sudo(cmd_list): check=False ) if test_result.returncode == 0: - print(f"[installer][web-mozilla] Firefox successfully installed") - print(f"[installer][web-mozilla] Binary location: {firefox_binary_path}") - print(f"[installer][web-mozilla] Symlink: {symlink_path} -> {firefox_binary_path}") + logger.info("[installer][web-mozilla] Firefox successfully installed") + logger.info("[installer][web-mozilla] Binary location: %s", firefox_binary_path) + logger.info("[installer][web-mozilla] Symlink: %s -> %s", symlink_path, firefox_binary_path) return True else: error_msg = f"Firefox verification failed: {test_result.stderr}" - print(f"[installer][web-mozilla] {error_msg}") + logger.error("[installer][web-mozilla] %s", error_msg) await send_response({ "action": "status", "data": { @@ -630,7 +628,7 @@ def run_sudo(cmd_list): return False else: error_msg = "Could not find Firefox directory or binary after extraction" - print(f"[installer][web-mozilla] {error_msg}") + logger.error("[installer][web-mozilla] %s", error_msg) await send_response({ "action": "status", "data": { @@ -643,7 +641,7 @@ def run_sudo(cmd_list): return False except Exception as e: error_msg = f"Installation from .tar.xz failed: {str(e)}" - print(f"[installer][web-mozilla] {error_msg}") + logger.exception("[installer][web-mozilla] %s", error_msg) import traceback traceback.print_exc() await send_response({ @@ -659,7 +657,7 @@ def run_sudo(cmd_list): # If no installer file was downloaded or tar.xz installation not attempted error_msg = "Firefox installer file not found or invalid. Cannot proceed with installation." - print(f"[installer][web-mozilla] {error_msg}") + logger.error("[installer][web-mozilla] %s", error_msg) await send_response({ "action": "status", "data": { @@ -671,7 +669,7 @@ def run_sudo(cmd_list): }) return False except Exception as e: - print(f"[installer][web-mozilla] Linux installation failed: {e}") + logger.exception("[installer][web-mozilla] Linux installation failed: %s", e) await send_response({ "action": "status", "data": { @@ -686,7 +684,7 @@ def run_sudo(cmd_list): async def _install_firefox_darwin(installer_path, user_password: str = ""): """Install Firefox on macOS""" - print("[installer][web-mozilla] Installing Mozilla Firefox on macOS...") + logger.info("[installer][web-mozilla] Installing Mozilla Firefox on macOS...") await send_response({ "action": "status", "data": { @@ -707,7 +705,7 @@ async def _install_firefox_darwin(installer_path, user_password: str = ""): ) if brew_result.returncode == 0: - print("[installer][web-mozilla] Mozilla Firefox installed via homebrew") + logger.info("[installer][web-mozilla] Mozilla Firefox installed via homebrew") return True # Fallback to .dmg installer @@ -747,7 +745,7 @@ async def _install_firefox_darwin(installer_path, user_password: str = ""): ) if copy_result.returncode == 0: - print("[installer][web-mozilla] Mozilla Firefox installed via .dmg") + logger.info("[installer][web-mozilla] Mozilla Firefox installed via .dmg") return True finally: # Unmount the DMG (doesn't need sudo) @@ -769,7 +767,7 @@ async def _install_firefox_darwin(installer_path, user_password: str = ""): }) return False except Exception as e: - print(f"[installer][web-mozilla] macOS installation failed: {e}") + logger.exception("[installer][web-mozilla] macOS installation failed: %s", e) await send_response({ "action": "status", "data": { @@ -784,7 +782,7 @@ async def _install_firefox_darwin(installer_path, user_password: str = ""): async def _verify_firefox_installation(): """Verify that Firefox is properly installed""" - print("[installer][web-mozilla] Verifying Mozilla Firefox installation...") + logger.info("[installer][web-mozilla] Verifying Mozilla Firefox installation...") await send_response({ "action": "status", "data": { @@ -804,11 +802,11 @@ async def _verify_firefox_installation(): async def install(user_password: str = "") -> bool: """Main function to install Mozilla Firefox""" - print("[installer][web-mozilla] Installing Mozilla Firefox...") + logger.info("[installer][web-mozilla] Installing Mozilla Firefox...") # Check if Firefox is already installed if await check_status(): - print("[installer][web-mozilla] Mozilla Firefox is already installed") + logger.info("[installer][web-mozilla] Mozilla Firefox is already installed") return True installer_path = None @@ -844,13 +842,13 @@ async def install(user_password: str = "") -> bool: # Verify installation if not await _verify_firefox_installation(): - print("[installer][web-mozilla] Mozilla Firefox installation verification failed") + logger.error("[installer][web-mozilla] Mozilla Firefox installation verification failed") return False # Keep installer file in downloads directory (not cleaning up) # The installer is kept for potential reuse - print("[installer][web-mozilla] Mozilla Firefox installation complete") + logger.info("[installer][web-mozilla] Mozilla Firefox installation complete") await send_response({ "action": "status", "data": { diff --git a/Framework/install_handler/windows/inspector.py b/Framework/install_handler/windows/inspector.py index 68e3c982..91676d81 100644 --- a/Framework/install_handler/windows/inspector.py +++ b/Framework/install_handler/windows/inspector.py @@ -1,14 +1,16 @@ import httpx import asyncio from pathlib import Path +from Framework.install_handler.install_log_config import get_logger from Framework.install_handler.utils import send_response +logger = get_logger() inspector_path = Path("Apps/Windows/inspector.exe").absolute() async def check_status() -> bool: - print("[installer][windows-inspector] Checking status...") + logger.info("[installer][windows-inspector] Checking status...") exists = inspector_path.exists() if exists: - print("[installer][windows-inspector] Already installed") + logger.info("[installer][windows-inspector] Already installed") await send_response({ "action": "status", "data": { @@ -21,7 +23,7 @@ async def check_status() -> bool: }) return True else: - print("[installer][windows-inspector] Not installed") + logger.info("[installer][windows-inspector] Not installed") await send_response({ "action": "status", "data": { @@ -36,7 +38,7 @@ async def check_status() -> bool: async def install() -> bool: - print("[installer][windows-inspector] Installing...") + logger.info("[installer][windows-inspector] Installing...") status = inspector_path.exists() if status: @@ -50,7 +52,7 @@ async def install() -> bool: "install_text": "installed", } }) - print("[installer][windows-inspector] Already installed") + logger.info("[installer][windows-inspector] Already installed") return True url = "https://raw.githubusercontent.com/AutomationSolutionz/Zeuz_Python_Node_Setup/master/installation_files/Windows/inspector.exe" @@ -80,7 +82,7 @@ async def install() -> bool: mb_downloaded = downloaded / (1024 * 1024) mb_total = total_size / (1024 * 1024) - print(f"\r[installer][windows-inspector] |{bar}| {progress:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end='', flush=True) + logger.info("[installer][windows-inspector] |%s| %.1f%% (%.1f/%.1f MB)", bar, progress, mb_downloaded, mb_total) p = round(mb_downloaded/mb_total, 1) if p not in count: @@ -96,9 +98,8 @@ async def install() -> bool: } })) - print() - print(f"[installer][windows-inspector] Download completed: {inspector_path}") - print(f"[installer][windows-inspector] Installation successful") + logger.info("[installer][windows-inspector] Download completed: %s", inspector_path) + logger.info("[installer][windows-inspector] Installation successful") await send_response({ "action": "status", "data": { diff --git a/node_cli.py b/node_cli.py index 65fdcd85..6610b1be 100755 --- a/node_cli.py +++ b/node_cli.py @@ -400,7 +400,7 @@ def deploy_srv_addr(): from Framework import node_server_state - install_handler = InstallHandler() + install_handler = InstallHandler(log_dir=log_dir) install_task = asyncio.create_task(install_handler.run()) async def response_callback(response: str): diff --git a/server/installers.py b/server/installers.py index d7ba279c..a7ed2d45 100644 --- a/server/installers.py +++ b/server/installers.py @@ -12,9 +12,10 @@ from typing import Any, Literal from fastapi import APIRouter, HTTPException -from fastapi.responses import StreamingResponse +from fastapi.responses import FileResponse, StreamingResponse from pydantic import BaseModel +from Framework.install_handler import install_log_config from Framework.install_handler import utils as install_utils from Framework.install_handler.route import services as INSTALLER_SERVICES from Framework.install_handler.android.emulator import ( @@ -629,3 +630,25 @@ async def event_stream(): EVENT_BUS.unsubscribe("*", queue) return StreamingResponse(event_stream(), media_type="text/event-stream") + + +# --- Installer log download (node-side; Zeuz UI fetches from node host:port) --- # + + +@router.get("/logs/download") +async def download_installer_log(): + """Serve the current installer log file. Returns 404 if log dir or file is missing.""" + try: + log_dir = install_log_config.get_installer_log_dir() + log_path = log_dir / install_log_config.INSTALLER_LOG_FILENAME + if not log_path.is_file(): + raise HTTPException(status_code=404, detail="Installer log not found") + return FileResponse( + path=str(log_path), + filename=install_log_config.INSTALLER_LOG_FILENAME, + media_type="text/plain", + ) + except HTTPException: + raise + except Exception: + raise HTTPException(status_code=404, detail="Installer log not found") From f3be8fdd43ea6d35b5d283aa3e6f29997d065726 Mon Sep 17 00:00:00 2001 From: Zayadul-huq-afnan Date: Mon, 30 Mar 2026 13:00:59 +0600 Subject: [PATCH 2/2] separated the file for emulator install for mac and windows-linux. Fixed the emulator install bug by capping the latest API to level 36 , level 37 is not reliable still. Also prioritized host machine compatible architecture while downloading system-image, previously it was choosing the highest API regardless the host machine compatible architecture --- .../android/android_emulator.py | 79 +- .../install_handler/android/android_sdk.py | 8 +- Framework/install_handler/android/emulator.py | 7 + .../android/emulator_windows_linux.py | 1855 +++++++++++++++++ Framework/install_handler/android/java.py | 8 +- .../install_handler/install_log_config.py | 17 +- .../install_handler/long_poll_handler.py | 2 +- Framework/install_handler/route.py | 6 +- server/installers.py | 3 +- 9 files changed, 1967 insertions(+), 18 deletions(-) create mode 100644 Framework/install_handler/android/emulator_windows_linux.py diff --git a/Framework/install_handler/android/android_emulator.py b/Framework/install_handler/android/android_emulator.py index 8060ebc0..d16b7676 100644 --- a/Framework/install_handler/android/android_emulator.py +++ b/Framework/install_handler/android/android_emulator.py @@ -1,7 +1,78 @@ -async def check_status(): - print("[android_emulator] Checking status...") +import platform +from Framework.install_handler.install_log_config import get_logger +from . import emulator as emulator_macos +from . import emulator_windows_linux + +logger = get_logger() + +def _is_darwin() -> bool: + return platform.system().lower() == "darwin" + + +async def check_emulator_list(): + """ + OS router for AndroidEmulator system images services. + - macOS: uses `emulator.py` + - Windows/Linux: uses `emulator_windows_linux.py` + """ + system = platform.system().lower() + if system == "darwin": + logger.debug("[android_emulator] Routing to macOS emulator module") + return await emulator_macos.check_emulator_list() + + logger.debug("[android_emulator] Routing to Windows/Linux emulator module") + return await emulator_windows_linux.check_emulator_list() + + +async def android_emulator_install(): + """ + OS router for AndroidEmulator installation. + - macOS: uses `emulator.py` + - Windows/Linux: uses `emulator_windows_linux.py` + """ + system = platform.system().lower() + if system == "darwin": + logger.debug("[android_emulator] Routing to macOS emulator module") + return await emulator_macos.android_emulator_install() + + logger.debug("[android_emulator] Routing to Windows/Linux emulator module") + return await emulator_windows_linux.android_emulator_install() + + +async def create_avd_from_system_image(device_param: str) -> bool: + """ + Create an AVD from a chosen system-image device param. + Routes to the correct platform implementation dynamically. + """ + if _is_darwin(): + logger.debug("[android_emulator] Routing create_avd to macOS module") + return await emulator_macos.create_avd_from_system_image(device_param) + logger.debug("[android_emulator] Routing create_avd to Windows/Linux module") + return await emulator_windows_linux.create_avd_from_system_image(device_param) + + +async def get_filtered_avd_services(): + """ + Return filtered AVD services to display on the installer UI. + Routes to the correct platform implementation dynamically. + """ + if _is_darwin(): + logger.debug("[android_emulator] Routing get_filtered_avd_services to macOS module") + return await emulator_macos.get_filtered_avd_services() + logger.debug("[android_emulator] Routing get_filtered_avd_services to Windows/Linux module") + return await emulator_windows_linux.get_filtered_avd_services() + + +async def launch_avd(avd_name: str) -> bool: + """ + Launch an existing AVD. + Routes to the correct platform implementation dynamically. + """ + if _is_darwin(): + logger.debug("[android_emulator] Routing launch_avd to macOS module") + return await emulator_macos.launch_avd(avd_name) + logger.debug("[android_emulator] Routing launch_avd to Windows/Linux module") + return await emulator_windows_linux.launch_avd(avd_name) -async def install(): - print("[android_emulator] Installing...") diff --git a/Framework/install_handler/android/android_sdk.py b/Framework/install_handler/android/android_sdk.py index 5ceba203..e9c6d987 100644 --- a/Framework/install_handler/android/android_sdk.py +++ b/Framework/install_handler/android/android_sdk.py @@ -77,13 +77,13 @@ def update_android_sdk_path(): # Check if SDK exists adb_path = get_adb_path() if not adb_path.exists(): - logger.warning("[installer][android-sdk] Warning: Android SDK not found for PATH update.") - return + print(f"[installer][android-sdk] Warning: Android SDK not found for PATH update.") + return # Set ANDROID_HOME and ANDROID_SDK_ROOT for current process os.environ['ANDROID_HOME'] = str(sdk_root) os.environ['ANDROID_SDK_ROOT'] = str(sdk_root) - logger.info("[installer][android-sdk] ANDROID_HOME set for current process: %s", sdk_root) + print(f"[installer][android-sdk] ANDROID_HOME set for current process: {sdk_root}") # Add SDK paths to PATH for current process (prepend so they take precedence) sdk_paths = [ @@ -97,7 +97,7 @@ def update_android_sdk_path(): # Always prepend to ensure isolated SDK takes precedence (even if path already exists) os.environ['PATH'] = f"{sdk_path}{os.pathsep}{current_path}" current_path = os.environ['PATH'] - logger.info("[installer][android-sdk] Prepended to current process PATH: %s", sdk_path) + print(f"[installer][android-sdk] Prepended to current process PATH:{ sdk_path}") def _get_cmdline_tools_url() -> str: diff --git a/Framework/install_handler/android/emulator.py b/Framework/install_handler/android/emulator.py index 98c7d0bb..c1a834a5 100644 --- a/Framework/install_handler/android/emulator.py +++ b/Framework/install_handler/android/emulator.py @@ -1,3 +1,6 @@ +#this file is for mac os emulator installation. there are codes for windows and linux as well but they are not used for now. +#windows and linux codes are in emulator_windows_linux.py + import os import platform import subprocess @@ -1922,3 +1925,7 @@ async def create_avd_from_system_image(device_param: str) -> bool: } }) return False + + +######################################## + diff --git a/Framework/install_handler/android/emulator_windows_linux.py b/Framework/install_handler/android/emulator_windows_linux.py new file mode 100644 index 00000000..173dfd5b --- /dev/null +++ b/Framework/install_handler/android/emulator_windows_linux.py @@ -0,0 +1,1855 @@ +import os +import platform +import subprocess +import asyncio +import re +import random +from pathlib import Path +from settings import ZEUZ_NODE_DOWNLOADS_DIR +from Framework.install_handler.utils import send_response, debug +from Framework.install_handler.android.android_sdk import _get_sdk_root +from Framework.install_handler.install_log_config import get_logger + +logger = get_logger() + + +def _find_executable(base_path: Path, base_name: str) -> Path | None: + """Find an executable file with platform-specific extensions""" + system = platform.system() + if system == "Windows": + exts = [".exe", ".bat", ".cmd", ""] + elif system == "Linux": + exts = ["", ".sh"] + elif system == "Darwin": # iOS/macOS + exts = ["", ".sh"] + else: + return None + + for ext in exts: + p = base_path / (base_name + ext) + if p.is_file(): + return p + return None + + +def _find_avdmanager(sdk_root: Path) -> Path | None: + """Find avdmanager executable""" + return _find_executable(sdk_root / "cmdline-tools" / "latest" / "bin", "avdmanager") + + +def _find_sdkmanager(sdk_root: Path) -> Path | None: + """Find sdkmanager executable""" + return _find_executable(sdk_root / "cmdline-tools" / "latest" / "bin", "sdkmanager") + + +def _is_windows(): + """Check if running on Windows""" + return platform.system() == 'Windows' + + +def _is_linux(): + """Check if running on Linux""" + return platform.system() == 'Linux' + + +def _is_darwin(): + """Check if running on macOS""" + return platform.system() == 'Darwin' + + +def get_emulator_command(): + """ + Returns the correct emulator executable path depending on OS. + Uses isolated SDK installation from android_sdk.py + """ + system = platform.system() + sdk_root = _get_sdk_root() + + if debug: + logger.debug("Launch avd: %s", sdk_root) + + if system == "Windows": + return os.path.join(str(sdk_root), "emulator", "emulator.exe") + + elif system == "Darwin": # macOS + return os.path.join(str(sdk_root), "emulator", "emulator") + + elif system == "Linux": + return os.path.join(str(sdk_root), "emulator", "emulator") + + else: + raise RuntimeError(f"Unsupported OS: {system}") + + +async def get_available_avds() -> list[dict]: + """ + List available Android Virtual Devices (AVDs) by running avdmanager list avd. + Returns a list of dictionaries with name and comment fields. + """ + try: + sdk_root = _get_sdk_root() + + # Check if Android SDK is installed + if sdk_root is None: + if debug: + logger.debug("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + return [] + + avdmanager = _find_avdmanager(sdk_root) + + if not avdmanager: + if debug: + logger.debug("[installer][emulator] avdmanager not found") + return [] + + # Run avdmanager list avd command using async executor + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, + lambda: subprocess.run( + [str(avdmanager), "list", "avd"], + capture_output=True, + text=True, + timeout=30 + ) + ) + + if result.returncode != 0: + if debug: + logger.debug("[installer][emulator] avdmanager list avd failed: %s", result.stderr) + return [] + + output = result.stdout + + # Parse the output preserving original formatting + avds = [] + current_avd = {} + comment_lines = [] + lines = output.split('\n') + + for line in lines: + stripped_line = line.strip() + + # Skip empty lines and header + if not stripped_line: + # Preserve empty lines if we're collecting comments (they're part of formatting) + if current_avd.get("name") and comment_lines: + # Only add empty line if it's not trailing (there are non-empty lines after) + pass # We'll skip empty lines to avoid trailing ones + continue + + if stripped_line.startswith("Available Android Virtual Devices"): + continue + + # Skip separator lines (e.g., "---------") + if stripped_line.startswith("-") and all(c == "-" for c in stripped_line): + continue + + # Parse Name + if stripped_line.startswith("Name:"): + # Save previous AVD if exists + if current_avd.get("name"): + # Join all comment lines preserving original format + current_avd["comment"] = "\n".join(comment_lines).rstrip() + avds.append(current_avd) + comment_lines = [] + + # Start new AVD + name = stripped_line.replace("Name:", "").strip() + + current_avd = { + "name": name, + "status": "installed", + "comment": "", + "install_text": "", + "os": ["windows", "linux", "darwin"], + "check_text": "Launch", + "status_function": lambda avd=name: launch_avd(avd), + "user_password": "no", + } + continue + + # For all other lines (Path, Target, Based on, Tag/ABI, Sdcard) + # Preserve the original line format (including indentation) + if current_avd.get("name"): + comment_lines.append(line) + + # Don't forget the last AVD + if current_avd.get("name"): + current_avd["comment"] = "\n".join(comment_lines).rstrip() + avds.append(current_avd) + + return avds + + except subprocess.TimeoutExpired: + if debug: + logger.debug("[installer][emulator] avdmanager list avd timed out") + return [] + except Exception as e: + if debug: + logger.debug("[installer][emulator] Error listing AVDs: %s", e) + import traceback + traceback.print_exc() + return [] + + +async def launch_avd(avd_name: str) -> bool: + """ + Launch AVD using emulator command determined by OS. + Non-blocking - the emulator starts in the background. + Sends response to server on success or failure. + """ + try: + emulator_path = get_emulator_command() + + # Launch emulator in background using Popen (non-blocking) + # Popen returns immediately, so we can call it directly without blocking + process = subprocess.Popen( + [emulator_path, "-avd", avd_name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True # Detach from parent process + ) + + logger.info("[installer][emulator] Launching AVD: %s... (PID: %s)", avd_name, process.pid) + + # Send success response to server + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "name": avd_name, + "status": "installed", + "comment": f"Emulator {avd_name} is launching (PID: {process.pid})", + } + }) + return True + + except FileNotFoundError: + error_msg = f"Emulator executable not found" + + logger.error("[installer][emulator] %s", error_msg) + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "name": avd_name, + "status": "not installed", + "comment": f"Failed to launch {avd_name}: {error_msg}", + } + }) + return False + except Exception as e: + error_msg = f"Failed to launch AVD {avd_name}: {e}" + + logger.error("[installer][emulator] %s", error_msg) + import traceback + traceback.print_exc() + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "name": avd_name, + "status": "not installed", + "comment": error_msg, + } + }) + return False + +async def get_filtered_avd_services(): + """ + Get available AVDs and filter them by OS, returning a formatted category dictionary. + Uses the same filtering logic as generate_services_list in utils.py. + + Returns: + Dictionary with category "AndroidEmulator" and filtered services, or None if no AVDs match + """ + current_os = platform.system().lower() + + try: + avds = await get_available_avds() + + if not avds: + return None + + # Filter AVDs by OS (same logic as generate_services_list) + filtered_services = [] + for avd in avds: + # Check if AVD supports current OS + if "os" in avd and current_os not in avd["os"]: + continue + + # Create filtered service with only the fields needed (matching generate_services_list format) + filtered_service = { + "name": avd.get("name", ""), + "status": avd.get("status", "installed"), + "comment": avd.get("comment", ""), + "install_text": avd.get("install_text", ""), + "check_text": "Launch", + "os": avd.get("os", []), + "user_password": avd.get("user_password", "no") + } + filtered_services.append(filtered_service) + + # Return None if no services match, otherwise return category dict + if not filtered_services: + return None + + return { + "category": "AndroidEmulator", + "services": filtered_services + } + + except Exception as e: + if debug: + logger.debug("[installer][emulator] Error getting filtered AVD services: %s", e) + import traceback + traceback.print_exc() + return None + +def _run_sdkmanager_list(sdkmanager: Path, sdk_root: Path) -> str: + """Run sdkmanager --list with shell piping to filter system-images (OS-agnostic, synchronous)""" + try: + system = platform.system() + + if system == "Windows": + # Windows: Use PowerShell Select-String + command = f'& "{sdkmanager}" --sdk_root="{sdk_root}" --list | Select-String "system-images"' + result = subprocess.run( + ["powershell", "-Command", command], + capture_output=True, + text=True, + timeout=60 + ) + elif system in ["Linux", "Darwin"]: + # Linux/macOS: Use grep + command = f'"{sdkmanager}" --sdk_root="{sdk_root}" --list | grep "system-images"' + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=60 + ) + else: + # Fallback: run without filtering + result = subprocess.run( + [str(sdkmanager), f"--sdk_root={sdk_root}", "--list"], + capture_output=True, + text=True, + timeout=60 + ) + if result.returncode == 0: + # Filter manually + lines = result.stdout.split('\n') + filtered = [line for line in lines if 'system-images' in line] + return '\n'.join(filtered) + else: + if debug: + logger.debug("[installer][emulator] sdkmanager --list failed: %s", result.stderr) + return "" + + if result.returncode == 0: + return result.stdout + else: + if debug: + logger.debug("[installer][emulator] sdkmanager --list failed: %s", result.stderr) + return "" + except subprocess.TimeoutExpired: + if debug: + logger.debug("[installer][emulator] sdkmanager --list timed out") + return "" + except Exception as e: + if debug: + logger.debug("[installer][emulator] Error running sdkmanager --list: %s", e) + return "" + + +def _parse_system_image_details(output: str) -> list[dict]: + """ + Parse system image details from sdkmanager --list output. + Expected format: system-images;android-36.1;google_apis_playstore;x86_64 | 3 | Google Play Intel x86_64 Atom System Image + Returns list of dicts with package, version, and description. + """ + system_images = [] + lines = output.split('\n') + + for line in lines: + stripped = line.strip() + if not stripped or not stripped.startswith('system-images;'): + continue + + # Parse the line: package | version | description + # Split by | and clean up + parts = [p.strip() for p in stripped.split('|')] + + if len(parts) >= 1: + package = parts[0].strip() + + # Extract additional info if available + version = parts[1].strip() if len(parts) > 1 else "" + description = parts[2].strip() if len(parts) > 2 else "" + + system_images.append({ + "package": package, + "version": version, + "description": description, + "status" : "not installed", + "comment" : "" + }) + + return system_images + + +async def get_available_system_images() -> list[dict]: + """ + Get available system images by running sdkmanager --list and parsing system-images. + Returns a list of dictionaries with package, version, and description. + Example: [{"package": "system-images;android-34;google_apis;x86_64", "version": "3", "description": "Google APIs Intel x86_64 Atom System Image"}] + """ + try: + sdk_root = _get_sdk_root() + + # Check if Android SDK is installed + if sdk_root is None: + if debug: + logger.debug("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + await send_response({ + "action": "status", + "data": { + "category": "Android Emulator", + "name": "System Images", + "status": "not installed", + "comment": "No system images found. Please make sure you have installed the ANDROID SDK components.", + } + }) + return [] + + sdkmanager = _find_sdkmanager(sdk_root) + + if not sdkmanager: + if debug: + logger.debug("[installer][emulator] sdkmanager not found") + return [] + + # Check platform support + if not (_is_windows() or _is_linux() or _is_darwin()): + if debug: + logger.debug("[installer][emulator] Unsupported platform: %s", platform.system()) + return [] + + # Run sdkmanager --list using async executor + loop = asyncio.get_event_loop() + output = await loop.run_in_executor( + None, + _run_sdkmanager_list, + sdkmanager, + sdk_root + ) + + if not output: + if debug: + logger.debug("[installer][emulator] sdkmanager --list returned empty output") + return [] + + # Parse system image details from output + system_images = _parse_system_image_details(output) + + # Remove duplicates based on package name and sort + seen = set() + unique_images = [] + for img in system_images: + if img["package"] not in seen: + seen.add(img["package"]) + unique_images.append(img) + + # Sort by package name + system_images = sorted(unique_images, key=lambda x: x["package"]) + + if debug: + logger.debug("[installer][emulator] Found %s available system images", len(system_images)) + return system_images + + except subprocess.TimeoutExpired: + if debug: + logger.debug("[installer][emulator] sdkmanager --list timed out") + return [] + except Exception as e: + if debug: + logger.debug("[installer][emulator] Error getting available system images: %s", e) + import traceback + traceback.print_exc() + return [] + + +def _parse_device_list(output: str) -> list[dict]: + """ + Parse device list from avdmanager list device output. + package = device id + version = device name + description = OEM + As perviously, we were sending avaailable system images list, this is done to + keep the response send to the server same as before. Minimal changes required in the server. + """ + devices = [] + lines = output.split('\n') + current_device = {} + + for line in lines: + stripped = line.strip() + + # Skip empty lines and header + if not stripped or stripped.startswith("Available devices definitions:"): + continue + + # Skip separator lines + if stripped.startswith("-") and all(c == "-" for c in stripped): + # Save previous device if exists + if current_device.get("package"): + # Add status and comment fields before appending + current_device["status"] = "Not installed" + current_device["comment"] = "" + devices.append(current_device) + current_device = {} + continue + + # Parse device ID: id: 0 or "desktop_large" + if stripped.startswith("id:"): + # Extract the quoted part (device ID) -> package + # Format: id: 2 or "desktop_large" + match = re.search(r'"([^"]+)"', stripped) + if match: + current_device["package"] = match.group(1) + else: + # Fallback: try to extract numeric ID + match = re.search(r'id:\s*(\d+)', stripped) + if match: + current_device["package"] = match.group(1) + continue + + # Parse Name: Name: Large Desktop -> version + if stripped.startswith("Name:"): + name = stripped.replace("Name:", "").strip() + current_device["version"] = name + continue + + # Parse OEM: OEM : Google -> description + if stripped.startswith("OEM"): + oem = stripped.split(":", 1)[1].strip() if ":" in stripped else "" + current_device["description"] = oem + continue + + # Don't forget the last device + if current_device.get("package"): + # Add status and comment fields before appending + current_device["status"] = "Not installed" + current_device["comment"] = "" + devices.append(current_device) + + return devices + + +async def get_available_devices() -> list[dict]: + """ + Get available devices by running avdmanager list device. + Returns a list of dictionaries with package, version, and description (matching system images format). + """ + try: + sdk_root = _get_sdk_root() + + # Check if Android SDK is installed + if sdk_root is None: + if debug: + logger.debug("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + await send_response({ + "action": "status", + "data": { + "category": "Android Emulator", + "name": "System Images", + "status": "Not Found", + "comment": "No devices found. Please make sure you have installed the ANDROID SDK components.", + } + }) + return [] + + avdmanager = _find_avdmanager(sdk_root) + + if not avdmanager: + if debug: + logger.debug("[installer][emulator] avdmanager not found") + return [] + + # Run avdmanager list device using async executor + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, + lambda: subprocess.run( + [str(avdmanager), "list", "device"], + capture_output=True, + text=True, + timeout=60 + ) + ) + + if result.returncode != 0: + if debug: + logger.debug("[installer][emulator] avdmanager list device failed: %s", result.stderr) + return [] + + # Parse device details from output + devices = _parse_device_list(result.stdout) + + # Filter out devices that already have AVDs created + existing_avds = await get_available_avds() + existing_avd_names = {avd["name"] for avd in existing_avds} + + filtered_devices = [] + for device in devices: + device_name = device.get("version", "") # version field contains device name + if device_name: + # Sanitize device name to match how AVD names are created + sanitized_name = _sanitize_avd_name(device_name) + # Check if an AVD with this name already exists + if sanitized_name not in existing_avd_names: + filtered_devices.append(device) + elif debug: + logger.debug( + "[installer][emulator] Filtering out device '%s' (AVD '%s' already exists)", + device_name, + sanitized_name, + ) + else: + # If no device name, include it (shouldn't happen, but safe fallback) + filtered_devices.append(device) + + if debug: + logger.debug( + "[installer][emulator] Found %s available devices, %s not yet installed", + len(devices), + len(filtered_devices), + ) + return filtered_devices + + except subprocess.TimeoutExpired: + if debug: + logger.debug("[installer][emulator] avdmanager list device timed out") + return [] + except Exception as e: + if debug: + logger.debug("[installer][emulator] Error getting available devices: %s", e) + import traceback + traceback.print_exc() + return [] + +async def check_emulator_list(): + """ + Sends response to server with list of installed emulators for Android. + """ + avd_list = await get_filtered_avd_services() + if avd_list: + await send_response( + { + "action": "services_update", + "data": { + 'category': 'AndroidEmulator', + "services": avd_list['services'], + }, + } + ) + return True + return False + +async def android_emulator_install(): + """ + Get available devices when install button is clicked. + Returns list of available devices for emulator installation. + """ + if debug: + logger.debug("[installer][emulator] Getting available devices...") + + try: + # Check if Android SDK is installed first + sdk_root = _get_sdk_root() + if sdk_root is None: + if debug: + logger.debug("[installer][emulator] Android SDK not found") + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "name": "System Images", + "status": "Not Found", + "comment": "Download and install Android SDK first", + "installables": [] + } + }) + return False + + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "name": "Devices", + "status": "Fetching", + "comment": "Fetching available devices...", + "installables": [] + } + }) + + # Get available devices + devices = await get_available_devices() + if debug: + logger.debug("[installer][emulator] Available devices: %s", devices) + + + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + ##"name": "System Images", + #"status": "Found", + # "comment": f"Available devices ({len(devices)} total)", + "installables": devices, # Send the full list with details + } + }) + return True + + except Exception as e: + if debug: + logger.debug("[installer][emulator] Error getting devices: %s", e) + import traceback + traceback.print_exc() + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "name": "Devices", + "status": "not installed", + "comment": f"Error getting devices: {str(e)}", + "installables": [] + } + }) + return False + + +# List of 20 four-letter words for AVD name generation +_AVD_NAME_WORDS = [ + "blue", "fast", "cool", "wave", "star", "moon", "fire", "wind", "rock", "tree", + "lake", "snow", "rain", "gold", "dark", "light", "bold", "soft", "wild", "calm" +] + + +def _extract_android_version(system_image_name: str) -> str: + """ + Extract Android version from system image name. + Example: system-images;android-36-ext18;google_apis;arm64-v8 -> android-36 + """ + # Split by semicolon and get the second part (android-XX-...) + parts = system_image_name.split(';') + if len(parts) < 2: + raise ValueError(f"Invalid system image name format: {system_image_name}") + + android_part = parts[1] # e.g., "android-36-ext18" + + # Extract just the version part (android-XX) + # Match "android-" followed by digits + match = re.match(r'android-(\d+)', android_part) + if not match: + raise ValueError(f"Could not extract Android version from: {android_part}") + + return f"android-{match.group(1)}" + + +def _get_existing_avd_names() -> list[str]: + """Get list of existing AVD names""" + try: + sdk_root = _get_sdk_root() + if sdk_root is None: + return [] + + avdmanager = _find_avdmanager(sdk_root) + if not avdmanager: + return [] + + result = subprocess.run( + [str(avdmanager), "list", "avd"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode != 0: + return [] + + # Parse AVD names from output + avd_names = [] + for line in result.stdout.split('\n'): + if line.strip().startswith('Name:'): + name = line.strip().replace('Name:', '').strip() + if name: + avd_names.append(name) + + return avd_names + except Exception: + return [] + + +def _generate_avd_name(android_version: str, existing_avds: list[str]) -> str: + """ + Generate a unique AVD name by combining Android version with two random words. + Format: android-{version}-{word1}-{word2} + """ + max_attempts = 100 # Prevent infinite loop + + for _ in range(max_attempts): + # Pick two random words + word1, word2 = random.sample(_AVD_NAME_WORDS, 2) + avd_name = f"{android_version}-{word1}-{word2}" + + # Check if AVD name already exists + if avd_name not in existing_avds: + return avd_name + + # Fallback: add random number if all combinations are taken + word1, word2 = random.sample(_AVD_NAME_WORDS, 2) + random_num = random.randint(1000, 9999) + return f"{android_version}-{word1}-{word2}-{random_num}" + + +def _run_sdkmanager_install_windows(sdkmanager: Path, sdk_root: Path, system_image: str, loop=None, device_id: str = None) -> tuple[bool, str]: + """Install system image on Windows with real-time output""" + try: + # Use PowerShell to handle the command properly and auto-accept licenses + # This approach pipes 'y' responses to automatically accept licenses + yes_responses = ";".join(["echo y"] * 20) + quoted_image = f"'{system_image}'" + shell_cmd = f'powershell -Command "{yes_responses} | &\\"{str(sdkmanager)}\\" --sdk_root={sdk_root} {quoted_image}"' + + if debug: + logger.debug( + "[installer][emulator] Running: sdkmanager --sdk_root=%s %s", + sdk_root, + system_image, + ) + logger.debug("[installer][emulator] This may take 10-30 minutes to download system image...") + + process = subprocess.Popen( + shell_cmd, + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 # Line buffered + ) + # Close stdin - PowerShell piping will provide input + process.stdin.close() + + # Print output in real-time as it comes, showing progress on single line + output_lines = [] + last_progress = "" + progress_count = [] + try: + for line in iter(process.stdout.readline, ''): + if line: + stripped = line.strip() + output_lines.append(stripped) + + # Extract progress percentage from lines like "[====] 25% Loading..." + progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped) + if progress_match: + percent = int(progress_match.group(1)) + status = progress_match.group(2).strip() + current_progress = f"{percent}% {status}" + if current_progress != last_progress: + logger.info("[installer][emulator] Download progress: %s", current_progress) + last_progress = current_progress + + if loop and device_id: + rounded_percent = round(percent / 10) * 10 + if rounded_percent not in progress_count: + progress_count.append(rounded_percent) + asyncio.run_coroutine_threadsafe( + send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Downloading system image... {percent}% {status}", + } + }), + loop + ) + elif stripped and not stripped.startswith('[') and '%' not in stripped: + # Print important non-progress messages on new line + logger.info("[installer][emulator] %s", stripped) + elif stripped.endswith('%'): + # Handle lines that end with just percentage + percent_match = re.search(r'(\d+)%', stripped) + if percent_match: + percent = int(percent_match.group(1)) + logger.info("[installer][emulator] Download progress: %s", stripped) + + if loop and device_id: + rounded_percent = round(percent / 10) * 10 + if rounded_percent not in progress_count: + progress_count.append(rounded_percent) + asyncio.run_coroutine_threadsafe( + send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Downloading system image... {percent}%", + } + }), + loop + ) + except Exception as e: + logger.error("[installer][emulator] Output reading error: %s", e) + finally: + logger.debug("[installer][emulator] Download progress complete") + + process.stdout.close() + returncode = process.wait(timeout=1800) # 30 minutes for large system image downloads + + output = "\n".join(output_lines) + if returncode == 0: + return True, output + else: + return False, output + except subprocess.TimeoutExpired: + return False, "Installation timed out after 30 minutes" + except Exception as e: + return False, str(e) + + +def _run_sdkmanager_install_linux(sdkmanager: Path, sdk_root: Path, system_image: str, loop=None, device_id: str = None) -> tuple[bool, str]: + """Install system image on Linux with real-time output""" + try: + if debug: + logger.debug( + "[installer][emulator] Running: sdkmanager --sdk_root=%s %s", + sdk_root, + system_image, + ) + logger.debug("[installer][emulator] This may take 10-30 minutes to download system image...") + + process = subprocess.Popen( + [str(sdkmanager), f"--sdk_root={sdk_root}", system_image], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 # Line buffered + ) + + # Print output in real-time as it comes, showing progress on single line + output_lines = [] + last_progress = "" + progress_count = [] + try: + for line in iter(process.stdout.readline, ''): + if line: + stripped = line.strip() + output_lines.append(stripped) + + # Extract progress percentage from lines like "[====] 25% Loading..." + progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped) + if progress_match: + percent = int(progress_match.group(1)) + status = progress_match.group(2).strip() + current_progress = f"{percent}% {status}" + if current_progress != last_progress: + logger.info("[installer][emulator] Download progress: %s", current_progress) + last_progress = current_progress + + if loop and device_id: + rounded_percent = round(percent / 10) * 10 + if rounded_percent not in progress_count: + progress_count.append(rounded_percent) + asyncio.run_coroutine_threadsafe( + send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Downloading system image... {percent}% {status}", + } + }), + loop + ) + elif stripped and not stripped.startswith('[') and '%' not in stripped: + # Print important non-progress messages on new line + logger.info("[installer][emulator] %s", stripped) + elif stripped.endswith('%'): + # Handle lines that end with just percentage + percent_match = re.search(r'(\d+)%', stripped) + if percent_match: + percent = int(percent_match.group(1)) + logger.info("[installer][emulator] Download progress: %s", stripped) + + if loop and device_id: + rounded_percent = round(percent / 10) * 10 + if rounded_percent not in progress_count: + progress_count.append(rounded_percent) + asyncio.run_coroutine_threadsafe( + send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Downloading system image... {percent}%", + } + }), + loop + ) + except Exception as e: + logger.error("[installer][emulator] Output reading error: %s", e) + finally: + logger.debug("[installer][emulator] Download progress complete") + + process.stdout.close() + returncode = process.wait(timeout=1800) # 30 minutes for large system image downloads + + output = "\n".join(output_lines) + if returncode == 0: + return True, output + else: + return False, output + except subprocess.TimeoutExpired: + return False, "Installation timed out after 30 minutes" + except Exception as e: + return False, str(e) + + +def _run_sdkmanager_install_darwin(sdkmanager: Path, sdk_root: Path, system_image: str, loop=None, device_id: str = None) -> tuple[bool, str]: + """Install system image on macOS with real-time output""" + try: + if debug: + logger.debug( + "[installer][emulator] Running: sdkmanager --sdk_root=%s %s", + sdk_root, + system_image, + ) + logger.debug("[installer][emulator] This may take 10-30 minutes to download system image...") + + process = subprocess.Popen( + [str(sdkmanager), f"--sdk_root={sdk_root}", system_image], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 # Line buffered + ) + + # Print output in real-time as it comes, showing progress on single line + output_lines = [] + last_progress = "" + progress_count = [] + try: + for line in iter(process.stdout.readline, ''): + if line: + stripped = line.strip() + output_lines.append(stripped) + + # Extract progress percentage from lines like "[====] 25% Loading..." + progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped) + if progress_match: + percent = int(progress_match.group(1)) + status = progress_match.group(2).strip() + current_progress = f"{percent}% {status}" + if current_progress != last_progress: + logger.info("[installer][emulator] Download progress: %s", current_progress) + last_progress = current_progress + + if loop and device_id: + rounded_percent = round(percent / 10) * 10 + if rounded_percent not in progress_count: + progress_count.append(rounded_percent) + asyncio.run_coroutine_threadsafe( + send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Downloading system image... {percent}% {status}", + } + }), + loop + ) + elif stripped and not stripped.startswith('[') and '%' not in stripped: + # Print important non-progress messages on new line + logger.info("[installer][emulator] %s", stripped) + elif stripped.endswith('%'): + # Handle lines that end with just percentage + percent_match = re.search(r'(\d+)%', stripped) + if percent_match: + percent = int(percent_match.group(1)) + logger.info("[installer][emulator] Download progress: %s", stripped) + + if loop and device_id: + rounded_percent = round(percent / 10) * 10 + if rounded_percent not in progress_count: + progress_count.append(rounded_percent) + asyncio.run_coroutine_threadsafe( + send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Downloading system image... {percent}%", + } + }), + loop + ) + except Exception as e: + logger.error("[installer][emulator] Output reading error: %s", e) + finally: + logger.debug("[installer][emulator] Download progress complete") + + process.stdout.close() + returncode = process.wait(timeout=1800) # 30 minutes for large system image downloads + + output = "\n".join(output_lines) + if returncode == 0: + return True, output + else: + return False, output + except subprocess.TimeoutExpired: + return False, "Installation timed out after 30 minutes" + except Exception as e: + return False, str(e) + + +def _run_avdmanager_create_windows(avdmanager: Path, sdk_root: Path, avd_name: str, system_image: str, device_id: str) -> tuple[bool, str]: + """Create AVD on Windows with real-time output""" + try: + # Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id} + # Answer "no" to custom hardware profile prompt + process = subprocess.Popen( + [str(avdmanager), "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 # Line buffered + ) + + # Send "no" to custom hardware profile prompt + process.stdin.write("no\n") + process.stdin.close() + + # Print output in real-time as it comes, showing progress on single line + output_lines = [] + last_progress = "" + try: + for line in iter(process.stdout.readline, ''): + if line: + stripped = line.strip() + output_lines.append(stripped) + + # Extract progress percentage from lines like "[====] 25% Loading..." + progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped) + if progress_match: + percent = progress_match.group(1) + status = progress_match.group(2).strip() + current_progress = f"{percent}% {status}" + if current_progress != last_progress: + logger.info("[installer][emulator] Download progress: %s", current_progress) + last_progress = current_progress + elif stripped and not stripped.startswith('[') and '%' not in stripped: + # Print important non-progress messages on new line + logger.info("[installer][emulator] %s", stripped) + elif stripped.endswith('%'): + # Handle lines that end with just percentage + logger.info("[installer][emulator] Download progress: %s", stripped) + except Exception as e: + logger.error("[installer][emulator] Output reading error: %s", e) + finally: + logger.debug("[installer][emulator] Creation progress complete") + + process.stdout.close() + returncode = process.wait(timeout=120) + + output = "\n".join(output_lines) + if returncode == 0: + return True, output + else: + return False, output + except subprocess.TimeoutExpired: + return False, "AVD creation timed out" + except Exception as e: + return False, str(e) + + +def _run_avdmanager_create_linux(avdmanager: Path, sdk_root: Path, avd_name: str, system_image: str, device_id: str) -> tuple[bool, str]: + """Create AVD on Linux with real-time output""" + try: + # Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id} + # Answer "no" to custom hardware profile prompt + process = subprocess.Popen( + [str(avdmanager), "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 # Line buffered + ) + + # Send "no" to custom hardware profile prompt + process.stdin.write("no\n") + process.stdin.close() + + # Print output in real-time as it comes, showing progress on single line + output_lines = [] + last_progress = "" + try: + for line in iter(process.stdout.readline, ''): + if line: + stripped = line.strip() + output_lines.append(stripped) + + # Extract progress percentage from lines like "[====] 25% Loading..." + progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped) + if progress_match: + percent = progress_match.group(1) + status = progress_match.group(2).strip() + current_progress = f"{percent}% {status}" + if current_progress != last_progress: + logger.info("[installer][emulator] Download progress: %s", current_progress) + last_progress = current_progress + elif stripped and not stripped.startswith('[') and '%' not in stripped: + # Print important non-progress messages on new line + logger.info("[installer][emulator] %s", stripped) + elif stripped.endswith('%'): + # Handle lines that end with just percentage + logger.info("[installer][emulator] Download progress: %s", stripped) + except Exception as e: + logger.error("[installer][emulator] Output reading error: %s", e) + finally: + logger.debug("[installer][emulator] Creation progress complete") + + process.stdout.close() + returncode = process.wait(timeout=120) + + output = "\n".join(output_lines) + if returncode == 0: + return True, output + else: + return False, output + except subprocess.TimeoutExpired: + return False, "AVD creation timed out" + except Exception as e: + return False, str(e) + + +def _run_avdmanager_create_darwin(avdmanager: Path, sdk_root: Path, avd_name: str, system_image: str, device_id: str) -> tuple[bool, str]: + """Create AVD on macOS with real-time output""" + try: + # Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id} + # Answer "no" to custom hardware profile prompt + process = subprocess.Popen( + [str(avdmanager), "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 # Line buffered + ) + + # Send "no" to custom hardware profile prompt + process.stdin.write("no\n") + process.stdin.close() + + # Print output in real-time as it comes, showing progress on single line + output_lines = [] + last_progress = "" + try: + for line in iter(process.stdout.readline, ''): + if line: + stripped = line.strip() + output_lines.append(stripped) + + # Extract progress percentage from lines like "[====] 25% Loading..." + progress_match = re.search(r'\[.*?\]\s*(\d+)%\s*(.+)', stripped) + if progress_match: + percent = progress_match.group(1) + status = progress_match.group(2).strip() + current_progress = f"{percent}% {status}" + if current_progress != last_progress: + logger.info("[installer][emulator] Download progress: %s", current_progress) + last_progress = current_progress + elif stripped and not stripped.startswith('[') and '%' not in stripped: + # Print important non-progress messages on new line + logger.info("[installer][emulator] %s", stripped) + elif stripped.endswith('%'): + # Handle lines that end with just percentage + logger.info("[installer][emulator] Download progress: %s", stripped) + except Exception as e: + logger.error("[installer][emulator] Output reading error: %s", e) + finally: + logger.debug("[installer][emulator] Creation progress complete") + + process.stdout.close() + returncode = process.wait(timeout=120) + + output = "\n".join(output_lines) + if returncode == 0: + return True, output + else: + return False, output + except subprocess.TimeoutExpired: + return False, "AVD creation timed out" + except Exception as e: + return False, str(e) + + +def _sanitize_avd_name(device_name: str) -> str: + """ + Sanitize device name to create a valid AVD name. + AVD names can only contain: a-z A-Z 0-9 . _ - + + Args: + device_name: Original device name (e.g., "Pixel 6") + + Returns: + Sanitized AVD name (e.g., "Pixel-6") + """ + # Replace spaces with hyphens + sanitized = device_name.replace(" ", "-") + + # Remove any characters that are not allowed (a-z A-Z 0-9 . _ -) + sanitized = re.sub(r'[^a-zA-Z0-9._-]', '', sanitized) + + # Ensure it's not empty + if not sanitized: + sanitized = "AVD" + + return sanitized + + +def _configure_avd_hardware(avd_name: str) -> bool: + """ + Configure AVD hardware settings to ensure buttons work properly. + Sets hw.keyboard=yes in config.ini so hardware buttons are functional. + + Args: + avd_name: Name of the AVD + + Returns: + True if successful, False otherwise + """ + try: + # Find AVD config directory + # AVDs are stored in ~/.android/avd/{avd_name}.avd/ on Linux/macOS + # or %USERPROFILE%\.android\avd\{avd_name}.avd\ on Windows + system = platform.system() + + if system == "Windows": + avd_home = os.environ.get('ANDROID_AVD_HOME') + if not avd_home: + user_profile = os.environ.get('USERPROFILE', os.environ.get('HOME', '')) + avd_home = os.path.join(user_profile, '.android', 'avd') + avd_home = os.path.expandvars(avd_home) + else: + # Linux/macOS + avd_home = os.environ.get('ANDROID_AVD_HOME') + if not avd_home: + avd_home = os.path.join(os.path.expanduser('~'), '.android', 'avd') + + config_path = Path(avd_home) / f"{avd_name}.avd" / "config.ini" + + if not config_path.exists(): + logger.warning("[installer][emulator] AVD config file not found: %s", config_path) + return False + + # Read current config + with open(config_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # Modify or add hw.keyboard setting + # hw.keyboard=yes enables hardware input so buttons work + modified = False + new_lines = [] + hw_keyboard_found = False + + for line in lines: + stripped = line.strip() + if stripped.startswith('hw.keyboard='): + # Replace existing setting + new_lines.append('hw.keyboard=yes\n') + hw_keyboard_found = True + if 'no' in stripped.lower(): + modified = True + else: + new_lines.append(line) + + # Add setting if not found + if not hw_keyboard_found: + # Add after other hw.* settings if any, otherwise at the end + insert_pos = len(new_lines) + for i, line in enumerate(new_lines): + if line.strip().startswith('hw.') and i < len(new_lines) - 1: + # Check if next line doesn't start with hw. + if i + 1 < len(new_lines) and not new_lines[i + 1].strip().startswith('hw.'): + insert_pos = i + 1 + break + + new_lines.insert(insert_pos, 'hw.keyboard=yes\n') + modified = True + + # Write back if modified + if modified: + with open(config_path, 'w', encoding='utf-8') as f: + f.writelines(new_lines) + logger.info( + "[installer][emulator] Configured hardware settings (hw.keyboard=yes) for AVD '%s'", + avd_name, + ) + return True + else: + logger.info( + "[installer][emulator] Hardware settings already configured for AVD '%s'", + avd_name, + ) + return True + + except Exception as e: + logger.error( + "[installer][emulator] Failed to configure hardware settings for AVD '%s': %s", + avd_name, + e, + ) + import traceback + traceback.print_exc() + return False + + +def _host_accepts_emulator_abi(abi: str) -> bool: + """ + True if this sdkmanager system-image ABI can run on the current host. + Windows/Linux x86_64 hosts cannot use ARM-only images (e.g. arm64-v8a); ARM64 hosts can use + ARM images or x86 images (where the emulator supports it). + """ + if not abi: + return False + machine = platform.machine().lower() + intel_abi = abi in ("x86_64", "x86") + arm_abi = abi in ("arm64-v8a", "armeabi-v7a") + is_intel_host = machine in ("amd64", "x86_64", "i386", "i686", "x86") + is_arm_host = machine in ("aarch64", "arm64", "armv8l") + + if is_intel_host: + return intel_abi + if is_arm_host: + return arm_abi or intel_abi + # Unknown machine: prefer Intel ABIs (typical desktop/CI) + return intel_abi + + +def _get_highest_api_system_image(system_images: list[dict]) -> str | None: + """ + Get the system image with the highest API level among images whose ABI matches this host. + API level is the primary priority. Uses variant/arch as tiebreaker when API levels are equal. + + Args: + system_images: List of system image dictionaries with package, version, description + + Returns: + System image package name (e.g., "system-images;android-36;google_apis;x86_64") or None + """ + if not system_images: + return None + + # Extract API levels - API level is the priority + candidates = [] + for img in system_images: + package = img.get("package", "") + if not package.startswith("system-images;"): + continue + + # Parse: system-images;android-XX;variant;arch (arch is last segment) + parts = package.split(";") + if len(parts) < 2: + continue + + # Extract API level from android-XX + android_part = parts[1] + match = re.match(r'android-(\d+)', android_part) + if not match: + continue + + api_level = int(match.group(1)) + if api_level > 36: # Cap at API 36; API 37+ is not reliable still. + continue + variant = parts[2] if len(parts) > 2 else "" + arch = parts[-1] if len(parts) >= 4 else "" + + if not _host_accepts_emulator_abi(arch): + if debug: + logger.debug( + "[installer][emulator] Skipping system image (ABI not usable on this host): %s (host=%r)", + package, + platform.machine(), + ) + continue + + # Variant/arch preference for tiebreaking (only used when API levels are equal) + variant_priority = 0 + if variant == "google_apis" and arch == "x86_64": + variant_priority = 3 # Best variant/arch combo + elif variant == "google_apis_playstore" and arch == "x86_64": + variant_priority = 2 # Second best + elif variant == "google_apis": + variant_priority = 1 + elif variant == "google_apis_playstore": + variant_priority = 1 + else: + variant_priority = 0 + + candidates.append({ + "package": package, + "api_level": api_level, + "variant_priority": variant_priority + }) + + if not candidates: + return None + + # Sort by API level (descending - highest first), then by variant priority (tiebreaker) + candidates.sort(key=lambda x: (x["api_level"], x["variant_priority"]), reverse=True) + + # Return the highest API level (variant priority only matters if API levels are equal) + return candidates[0]["package"] + + +async def create_avd_from_system_image(device_param: str) -> bool: + """ + Create AVD from device ID and device name, using highest API level system image. + + Args: + device_param: Format "install device;device_id;device_name" + Example: "install device;desktop_large;Large Desktop" + + Returns: + bool: True if successful, False otherwise + """ + try: + # Parse device parameter: "install device;device_id;device_name" + parts = device_param.split(";") + if len(parts) < 3: + error_msg = f"Invalid device parameter format. Expected 'install device;device_id;device_name', got: {device_param}" + logger.error("[installer][emulator] %s", error_msg) + device_id = parts[1].strip() if len(parts) > 1 else "unknown" + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "Not Found", + "comment": error_msg, + } + }) + return False + + device_id = parts[1].strip() + device_name = parts[2].strip() + + # Sanitize device name for AVD (AVD names can only contain: a-z A-Z 0-9 . _ -) + avd_name = _sanitize_avd_name(device_name) + + logger.info( + "[installer][emulator] Creating AVD '%s' (from device name '%s') with device ID '%s'", + avd_name, + device_name, + device_id, + ) + + # Check if Android SDK is installed + sdk_root = _get_sdk_root() + if sdk_root is None: + logger.error("[installer][emulator] Android SDK not found. ANDROID_HOME or ANDROID_SDK_ROOT not set.") + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "not installed", + "comment": "Download and install Android SDK first", + } + }) + return False + + # Find required tools + sdkmanager = _find_sdkmanager(sdk_root) + avdmanager = _find_avdmanager(sdk_root) + + if not sdkmanager: + if debug: + logger.debug("[installer][emulator] sdkmanager not found") + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "Not Found", + "comment": "sdkmanager not found. Please check Android SDK installation.", + } + }) + return False + + if not avdmanager: + if debug: + logger.debug("[installer][emulator] avdmanager not found") + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "Not Found", + "comment": "avdmanager not found. Please check Android SDK installation.", + } + }) + return False + + # Step 0: Get available system images and select highest API level + logger.info("[installer][emulator] Getting available system images with Android Version 16") + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": "Finding system image with Android Version 16", + } + }) + + system_images = await get_available_system_images() + if not system_images: + error_msg = "No system images found. Please install Android SDK components first." + logger.error("[installer][emulator] %s", error_msg) + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + # Get highest API level system image (prefer google_apis;x86_64) + system_image_name = _get_highest_api_system_image(system_images) + if not system_image_name: + error_msg = "Could not find a suitable system image." + logger.error("[installer][emulator] %s", error_msg) + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + logger.info("[installer][emulator] Selected system image: %s", system_image_name) + logger.info( + "[installer][emulator] Creating AVD '%s' with device ID '%s' and system image '%s'", + device_name, + device_id, + system_image_name, + ) + + # Step 1: Install system image + logger.info("[installer][emulator] Installing system image: %s", system_image_name) + + loop = asyncio.get_event_loop() + if _is_windows(): + success, output = await loop.run_in_executor( + None, + _run_sdkmanager_install_windows, + sdkmanager, + sdk_root, + system_image_name, + loop, + device_id + ) + elif _is_linux(): + success, output = await loop.run_in_executor( + None, + _run_sdkmanager_install_linux, + sdkmanager, + sdk_root, + system_image_name, + loop, + device_id + ) + elif _is_darwin(): + success, output = await loop.run_in_executor( + None, + _run_sdkmanager_install_darwin, + sdkmanager, + sdk_root, + system_image_name, + loop, + device_id + ) + else: + # Fallback to Linux for unknown platforms + success, output = await loop.run_in_executor( + None, + _run_sdkmanager_install_linux, + sdkmanager, + sdk_root, + system_image_name, + loop, + device_id + ) + + if not success: + error_msg = f"Failed to install Android Version 16: {output}" + logger.error("[installer][emulator] %s", error_msg) + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "Not Found", + "comment": error_msg, + } + }) + return False + + logger.info("[installer][emulator] System image installed successfully") + + # Step 2: Create AVD with device_id and device_name + logger.info( + "[installer][emulator] Creating AVD: %s with device ID: %s", + avd_name, + device_id, + ) + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installing", + "comment": f"Creating AVD '{avd_name}'...", + } + }) + + if _is_windows(): + success, output = await loop.run_in_executor( + None, + _run_avdmanager_create_windows, + avdmanager, + sdk_root, + avd_name, + system_image_name, + device_id + ) + elif _is_linux(): + success, output = await loop.run_in_executor( + None, + _run_avdmanager_create_linux, + avdmanager, + sdk_root, + avd_name, + system_image_name, + device_id + ) + elif _is_darwin(): + success, output = await loop.run_in_executor( + None, + _run_avdmanager_create_darwin, + avdmanager, + sdk_root, + avd_name, + system_image_name, + device_id + ) + else: + # Fallback to Linux for unknown platforms + success, output = await loop.run_in_executor( + None, + _run_avdmanager_create_linux, + avdmanager, + sdk_root, + avd_name, + system_image_name, + device_id + ) + + if not success: + error_msg = f"Failed to create AVD: {output}" + logger.error("[installer][emulator] %s", error_msg) + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "not installed", + "comment": error_msg, + } + }) + return False + + logger.info("[installer][emulator] AVD '%s' created successfully", avd_name) + + # Configure hardware settings so buttons work properly + _configure_avd_hardware(avd_name) + + # Note: AVD list will be automatically refreshed when services_list is requested + # in long_poll_handler.py, so no manual refresh is needed here + + # Send success response + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "installed", + "comment": f"Installation of AVD '{avd_name}' completed", + } + }) + + return True + + except ValueError as e: + error_msg = f"Invalid device parameter: {e}" + logger.error("[installer][emulator] %s", error_msg) + device_name = device_param.split(";")[2].strip() if len(device_param.split(";")) > 2 else device_param + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "not installed", + "comment": error_msg, + } + }) + return False + except Exception as e: + error_msg = f"Error creating AVD: {e}" + logger.error("[installer][emulator] %s", error_msg) + import traceback + traceback.print_exc() + device_name = device_param.split(";")[2].strip() if len(device_param.split(";")) > 2 else device_param + await send_response({ + "action": "status", + "data": { + "category": "AndroidEmulator", + "package": device_id, + "status": "not installed", + "comment": error_msg, + } + }) + return False \ No newline at end of file diff --git a/Framework/install_handler/android/java.py b/Framework/install_handler/android/java.py index 616b2352..a81f7b7d 100644 --- a/Framework/install_handler/android/java.py +++ b/Framework/install_handler/android/java.py @@ -101,10 +101,10 @@ def update_java_path(): """Add Java binaries to PATH and set JAVA_HOME for the current process (following Node.js pattern).""" java_path = get_java_path() - logger.info("Updating java path for the current session") + print("Updating java path for the current session") # Check if java exists if not java_path.exists(): - logger.warning("Java not found for PATH update.") + print("Java not found for PATH update.") return # Get JDK home directory (parent of bin directory) @@ -118,14 +118,14 @@ def update_java_path(): # Set JAVA_HOME for the current process os.environ['JAVA_HOME'] = str(jdk_home) - logger.info("JAVA_HOME set for current process: %s", jdk_home) + print("JAVA_HOME set for current process: %s", jdk_home) # Add Java bin to PATH for the current process (always prepend so it takes precedence) java_bin_path = str(java_path.parent) current_path = os.environ.get('PATH', '') # Always prepend to ensure isolated Java takes precedence (even if path already exists) os.environ['PATH'] = f"{java_bin_path}{os.pathsep}{current_path}" - logger.info("Java prepended to current process PATH: %s", java_bin_path) + print("Java prepended to current process PATH: %s", java_bin_path) async def _get_jdk_download_url(): diff --git a/Framework/install_handler/install_log_config.py b/Framework/install_handler/install_log_config.py index 925dc3fa..62ed4fb5 100644 --- a/Framework/install_handler/install_log_config.py +++ b/Framework/install_handler/install_log_config.py @@ -16,6 +16,20 @@ ROTATING_BACKUP_COUNT = 3 +class ConditionalLevelFormatter(logging.Formatter): + """ + Show log level name only for WARNING+ so INFO stays message-only. + This keeps current installer log appearance for success/progress lines, + while making warnings/errors/exception tracebacks stand out. + """ + + def format(self, record: logging.LogRecord) -> str: + msg = record.getMessage() + if record.levelno >= logging.WARNING: + return f"{record.levelname}: {msg}" + return msg + + def get_installer_log_dir() -> Path: """Return the directory for installer log files. Uses default if not yet set.""" if INSTALLER_LOG_DIR is not None: @@ -46,7 +60,8 @@ def setup_installer_logging(log_dir: Path | None = None) -> None: logger = logging.getLogger(INSTALLER_LOGGER_NAME) logger.setLevel(logging.DEBUG) - formatter = logging.Formatter(LOG_FORMAT, style="{", datefmt=LOG_DATE_FMT) + # Keep INFO message-only, but add level for WARNING+ (warnings/errors/exception). + formatter = ConditionalLevelFormatter(LOG_FORMAT, style="{", datefmt=LOG_DATE_FMT) # Avoid duplicate handlers: remove existing ones from this logger for h in list(logger.handlers): diff --git a/Framework/install_handler/long_poll_handler.py b/Framework/install_handler/long_poll_handler.py index 85c395dd..5b4d5d71 100644 --- a/Framework/install_handler/long_poll_handler.py +++ b/Framework/install_handler/long_poll_handler.py @@ -17,7 +17,7 @@ ) from Framework.Utilities import RequestFormatter, ConfigModule from Framework.node_server_state import STATE -from Framework.install_handler.android.emulator import ( +from Framework.install_handler.android.android_emulator import ( check_emulator_list, create_avd_from_system_image, get_filtered_avd_services, diff --git a/Framework/install_handler/route.py b/Framework/install_handler/route.py index 802cde65..f9e66d9c 100644 --- a/Framework/install_handler/route.py +++ b/Framework/install_handler/route.py @@ -12,7 +12,7 @@ from .ios import xcode, simulator from .macos import xcode as macos_xcode from .windows import inspector -from .android import emulator +from .android import android_emulator, emulator_windows_linux, emulator services = [ { @@ -89,8 +89,8 @@ "check_text":"Check status", "install_text": "Install", "os": ["windows", "linux", "darwin"], - "status_function": emulator.check_emulator_list, - "install_function": emulator.android_emulator_install, + "status_function": android_emulator.check_emulator_list, + "install_function": android_emulator.android_emulator_install, "installables": [], "services": [], }, diff --git a/server/installers.py b/server/installers.py index a7ed2d45..bc421bc3 100644 --- a/server/installers.py +++ b/server/installers.py @@ -18,7 +18,7 @@ from Framework.install_handler import install_log_config from Framework.install_handler import utils as install_utils from Framework.install_handler.route import services as INSTALLER_SERVICES -from Framework.install_handler.android.emulator import ( +from Framework.install_handler.android.android_emulator import ( android_emulator_install, check_emulator_list, create_avd_from_system_image, @@ -276,6 +276,7 @@ def _patch_send_response_targets() -> None: # Explicitly add emulator module modules.add("Framework.install_handler.android.emulator") + modules.add("Framework.install_handler.android.emulator_windows_linux") # Patch modules that use send_response for mod_name in modules: