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 @@ ![License](https://img.shields.io/badge/license-MIT-orange.svg) ![Python](https://img.shields.io/badge/python-3.10%2B-pink) -![Version](https://img.shields.io/badge/version-1.2.3-green) -![Platform](https://img.shields.io/badge/platform-Windows10/11-blue) +![Version](https://img.shields.io/badge/version-1.3.0-green) +![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue) ![cuda 12.x](https://img.shields.io/badge/CUDA-12.x-0f9d58?logo=nvidia) ## 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