From f02b38feedbbf466fca692c969d42e412c1bbd32 Mon Sep 17 00:00:00 2001
From: Anshuman Singh <148977651+DataBoySu@users.noreply.github.com>
Date: Sat, 17 Jan 2026 23:17:25 +0530
Subject: [PATCH 1/3] cross platform
---
.github/workflows/translate.yml | 29 ++++-
README.md | 33 ++++--
health_monitor.py | 149 ++++++++++++-----------
monitor/alerting/toaster.py | 119 +++++++++++--------
monitor/api/server.py | 19 ++-
monitor/collectors/gpu.py | 15 ++-
scripts/hi.txt | 11 +-
setup.sh | 204 ++++++++++++++++++++++++++++++++
8 files changed, 421 insertions(+), 158 deletions(-)
create mode 100644 setup.sh
diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml
index 8d875c8..8df7f6e 100644
--- a/.github/workflows/translate.yml
+++ b/.github/workflows/translate.yml
@@ -14,6 +14,10 @@ on:
permissions:
contents: write
+concurrency:
+ group: translate-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
prepare-matrix:
name: Prepare Translation Matrix
@@ -86,6 +90,8 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Download All Translations
uses: actions/download-artifact@v4
@@ -95,17 +101,28 @@ jobs:
merge-multiple: true
- name: Commit and Push
+ env:
+ BRANCH: ${{ github.ref_name }}
run: |
git config --global user.name "DataBoySu's Readme Translator"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ # Ensure we're on the correct branch and have full history
+ git fetch origin "$BRANCH"
+ git checkout "$BRANCH"
+
git add locales/*.md
git commit -m "docs: update translations" || echo "No changes to commit"
-
- # Retry logic for concurrent pushes
- for i in {1..5}; do
- git pull --rebase
- if git push; then exit 0; fi
- echo "Push failed, retrying in 5s..."
+
+ # Retry logic for pushes that fail due to remote updates
+ for i in 1 2 3 4 5; do
+ git pull --rebase origin "$BRANCH" || true
+ if git push origin "$BRANCH"; then
+ echo "Push succeeded"
+ exit 0
+ fi
+ echo "Push failed (attempt $i), retrying in 5s..."
sleep 5
done
+ echo "Push failed after retries"
exit 1
diff --git a/README.md b/README.md
index 2953d93..7e34751 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,8 @@


-
-
+
+

## Gallery
@@ -109,8 +109,8 @@ Contributions are welcome! Main future points to cover would be:
- **Containerization**: Official Docker support for easy deployment in containerized environments.
- **Remote Access**: SSH tunneling integration and secure remote management.
- **Cross-Platform**:
- - [ ] Linux Support (Ubuntu/Debian focus).
- - [ ] macOS Support (Apple Silicon monitoring).
+ - [x] Linux Support (Ubuntu/Debian focus).
+ - [x] macOS Support (Apple Silicon monitoring).
- **Hardware Agnostic**:
- [ ] AMD ROCm support.
- [ ] Intel Arc support.
@@ -122,11 +122,11 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get involved.
## Requirements
-- **OS**: Windows 10/11
+- **OS**: Windows 10/11, Linux, macOS
- **Python**: 3.10+
-- **Hardware**: NVIDIA GPU with installed drivers.
-- **CUDA**: Toolkit 12.x (Strictly required for Benchmarking/Simulation features).
- - *Note: If CUDA 12.x is not detected, GPU-specific benchmarking features will be disabled.*
+- **Hardware**: NVIDIA GPU (all platforms), Apple Silicon (macOS), or CPU-only.
+- **CUDA**: Toolkit 12.x (Recommended for Benchmarking/Simulation on NVIDIA).
+ - *Note: If CUDA/MPS is not detected, some benchmarking features may be disabled.*
---
@@ -159,16 +159,23 @@ Best for development and stress testing.
### Quick Start
-1. **Download** the latest release or clone the repo.
+1. **Download** or clone the repository.
2. **Run Setup**:
- ```powershell
- .\setup.ps1
- ```
+ **Windows**:
+ ```powershell
+ .\setup.ps1
+ ```
+
+ **Linux/macOS**:
+ ```bash
+ chmod +x setup.sh
+ ./setup.sh
+ ```
3. **Launch**:
-```powershell
+```bash
# Start the web dashboard (Standard/Full)
python health_monitor.py web
diff --git a/health_monitor.py b/health_monitor.py
index 502e0d4..0378c74 100644
--- a/health_monitor.py
+++ b/health_monitor.py
@@ -388,56 +388,60 @@ def _format_gpu_grid(gpus, total_width: int = None):
def _run_app(config_path, port, nodes, once, web_mode=False, cli_mode=False):
"""Helper to run main application logic."""
# If the user requested admin mode via --admin in argv, and the process is not elevated,
- # attempt to relaunch elevated (Windows UAC). This project targets Windows only,
+ # attempt to relaunch elevated.
try:
import sys
import platform
import os
+ import subprocess
+
def _is_elevated():
- try:
- import ctypes
- return bool(ctypes.windll.shell32.IsUserAnAdmin())
- except Exception:
- return False
+ if platform.system() == 'Windows':
+ try:
+ import ctypes
+ return bool(ctypes.windll.shell32.IsUserAnAdmin())
+ except Exception:
+ return False
+ else:
+ return os.getuid() == 0
if '--admin' in (sys.argv[1:] if len(sys.argv) > 1 else []) and not _is_elevated():
- # Attempt relaunch elevated on Windows using ShellExecuteW or PowerShell Start-Process
- try:
- import ctypes
- import subprocess
- params = '"' + os.path.abspath(sys.argv[0]) + '"'
- other_args = [a for a in sys.argv[1:]]
- if other_args:
- params += ' ' + ' '.join(str(a) for a in other_args)
-
+ if platform.system() == 'Windows':
+ # Attempt relaunch elevated on Windows
try:
+ import ctypes
+ params = '"' + os.path.abspath(sys.argv[0]) + '"'
+ other_args = [a for a in sys.argv[1:]]
+ if other_args:
+ params += ' ' + ' '.join(str(a) for a in other_args)
+
ret = ctypes.windll.shell32.ShellExecuteW(None, 'runas', sys.executable, params, None, 1)
- try:
- ok = int(ret) > 32
- except Exception:
- ok = False
- if ok:
- print('Relaunching elevated, exiting original process')
- try: os._exit(0)
- except Exception: pass
+ if int(ret) > 32:
+ os._exit(0)
except Exception:
- def _ps_quote(s):
- return "'" + str(s).replace("'", "''") + "'"
- ps_args = [os.path.abspath(sys.argv[0])] + list(sys.argv[1:])
- arglist_literal = ','.join(_ps_quote(a) for a in ps_args)
- ps_cmd = [
- 'powershell', '-NoProfile', '-NonInteractive', '-Command',
- f"Start-Process -FilePath '{sys.executable}' -ArgumentList {arglist_literal} -Verb RunAs"
- ]
+ # Fallback to powershell if ShellExecuteW fails
try:
- proc = subprocess.run(ps_cmd, capture_output=True, text=True, timeout=15)
- if proc.returncode == 0:
- try: os._exit(0)
- except Exception: pass
+ def _ps_quote(s):
+ return "'" + str(s).replace("'", "''") + "'"
+ ps_args = [os.path.abspath(sys.argv[0])] + list(sys.argv[1:])
+ arglist_literal = ','.join(_ps_quote(a) for a in ps_args)
+ ps_cmd = [
+ 'powershell', '-NoProfile', '-NonInteractive', '-Command',
+ f"Start-Process -FilePath '{sys.executable}' -ArgumentList {arglist_literal} -Verb RunAs"
+ ]
+ subprocess.run(ps_cmd, capture_output=True, text=True, timeout=15)
+ os._exit(0)
except Exception:
pass
- except Exception:
- pass
+ else:
+ # POSIX relaunch with sudo
+ try:
+ args = ['sudo', sys.executable, os.path.abspath(sys.argv[0])] + sys.argv[1:]
+ # Ensure we don't end up in an infinite loop if sudo fails or doesn't grant root
+ if 'SUDO_COMMAND' not in os.environ:
+ os.execvp('sudo', args)
+ except Exception:
+ pass
except Exception:
pass
@@ -484,42 +488,43 @@ async def main():
@click.pass_context
def cli(ctx, config, port, update, admin):
"""MyGPU: Real-time GPU and system health monitoring."""
- # If the user requested admin mode, attempt to relaunch this process elevated
- # on platforms that support elevation (Windows -> UAC, POSIX -> sudo).
+ # If admin requested and not already elevated, attempt to relaunch elevated
def _is_elevated():
- try:
- import ctypes
- return bool(ctypes.windll.shell32.IsUserAnAdmin())
- except Exception:
- return False
+ import platform
+ import os
+ if platform.system() == 'Windows':
+ try:
+ import ctypes
+ return bool(ctypes.windll.shell32.IsUserAnAdmin())
+ except Exception:
+ return False
+ else:
+ try:
+ return os.getuid() == 0
+ except Exception:
+ return False
def _relaunch_elevated():
- try:
- import sys
- import os
- import subprocess
- script = os.path.abspath(sys.argv[0])
- args = sys.argv[1:]
- if '--admin' not in args:
- args = args + ['--admin']
+ import sys
+ import os
+ import platform
+ import subprocess
+ script = os.path.abspath(sys.argv[0])
+ args = sys.argv[1:]
+ if '--admin' not in args:
+ args = args + ['--admin']
+ if platform.system() == 'Windows':
try:
import ctypes
params = '"' + script + '"'
if args:
params += ' ' + ' '.join(str(a) for a in args)
ret = ctypes.windll.shell32.ShellExecuteW(None, 'runas', sys.executable, params, None, 1)
- try:
- ok = int(ret) > 32
- except Exception:
- ok = False
- if ok:
- print('Relaunching elevated, exiting original process')
- try: os._exit(0)
- except SystemExit: raise
- except Exception: pass
- except Exception as e:
- # PowerShell fallback if ShellExecuteW is not possible
+ if int(ret) > 32:
+ os._exit(0)
+ except Exception:
+ # PowerShell fallback
try:
def _ps_quote(s):
return "'" + str(s).replace("'", "''") + "'"
@@ -529,17 +534,19 @@ def _ps_quote(s):
'powershell', '-NoProfile', '-NonInteractive', '-Command',
f"Start-Process -FilePath '{sys.executable}' -ArgumentList {arglist_literal} -Verb RunAs"
]
- proc = subprocess.run(ps_cmd, capture_output=True, text=True, timeout=15)
- if proc.returncode == 0:
- try: os._exit(0)
- except Exception: pass
+ subprocess.run(ps_cmd, capture_output=True, text=True, timeout=15)
+ os._exit(0)
except Exception:
pass
+ else:
+ # POSIX relaunch with sudo
+ try:
+ sudo_args = ['sudo', sys.executable, script] + args
+ if 'SUDO_COMMAND' not in os.environ:
+ os.execvp('sudo', sudo_args)
+ except Exception as e:
+ print(f'Relaunch elevation error: {e}')
- except Exception as e:
- print('Relaunch elevation error:', e)
-
- # If admin requested and not already elevated, attempt to relaunch elevated
try:
if admin and not _is_elevated():
_relaunch_elevated()
diff --git a/monitor/alerting/toaster.py b/monitor/alerting/toaster.py
index e2ce4df..1e0d685 100644
--- a/monitor/alerting/toaster.py
+++ b/monitor/alerting/toaster.py
@@ -1,33 +1,28 @@
import warnings
import threading
+import platform
+import subprocess
+import os
_ToastNotifierClass = None
-with warnings.catch_warnings():
- # suppress the known pkg_resources deprecation warning emitted by win10toast
- warnings.simplefilter('ignore')
- try:
- from win10toast import ToastNotifier as _ToastNotifierClass
- except Exception:
- _ToastNotifierClass = None
-
-# Prefer winrt notifications when available (more robust). We'll detect at import time.
_has_winrt = False
-try:
- from winrt.windows.ui.notifications import ToastNotificationManager, ToastNotification
- from winrt.windows.data.xml.dom import XmlDocument
- _has_winrt = True
-except Exception:
- _has_winrt = False
+# Only attempt Windows-specific imports on Windows
+if platform.system() == 'Windows':
+ with warnings.catch_warnings():
+ # suppress the known pkg_resources deprecation warning emitted by win10toast
+ warnings.simplefilter('ignore')
+ try:
+ from win10toast import ToastNotifier as _ToastNotifierClass
+ except Exception:
+ _ToastNotifierClass = None
-def _safe_show_toast(notifier, title, msg, duration):
try:
- # call the blocking API (threaded=False) inside our own thread to
- # avoid the library's internal WNDPROC callback lifecycle issues.
- notifier.show_toast(title, msg, duration=duration, threaded=False)
+ from winrt.windows.ui.notifications import ToastNotificationManager, ToastNotification
+ from winrt.windows.data.xml.dom import XmlDocument
+ _has_winrt = True
except Exception:
- # swallow errors from the underlying notification library
- pass
+ _has_winrt = False
def _safe_show_toast_win10(title, msg, duration):
@@ -40,41 +35,63 @@ def _safe_show_toast_win10(title, msg, duration):
pass
-def send_toast(title: str, msg: str, duration: int = 5, severity: str = 'info'):
- """Send a Windows toast if possible; otherwise no-op.
+def _show_linux_notification(title, msg, duration):
+ """Fallback for Linux using notify-send."""
+ try:
+ # duration is in milliseconds for notify-send
+ subprocess.run(['notify-send', '-t', str(duration * 1000), title, msg], check=False)
+ return True
+ except Exception:
+ return False
- `severity` may be 'info', 'warning', or 'critical'. The UI uses the
- title/message provided and avoids emoji prefixes per user's request.
- """
+
+def _show_macos_notification(title, msg):
+ """Fallback for macOS using AppleScript."""
try:
- display_title = title
- if severity and severity.lower() in ('critical', 'error'):
- display_title = str(title)
- elif severity and severity.lower() == 'warning':
- display_title = str(title)
+ script = f'display notification "{msg}" with title "{title}"'
+ subprocess.run(['osascript', '-e', script], check=False)
+ return True
+ except Exception:
+ return False
- # If winrt is available, use native Windows 10+ notifications (synchronous API)
- if _has_winrt:
- try:
- def _xml_escape(s):
- return (s.replace('&', '&').replace('<', '<').replace('>', '>'))
- xml = f"{_xml_escape(display_title)}{_xml_escape(msg)}"
- doc = XmlDocument()
- doc.load_xml(xml)
- notif = ToastNotification(doc)
- notifier = ToastNotificationManager.create_toast_notifier()
- notifier.show(notif)
- return True
- except Exception:
- pass
- if _ToastNotifierClass is not None:
- # instantiate and run the notifier inside a background thread to avoid WNDPROC lifecycle issues
- try:
- threading.Thread(target=_safe_show_toast_win10, args=(display_title, msg, duration), daemon=True).start()
+def send_toast(title: str, msg: str, duration: int = 5, severity: str = 'info'):
+ """Send a system notification if possible.
+
+ Supports Windows (WinRT/win10toast), Linux (notify-send), and macOS (osascript).
+ """
+ system = platform.system()
+
+ try:
+ if system == 'Windows':
+ # If winrt is available, use native Windows 10+ notifications
+ if _has_winrt:
+ try:
+ def _xml_escape(s):
+ return (s.replace('&', '&').replace('<', '<').replace('>', '>'))
+ xml = f"{_xml_escape(title)}{_xml_escape(msg)}"
+ from winrt.windows.data.xml.dom import XmlDocument
+ from winrt.windows.ui.notifications import ToastNotificationManager, ToastNotification
+ doc = XmlDocument()
+ doc.load_xml(xml)
+ notif = ToastNotification(doc)
+ notifier = ToastNotificationManager.create_toast_notifier()
+ notifier.show(notif)
+ return True
+ except Exception:
+ pass
+
+ if _ToastNotifierClass is not None:
+ threading.Thread(target=_safe_show_toast_win10, args=(title, msg, duration), daemon=True).start()
return True
- except Exception:
- return False
+
+ elif system == 'Linux':
+ return _show_linux_notification(title, msg, duration)
+
+ elif system == 'Darwin': # macOS
+ return _show_macos_notification(title, msg)
+
except Exception:
pass
+
return False
diff --git a/monitor/api/server.py b/monitor/api/server.py
index 9116e25..f7b1ec9 100644
--- a/monitor/api/server.py
+++ b/monitor/api/server.py
@@ -33,11 +33,20 @@ def create_app(config: Dict[str, Any]) -> FastAPI:
# Determine if the process is running with admin/elevated rights or was started with --admin
try:
import sys
- try:
- import ctypes
- is_elev = bool(ctypes.windll.shell32.IsUserAnAdmin())
- except Exception:
- is_elev = False
+ import platform
+ import os
+ is_elev = False
+ if platform.system() == 'Windows':
+ try:
+ import ctypes
+ is_elev = bool(ctypes.windll.shell32.IsUserAnAdmin())
+ except Exception:
+ is_elev = False
+ else:
+ try:
+ is_elev = os.getuid() == 0
+ except Exception:
+ is_elev = False
started_with_flag = '--admin' in (sys.argv[1:] if len(sys.argv) > 1 else [])
app.state.is_admin = bool(is_elev or started_with_flag)
diff --git a/monitor/collectors/gpu.py b/monitor/collectors/gpu.py
index 774e2f2..e21e92f 100644
--- a/monitor/collectors/gpu.py
+++ b/monitor/collectors/gpu.py
@@ -292,6 +292,7 @@ def _resolve_username(self, pid: int) -> str:
system = platform.system()
if system == 'Windows':
# Use WMI via PowerShell to get the process owner
+ # This is a fallback when psutil is not available
cmd = [
'powershell', '-NoProfile', '-NonInteractive',
'-Command',
@@ -299,15 +300,17 @@ def _resolve_username(self, pid: int) -> str:
]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
out = (proc.stdout or '').strip()
- if out:
- return out
+ return out if out else ''
+ elif system == 'Darwin':
+ # macOS: use ps -o user= -p PID
+ cmd = ['ps', '-o', 'user=', '-p', str(pid)]
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=2)
+ return (proc.stdout or '').strip()
else:
- # POSIX: use ps to get the user for a PID
+ # Linux/POSIX: use ps -o user= -p PID
cmd = ['ps', '-o', 'user=', '-p', str(pid)]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=2)
- out = (proc.stdout or '').strip()
- if out:
- return out
+ return (proc.stdout or '').strip()
except Exception:
pass
return ''
diff --git a/scripts/hi.txt b/scripts/hi.txt
index aba8015..a023d25 100644
--- a/scripts/hi.txt
+++ b/scripts/hi.txt
@@ -1,11 +1,10 @@
-ROLE: You are the user's friendly younger brother (Chhota Bhai) helping them set up their project.
-TONE: Warm, respectful (Adarshuchak), and formal.
-GREETING: Refer to the user as "Bhaiya" (Older Brother) or "Sir".
-Standard spelling for older brother is 'भैया'. Use this instead of any other variation.
+TONE: Warm, respectful and formal.
+GREETING: Refer to the user as Older Brother or "Sir".
+Standard spelling for older brother is 'Bhaiya'. Use this instead of any other variation.
### HINDI (Hinglish) TECHNICAL GUIDELINES:
1. RESPECTFUL PRONOUNS: Always use "Aap" (आप) for the user. Never use "Tum" or "Tu".
-2. VERB ENDINGS: Use respectful endings like "Kijiye" (कीजिए) or "Kariye" (करिए) instead of "Karo".
+2. VERB ENDINGS: Use respectful endings like "Kijiye" or "Kariye" instead of "Karo".
- Dashboard -> डैशबोर्ड
- Monitoring -> मॉनिटरिंग
- Hardware Agnostic -> हार्डवेयर-स्वतंत्र (Hardware-Independent)
@@ -20,4 +19,4 @@ Standard spelling for older brother is 'भैया'. Use this instead of any o
Preserve all Markdown symbols (#, > *, ~~, **, `, -, [link](url)) exactly.
Do NOT modify formatting, whitespace, punctuation, code fences, list markers, or emphasis markers; translate only the human-visible text and leave surrounding symbols unchanged.
Preserve original formatting, punctuation, whitespace, and markdown/code symbols exactly; do NOT normalize, reflow, or 'fix' the input.
-6. NO HALLUCINATIONS: Do not explain the code. Just translate the prose with a respectful, brotherly touch.
\ No newline at end of file
+6. NO HALLUCINATIONS: Do not explain the code. Just translate the prose.
diff --git a/setup.sh b/setup.sh
new file mode 100644
index 0000000..b1c64f1
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,204 @@
+#!/bin/bash
+# MyGPU - Lightweight GPU Management Utility - Setup Script (Bash)
+# Supports Linux and macOS.
+
+set -e
+
+# Colors for output
+CYAN='\033[0;36m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+write_info() { echo -e "${CYAN}$1${NC}"; }
+write_ok() { echo -e "${GREEN}$1${NC}"; }
+write_warn() { echo -e "${YELLOW}$1${NC}"; }
+write_err() { echo -e "${RED}$1${NC}"; }
+
+# Detect script directory
+PROJECT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd "$PROJECT_DIR"
+
+get_project_version() {
+ local ver_file="monitor/__version__.py"
+ if [[ -f "$ver_file" ]]; then
+ local version=$(grep "__version__" "$ver_file" | cut -d'"' -f2)
+ echo "$version"
+ else
+ echo "(unknown)"
+ fi
+}
+
+ensure_uv() {
+ if command -v uv &> /dev/null; then
+ write_ok "[OK] uv detected: $(uv --version)"
+ return
+ fi
+
+ write_info "[INFO] uv not found. Installing uv..."
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+
+ # Add uv to path for this session
+ export PATH="$HOME/.local/bin:$PATH"
+
+ if ! command -v uv &> /dev/null; then
+ write_err "[ERROR] uv still not found after install attempt."
+ write_warn "Try adding $HOME/.local/bin to your PATH manually."
+ exit 1
+ fi
+
+ write_ok "[OK] uv installed: $(uv --version)"
+}
+
+test_cuda() {
+ if command -v nvidia-smi &> /dev/null; then
+ if nvidia-smi -L &> /dev/null; then
+ return 0
+ fi
+ fi
+ return 1
+}
+
+ensure_venv() {
+ local venv_dir="$PROJECT_DIR/.venv"
+ local venv_python="$venv_dir/bin/python"
+
+ if [[ -f "$venv_python" ]]; then
+ write_ok "[OK] Found existing venv: $venv_dir"
+ echo "$venv_python"
+ return
+ fi
+
+ write_info "Creating virtual environment with uv..."
+ uv venv "$venv_dir" &> /dev/null
+
+ if [[ ! -f "$venv_python" ]]; then
+ write_err "[ERROR] uv venv created, but python not found in $venv_dir"
+ exit 1
+ fi
+
+ write_ok "[OK] Created venv: $venv_dir"
+ echo "$venv_python"
+}
+
+version=$(get_project_version)
+write_info "\n=== MyGPU — Lightweight GPU Management Utility Setup ($version) ===\n"
+
+ensure_uv
+
+cuda_available=false
+if test_cuda; then
+ write_ok "[OK] CUDA/NVIDIA driver detected."
+ cuda_available=true
+else
+ write_warn "[INFO] CUDA not detected (nvidia-smi check failed)."
+ write_warn "Full (GPU) mode may fail or run CPU-only depending on platform."
+fi
+
+VENV_PYTHON=$(ensure_venv)
+write_ok "[OK] Using venv python: $VENV_PYTHON"
+
+req_path="requirements.txt"
+if [[ ! -f "$req_path" ]]; then
+ write_err "[ERROR] requirements.txt not found!"
+ exit 1
+fi
+
+write_info "\n=== Install Options ==="
+echo "1) minimal - CLI monitoring only"
+echo "2) normal - CLI + Web UI"
+echo "3) full - normal + GPU benchmarking"
+
+default_choice="2"
+[[ "$cuda_available" == true ]] && default_choice="3"
+
+read -p "Select [1-3] (default $default_choice): " choice
+choice=${choice:-$default_choice}
+
+case $choice in
+ 1) mode="minimal" ;;
+ 2) mode="normal" ;;
+ 3) mode="full" ;;
+ *)
+ write_warn "Invalid choice '$choice'; using default $default_choice"
+ [[ "$default_choice" == "3" ]] && mode="full" || mode="normal"
+ ;;
+esac
+
+write_info "\nSelected mode: $mode\n"
+
+# Simple parser for requirements.txt sections
+get_requirements() {
+ local section=$1
+ local in_section=false
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*\[$section\] ]]; then
+ in_section=true
+ continue
+ fi
+ if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*\[ ]]; then
+ in_section=false
+ continue
+ fi
+ if [[ "$in_section" == true ]] && [[ -n "$line" ]] && [[ ! "$line" =~ ^[[:space:]]*# ]]; then
+ echo "$line"
+ fi
+ done < "$req_path"
+}
+
+pkgs=""
+if [[ "$mode" == "minimal" ]]; then
+ pkgs=$(get_requirements "minimal")
+elif [[ "$mode" == "normal" ]]; then
+ pkgs=$(echo -e "$(get_requirements "minimal")\n$(get_requirements "normal")")
+else
+ pkgs=$(echo -e "$(get_requirements "minimal")\n$(get_requirements "normal")\n$(get_requirements "full")")
+fi
+
+if [[ -z "$pkgs" ]]; then
+ write_err "[ERROR] No packages resolved for mode '$mode'."
+ exit 1
+fi
+
+# Install packages
+write_info "Installing dependencies..."
+if [[ "$mode" != "full" ]]; then
+ uv pip install --python "$VENV_PYTHON" $pkgs
+else
+ # Full mode: handle torch specially if needed
+ torch_pkgs=""
+ other_pkgs=""
+ while read -r p; do
+ if [[ "$p" =~ ^(torch|torchvision|torchaudio) ]]; then
+ torch_pkgs="$torch_pkgs $p"
+ else
+ other_pkgs="$other_pkgs $p"
+ fi
+ done <<< "$pkgs"
+
+ if [[ -n "$other_pkgs" ]]; then
+ uv pip install --python "$VENV_PYTHON" $other_pkgs
+ fi
+
+ if [[ -n "$torch_pkgs" ]]; then
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ # macOS: install standard torch (MPS support included)
+ uv pip install --python "$VENV_PYTHON" $torch_pkgs
+ else
+ # Linux: use CUDA index
+ uv pip install --python "$VENV_PYTHON" --extra-index-url https://download.pytorch.org/whl/cu121 $torch_pkgs
+ fi
+ fi
+fi
+
+write_info "\n=== Done ==="
+write_ok "To run (no activation needed):"
+echo " CLI: $VENV_PYTHON health_monitor.py cli"
+echo " Web: $VENV_PYTHON health_monitor.py web"
+echo " Help: $VENV_PYTHON health_monitor.py --help"
+if [[ "$mode" == "full" ]]; then
+ echo " Benchmark: $VENV_PYTHON health_monitor.py benchmark --mode quick"
+fi
+echo ""
+write_warn "Tip: If you want an activated shell, run: source .venv/bin/activate"
From b4c08e502719699851ec2ad0e7ed49811a0cce46 Mon Sep 17 00:00:00 2001
From: Anshuman Singh <148977651+DataBoySu@users.noreply.github.com>
Date: Sat, 17 Jan 2026 23:21:51 +0530
Subject: [PATCH 2/3] fixes
---
monitor/api/server.py | 83 +++++++++++++++++++---------------
monitor/benchmark/workloads.py | 3 --
2 files changed, 46 insertions(+), 40 deletions(-)
diff --git a/monitor/api/server.py b/monitor/api/server.py
index f7b1ec9..5bc0d1c 100644
--- a/monitor/api/server.py
+++ b/monitor/api/server.py
@@ -400,26 +400,57 @@ async def read_simulation():
async def websocket_simulation(websocket: WebSocket):
await websocket.accept()
sim_runner = None
- sim_thread = None
+ update_task = None
+
+ async def send_status_updates():
+ while sim_runner and sim_runner.running:
+ try:
+ status = sim_runner.get_status()
+ if sim_runner.stress_worker:
+ positions, masses, colors, glows = sim_runner.stress_worker.get_particle_sample(max_samples=500)
+ if positions is not None:
+ particles_data = []
+ for i in range(len(positions)):
+ particles_data.append({
+ 'x': float(positions[i][0]),
+ 'y': float(positions[i][1]),
+ 'mass': float(masses[i]),
+ 'color': [float(colors[i][0]), float(colors[i][1]), float(colors[i][2])],
+ 'glow': float(glows[i]),
+ 'radius': 36.0 if masses[i] > 100 else 8.0
+ })
+ await websocket.send_json({
+ 'type': 'frame',
+ 'fps': status.get('fps', 0),
+ 'gpu': status.get('gpu_util', 0),
+ 'active_particles': status.get('iterations', 0) if sim_runner.stress_worker else 0,
+ 'iterations': status.get('iterations', 0),
+ 'particles': particles_data
+ })
+ except Exception:
+ break
+ await asyncio.sleep(0.033)
try:
while True:
data = await websocket.receive_json()
if data['type'] == 'start':
- particle_count = data.get('particles', 100000)
+ if sim_runner and sim_runner.running:
+ continue
+
+ num_particles = data.get('particles', 100000)
backend_mult = data.get('backend', 1)
config = benchmark_config.BenchmarkConfig(
benchmark_type='particle',
- duration_seconds=3600, # Long duration, will be stopped manually
+ duration_seconds=3600,
sample_interval_ms=100,
- particle_count=particle_count,
+ num_particles=num_particles,
backend_multiplier=backend_mult
)
sim_runner = benchmark_runner.get_benchmark_instance()
-
sim_thread = threading.Thread(
target=sim_runner.run,
args=(config, True),
@@ -427,34 +458,9 @@ async def websocket_simulation(websocket: WebSocket):
)
sim_thread.start()
- while sim_runner.running:
- status = sim_runner.get_status()
-
- if sim_runner.stress_worker:
- positions, masses, colors, glows = sim_runner.stress_worker.get_particle_sample(max_samples=500)
-
- if positions is not None:
- particles_data = []
- for i in range(len(positions)):
- particles_data.append({
- 'x': float(positions[i][0]),
- 'y': float(positions[i][1]),
- 'mass': float(masses[i]),
- 'color': [float(colors[i][0]), float(colors[i][1]), float(colors[i][2])],
- 'glow': float(glows[i]),
- 'radius': 36.0 if masses[i] > 100 else 8.0
- })
-
- await websocket.send_json({
- 'type': 'frame',
- 'fps': status.get('fps', 0),
- 'gpu': status.get('gpu_util', 0),
- 'active_particles': status.get('iterations', 0) if sim_runner.stress_worker else 0,
- 'iterations': status.get('iterations', 0),
- 'particles': particles_data
- })
-
- await asyncio.sleep(0.033) # ~30 FPS update rate
+ if update_task:
+ update_task.cancel()
+ update_task = asyncio.create_task(send_status_updates())
elif data['type'] == 'spawn' and sim_runner and sim_runner.stress_worker:
x = data.get('x', 500)
@@ -472,18 +478,21 @@ async def websocket_simulation(websocket: WebSocket):
elif data['type'] == 'stop':
if sim_runner:
sim_runner.running = False
+ sim_runner.stop()
+ if update_task:
+ update_task.cancel()
break
except WebSocketDisconnect:
- if sim_runner:
- sim_runner.running = False
+ pass
except Exception as e:
print(f"WebSocket error: {e}")
- if sim_runner:
- sim_runner.running = False
finally:
if sim_runner:
sim_runner.running = False
+ sim_runner.stop()
+ if update_task:
+ update_task.cancel()
@app.get("/api/status")
async def get_status():
diff --git a/monitor/benchmark/workloads.py b/monitor/benchmark/workloads.py
index f85530b..be66ff5 100644
--- a/monitor/benchmark/workloads.py
+++ b/monitor/benchmark/workloads.py
@@ -159,9 +159,6 @@ def _setup_torch(self):
self.workload_type = f"Bounce Simulation ({n:,} particles, torch)"
else:
self.workload_type = f"Bounce Simulation ({n:,} particles, torch)"
- backend_mult = self.config.backend_multiplier
- self._backend_stress.initialize('torch', torch, n, backend_mult)
-
self._initial_particle_count = n
self._active_count = self._counters['active_count']
self._small_ball_count = self._counters['small_ball_count']
From cd9b917b4de06acfb316468f22554c819f207c03 Mon Sep 17 00:00:00 2001
From: Anshuman Singh <148977651+DataBoySu@users.noreply.github.com>
Date: Sat, 17 Jan 2026 23:42:33 +0530
Subject: [PATCH 3/3] release 1.4.0
---
monitor/__version__.py | 2 +-
requirements.txt | 4 ++--
setup.sh | 5 +++--
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/monitor/__version__.py b/monitor/__version__.py
index e8f9fbc..2a236e8 100644
--- a/monitor/__version__.py
+++ b/monitor/__version__.py
@@ -1,3 +1,3 @@
-__version__ = "1.3.0"
+__version__ = "1.4.0"
__author__ = "DataBoySu"
__license__ = "MIT"
diff --git a/requirements.txt b/requirements.txt
index 13dc391..a370ea1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,8 +13,8 @@ fastapi>=0.104.0
uvicorn[standard]>=0.24.0
websockets>=12.0
win10toast
-winrt.windows.ui.notifications
-winrt.windows.data.xml.dom
+winrt-Windows.UI.Notifications
+winrt-Windows.Data.Xml.Dom
# VISUALIZATION (optional)
pygame>=2.5.0
diff --git a/setup.sh b/setup.sh
index b1c64f1..2c599b5 100644
--- a/setup.sh
+++ b/setup.sh
@@ -23,7 +23,8 @@ cd "$PROJECT_DIR"
get_project_version() {
local ver_file="monitor/__version__.py"
if [[ -f "$ver_file" ]]; then
- local version=$(grep "__version__" "$ver_file" | cut -d'"' -f2)
+ # Matches both single and double quotes
+ local version=$(grep "__version__" "$ver_file" | sed -E "s/__version__[[:space:]]*=[[:space:]]*['\"]([^'\"]+)['\"].*/\1/")
echo "$version"
else
echo "(unknown)"
@@ -187,7 +188,7 @@ else
uv pip install --python "$VENV_PYTHON" $torch_pkgs
else
# Linux: use CUDA index
- uv pip install --python "$VENV_PYTHON" --extra-index-url https://download.pytorch.org/whl/cu121 $torch_pkgs
+ uv pip install --python "$VENV_PYTHON" --extra-index-url https://download.pytorch.org/whl/cu128 $torch_pkgs
fi
fi
fi