From 2f441623d3c58d9c2a670ffb426de15963908697 Mon Sep 17 00:00:00 2001 From: Douglas Schilling Landgraf Date: Fri, 31 Jan 2025 22:16:30 -0500 Subject: [PATCH] common: general improvements - if timeout happens we try 5 times before sending Timeout error to users. - improve user experience when timeout occurs - Added console source tree for handling messages Resolves: https://github.com/containers/ramalama/issues/693 Signed-off-by: Douglas Schilling Landgraf --- ramalama/common.py | 74 ++++++++++++++++++++++++++++++++++++--------- ramalama/console.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 ramalama/console.py diff --git a/ramalama/common.py b/ramalama/common.py index 7f4f2e4e..fdfa1aad 100644 --- a/ramalama/common.py +++ b/ramalama/common.py @@ -1,19 +1,29 @@ """ramalama common module.""" - import glob import hashlib import os import random +import logging import shutil import string import subprocess +import time import sys import urllib.request import urllib.error +import ramalama.console as console + from ramalama.http_client import HttpClient + +logging.basicConfig( + level=logging.WARNING, + format="%(asctime)s - %(levelname)s - %(message)s" +) + MNT_DIR = "/mnt/models" MNT_FILE = f"{MNT_DIR}/model.file" +HTTP_RANGE_NOT_SATISFIABLE = 416 def container_manager(): @@ -165,25 +175,61 @@ def download_file(url, dest_path, headers=None, show_progress=True): headers (dict): Optional headers to include in the request. show_progress (bool): Whether to show a progress bar during download. - Returns: - None + Raises: + RuntimeError: If the download fails after multiple attempts. """ - http_client = HttpClient() - headers = headers or {} - # if we are not a tty, don't show progress, can pollute CI output and such + # If not running in a TTY, disable progress to prevent CI pollution if not sys.stdout.isatty(): show_progress = False - try: - http_client.init(url=url, headers=headers, output_file=dest_path, progress=show_progress) - except urllib.error.HTTPError as e: - if e.code == 416: # Range not satisfiable - if show_progress: - print(f"File {url} already fully downloaded.") - else: - raise e + http_client = HttpClient() + max_retries = 5 # Stop after 5 failures + retries = 0 + + while retries < max_retries: + try: + # Initialize HTTP client for the request + http_client.init(url=url, headers=headers, output_file=dest_path, progress=show_progress) + return # Exit function if successful + + except urllib.error.HTTPError as e: + if e.code == HTTP_RANGE_NOT_SATISFIABLE: # "Range Not Satisfiable" error (file already downloaded) + return # No need to retry + + except urllib.error.URLError as e: + console.error(f"Network Error: {e.reason}") + retries += 1 + + except TimeoutError: + retries += 1 + console.warning(f"TimeoutError: The server took too long to respond. Retrying {retries}/{max_retries}...") + + except RuntimeError as e: # Catch network-related errors from HttpClient + retries += 1 + console.warning(f"{e}. Retrying {retries}/{max_retries}...") + + except IOError as e: + retries += 1 + console.warning(f"I/O Error: {e}. Retrying {retries}/{max_retries}...") + + except Exception as e: + console.error(f"Unexpected error: {str(e)}") + raise + + if retries >= max_retries: + error_message = ( + "\nDownload failed after multiple attempts.\n" + "Possible causes:\n" + "- Internet connection issue\n" + "- Server is down or unresponsive\n" + "- Firewall or proxy blocking the request\n" + ) + console.error(error_message) + sys.exit(1) + + time.sleep(2 ** retries * 0.1) # Exponential backoff (0.1s, 0.2s, 0.4s...) def engine_version(engine): diff --git a/ramalama/console.py b/ramalama/console.py new file mode 100644 index 00000000..533231ef --- /dev/null +++ b/ramalama/console.py @@ -0,0 +1,53 @@ +""" +MIT License + +(C) 2024-2025 ramalama developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +""" +import os +import sys +import locale +import logging + +def is_locale_utf8(): + """Check if the system locale is UTF-8.""" + lang = os.getenv("LC_CTYPE", "") or os.getenv("LANG", "") + return "UTF-8" in lang.upper() or "utf8" in lang.lower() + +def supports_emoji(): + """Detect if the terminal supports emoji output.""" + if not is_locale_utf8(): + return False # Block emoji if UTF-8 is not in locale + + if sys.platform == "win32": # works 32, 64 bits machines + # Windows Terminal, ConEmu, VSCode, and WSL support emoji + if os.getenv("WT_SESSION") or os.getenv("ConEmuANSI") or os.getenv("TERM_PROGRAM") == "vscode": + return True + return False # Legacy cmd.exe and PowerShell lack proper emoji support + + return True # Most Linux/macOS terminals support emoji + +# Allow users to override emoji support via an environment variable +EMOJI = os.getenv("RAMALAMA_FORCE_EMOJI", "1") != "0" and supports_emoji() + +# Define emoji-aware logging messages +def error(msg): + formatted_msg = f"❌ {msg}" if EMOJI else f"[ERROR] {msg}" + logging.error(formatted_msg) + +def warning(msg): + formatted_msg = f"⚠️ {msg}" if EMOJI else f"[WARNING] {msg}" + logging.warning(formatted_msg) + +def info(msg): + formatted_msg = f"ℹ️ {msg}" if EMOJI else f"[INFO] {msg}" + logging.info(formatted_msg)