#!/usr/bin/env python3
"""
Memory Sentinel Script

This script monitors the system's RAM usage and kills the process with the highest
memory consumption if available RAM drops below 5GB.

Usage:
    python memory_sentinel.py

The script runs continuously, checking memory usage every second.

Script was generated by Claude 3.7 with the following prompt:

I'm working on a server and I have one script that sometimes consumes so much memory that the entire server becomes unresponsive. This is a hue problem because then I cannot log in to it anymore.

Could you write a sentinel script in python that does the following:

Check the total available RAM of the system (disregarding swap)
Check the currently used RAM of the system
If we have more than 5G left, do nothing
Else, find the process with the highest RAM consumption and kill it. Note: Use the SIGKILL command
Check every second
"""

import logging
import os
import signal
import time

import psutil

# Set up logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", filename="memory_sentinel.log", filemode="a"
)

# Memory threshold in GB
MEMORY_THRESHOLD_GB = 5
# Convert to bytes for comparison
MEMORY_THRESHOLD_BYTES = MEMORY_THRESHOLD_GB * 1024 * 1024 * 1024


def get_available_ram():
    """Get the available RAM in bytes, excluding swap."""
    return psutil.virtual_memory().available


def get_total_ram():
    """Get the total RAM in bytes, excluding swap."""
    return psutil.virtual_memory().total


def get_used_ram():
    """Get the used RAM in bytes."""
    return psutil.virtual_memory().used


def get_process_with_highest_memory():
    """Find the process with the highest memory consumption."""
    processes = []
    for proc in psutil.process_iter(["pid", "name", "memory_info"]):
        try:
            processes.append((proc.info["pid"], proc.info["name"], proc.info["memory_info"].rss))
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass

    # Sort by memory usage (descending)
    processes.sort(key=lambda x: x[2], reverse=True)

    if processes:
        return processes[0]
    return None


def kill_process(pid):
    """Kill a process using SIGKILL."""
    try:
        os.kill(pid, signal.SIGKILL)
        return True
    except OSError as e:
        logging.error(f"Failed to kill process {pid}: {e}")
        return False


def format_bytes(bytes_value):
    """Format bytes to a human-readable string."""
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if bytes_value < 1024.0:
            return f"{bytes_value:.2f} {unit}"
        bytes_value /= 1024.0
    return f"{bytes_value:.2f} PB"


def main():
    """Main function that runs the memory monitoring loop."""
    logging.info("Memory Sentinel started")
    logging.info(f"Memory threshold set to {MEMORY_THRESHOLD_GB}GB")

    try:
        while True:
            available_ram = get_available_ram()
            total_ram = get_total_ram()
            used_ram = get_used_ram()

            logging.debug(
                f"Total RAM: {format_bytes(total_ram)}, "
                + f"Used RAM: {format_bytes(used_ram)}, "
                + f"Available RAM: {format_bytes(available_ram)}"
            )

            if available_ram < MEMORY_THRESHOLD_BYTES:
                logging.warning(
                    f"Available RAM ({format_bytes(available_ram)}) " + f"below threshold of {MEMORY_THRESHOLD_GB}GB"
                )

                process = get_process_with_highest_memory()
                if process:
                    pid, name, memory = process
                    logging.warning(f"Killing process {pid} ({name}) " + f"using {format_bytes(memory)}")

                    if kill_process(pid):
                        logging.info(f"Successfully killed process {pid} ({name})")
                    else:
                        logging.error(f"Failed to kill process {pid} ({name})")
                else:
                    logging.warning("No process found to kill")

            # Sleep for 1 second
            time.sleep(1)

    except KeyboardInterrupt:
        logging.info("Memory Sentinel stopped by user")
    except Exception as e:
        logging.error(f"Unexpected error: {e}")
        raise


if __name__ == "__main__":
    main()
