From fce28b6f0f77a5a97de6366b10ec6ac2df7ba3f2 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 | 35 +++++++++++++++++++++ 2 files changed, 95 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..f7980c7a --- /dev/null +++ b/ramalama/console.py @@ -0,0 +1,35 @@ +import os +import sys +import locale +import logging + +def is_locale_utf8(): + """Check if the system locale is UTF-8.""" + return 'UTF-8' in os.getenv('LC_CTYPE', '') or 'UTF-8' in os.getenv('LANG', '') + +def supports_emoji(): + """Detect if the terminal supports emoji output.""" + term = os.getenv("TERM") + if not term or term in ("dumb", "linux"): + return False + + return is_locale_utf8() + +# Allow users to override emoji support via an environment variable +# If RAMALAMA_FORCE_EMOJI is not set, it defaults to checking supports_emoji() +RAMALAMA_FORCE_EMOJI = os.getenv("RAMALAMA_FORCE_EMOJI") +FORCE_EMOJI = RAMALAMA_FORCE_EMOJI.lower() == "true" if RAMALAMA_FORCE_EMOJI else None +EMOJI = FORCE_EMOJI if FORCE_EMOJI is not None else 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)