| 1 | from helpers.extension import Extension |
| 2 | import os |
| 3 | import psutil |
| 4 | |
| 5 | |
| 6 | class SystemResourcesCheck(Extension): |
| 7 | async def execute(self, banners: list = [], frontend_context: dict = {}, **kwargs): |
| 8 | try: |
| 9 | cpu_percent = psutil.cpu_percent(interval=0.1) |
| 10 | except Exception: |
| 11 | cpu_percent = None |
| 12 | |
| 13 | try: |
| 14 | cpu_cores = psutil.cpu_count(logical=True) |
| 15 | except Exception: |
| 16 | cpu_cores = None |
| 17 | |
| 18 | load_avg = self._get_load_average() |
| 19 | |
| 20 | try: |
| 21 | vm = psutil.virtual_memory() |
| 22 | ram_percent = vm.percent |
| 23 | ram_used_gb = (vm.total - vm.available) / (1024 ** 3) |
| 24 | ram_total_gb = vm.total / (1024 ** 3) |
| 25 | except Exception: |
| 26 | ram_percent = None |
| 27 | |
| 28 | ram_used_gb = None |
| 29 | ram_total_gb = None |
| 30 | |
| 31 | disk_percent, disk_used_gb, disk_total_gb, disk_path = self._get_disk_usage() |
| 32 | |
| 33 | try: |
| 34 | net = psutil.net_io_counters() |
| 35 | net_sent = self._format_bytes(net.bytes_sent) |
| 36 | net_recv = self._format_bytes(net.bytes_recv) |
| 37 | except Exception: |
| 38 | net_sent = "N/A" |
| 39 | net_recv = "N/A" |
| 40 | |
| 41 | load_value = "N/A" |
| 42 | if load_avg: |
| 43 | la1, la5, la15 = load_avg |
| 44 | load_value = f"{la1:.2f} / {la5:.2f} / {la15:.2f}" |
| 45 | |
| 46 | if disk_percent is None or disk_used_gb is None or disk_total_gb is None: |
| 47 | disk_value = "N/A" |
| 48 | else: |
| 49 | disk_value = f"{disk_used_gb:.2f}/{disk_total_gb:.2f} GB" |
| 50 | |
| 51 | if cpu_percent is None: |
| 52 | cpu_value = "N/A" |
| 53 | else: |
| 54 | cores_value = "" if cpu_cores is None else f" ({cpu_cores} cores)" |
| 55 | cpu_value = f"{cpu_percent:.0f}%{cores_value}" |
| 56 | |
| 57 | if ram_percent is None or ram_used_gb is None or ram_total_gb is None: |
| 58 | ram_value = "N/A" |
| 59 | else: |
| 60 | ram_value = f"{ram_used_gb:.2f}/{ram_total_gb:.2f} GB" |
| 61 | |
| 62 | cpu_bar = self._bar_html(cpu_percent) |
| 63 | ram_bar = self._bar_html(ram_percent) |
| 64 | disk_bar = self._bar_html(disk_percent) |
| 65 | |
| 66 | banners.append({ |
| 67 | "id": "system-resources", |
| 68 | "type": "info", |
| 69 | "priority": 10, |
| 70 | "title": "System Resources", |
| 71 | "html": ( |
| 72 | "<div class=\"system-resource-body\">" |
| 73 | "<div class=\"system-resource-meter\">" |
| 74 | "<div class=\"system-resource-label\">" |
| 75 | "<div class=\"system-resource-name\">CPU</div>" |
| 76 | f"<div class=\"system-resource-value\">{cpu_value}</div>" |
| 77 | "</div>" |
| 78 | f"{cpu_bar}" |
| 79 | "</div>" |
| 80 | "<div class=\"system-resource-meter\">" |
| 81 | "<div class=\"system-resource-label\">" |
| 82 | "<div class=\"system-resource-name\">RAM</div>" |
| 83 | f"<div class=\"system-resource-value\">{ram_value}</div>" |
| 84 | "</div>" |
| 85 | f"{ram_bar}" |
| 86 | "</div>" |
| 87 | "<div class=\"system-resource-meter\">" |
| 88 | "<div class=\"system-resource-label\">" |
| 89 | "<div class=\"system-resource-name\">Disk</div>" |
| 90 | f"<div class=\"system-resource-value\">{disk_value}</div>" |
| 91 | "</div>" |
| 92 | f"{disk_bar}" |
| 93 | "</div>" |
| 94 | "<div class=\"system-resource-footer\">" |
| 95 | "<div class=\"system-resource-detail\">" |
| 96 | "<div class=\"system-resource-detail-title\">Load (1/5/15)</div>" |
| 97 | f"<div class=\"system-resource-detail-value\">{load_value}</div>" |
| 98 | "</div>" |
| 99 | "<div class=\"system-resource-detail\">" |
| 100 | "<div class=\"system-resource-detail-title\">Net (since boot)</div>" |
| 101 | f"<div class=\"system-resource-detail-value\">{net_sent} sent / {net_recv} recv</div>" |
| 102 | "</div>" |
| 103 | "</div>" |
| 104 | "</div>" |
| 105 | ), |
| 106 | "dismissible": True, |
| 107 | "source": "backend", |
| 108 | }) |
| 109 | |
| 110 | def _bar_html(self, percent: float | None) -> str: |
| 111 | if percent is None: |
| 112 | return "" |
| 113 | |
| 114 | p = max(0.0, min(100.0, float(percent))) |
| 115 | if p >= 85: |
| 116 | color = "#ef4444" |
| 117 | elif p >= 70: |
| 118 | color = "#f59e0b" |
| 119 | else: |
| 120 | color = "#22c55e" |
| 121 | |
| 122 | return ( |
| 123 | "<div class=\"system-resource-track\">" |
| 124 | f"<span class=\"system-resource-fill\" style=\"--resource-value:{p:.0f}%;--resource-color:{color};\"></span>" |
| 125 | "</div>" |
| 126 | ) |
| 127 | |
| 128 | def _get_load_average(self) -> tuple[float, float, float] | None: |
| 129 | try: |
| 130 | return os.getloadavg() |
| 131 | except Exception: |
| 132 | return None |
| 133 | |
| 134 | def _get_disk_usage(self) -> tuple[float | None, float | None, float | None, str]: |
| 135 | for path in ["/", os.path.expanduser("~")]: |
| 136 | try: |
| 137 | usage = psutil.disk_usage(path) |
| 138 | used_gb = usage.used / (1024 ** 3) |
| 139 | total_gb = usage.total / (1024 ** 3) |
| 140 | return usage.percent, used_gb, total_gb, path |
| 141 | except Exception: |
| 142 | continue |
| 143 | return None, None, None, "/" |
| 144 | |
| 145 | def _format_bytes(self, value: int) -> str: |
| 146 | size = float(value) |
| 147 | for unit in ["B", "KB", "MB", "GB", "TB", "PB"]: |
| 148 | if size < 1024: |
| 149 | return f"{size:.1f} {unit}" |
| 150 | size /= 1024 |
| 151 | return f"{size:.1f} EB" |