diff --git a/.flake-modules/macmon.nix b/.flake-modules/macmon.nix new file mode 100644 index 00000000..5df0cdf4 --- /dev/null +++ b/.flake-modules/macmon.nix @@ -0,0 +1,30 @@ +# Provides macmon binary for the worker. + +# These values would bind to the consumer flake when this flake module is imported: +{ + config, + self, + inputs, + getSystem, + moduleWithSystem, + withSystem, + ... +}: + +# The actual flake-parts module configuration +{ + perSystem = + { + config, + self', + inputs', + pkgs, + system, + ... + }: + { + make-shells.default = { + packages = if (system == "aarch64-darwin") then ([ pkgs.macmon ]) else ([]); + }; + }; +} diff --git a/flake.nix b/flake.nix index ce7d82d1..fd3f2c31 100644 --- a/flake.nix +++ b/flake.nix @@ -62,6 +62,7 @@ flakeModules.flakeRoot flakeModules.justFlake flakeModules.goForwarder + ./.flake-modules/macmon.nix ]; systems = [ "x86_64-linux" diff --git a/worker/utils/macmon/bin/LICENSE.txt b/worker/utils/macmon/bin/LICENSE.txt deleted file mode 100644 index 4659b63d..00000000 --- a/worker/utils/macmon/bin/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 vladkens - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/worker/utils/macmon/bin/readme.md b/worker/utils/macmon/bin/readme.md deleted file mode 100644 index 9c44eed7..00000000 --- a/worker/utils/macmon/bin/readme.md +++ /dev/null @@ -1,154 +0,0 @@ -# `macmon` – Mac Monitor - -
- -Sudoless performance monitoring CLI tool for Apple Silicon processors. - -[](https://github.com/vladkens/macmon/releases) -[](https://github.com/vladkens/macmon/releases) -[](https://github.com/vladkens/macmon/blob/main/LICENSE) -[donate](https://buymeacoffee.com/vladkens) - -
- -
- preview -
- -## Motivation - -Apple Silicon processors don't provide an easy way to see live power consumption. I was interested in this information while testing local LLM models. `asitop` is a nice and simple TUI to quickly see current metrics, but it reads data from `powermetrics` and requires root privileges. `macmon` uses a private macOS API to gather metrics (essentially the same as `powermetrics`) but runs without sudo. 🎉 - -## 🌟 Features - -- 🚫 Works without sudo -- ⚡ Real-time CPU / GPU / ANE power usage -- 📊 CPU utilization per cluster -- 💾 RAM / Swap usage -- 📈 Historical charts + avg / max values -- 🌡️ Average CPU / GPU temperature -- 🎨 Switchable colors (6 variants) -- 🪟 Can be rendered in a small window -- 🦀 Written in Rust - -## 🍺 Install via Homebrew - -You can install [`macmon`](https://formulae.brew.sh/formula/macmon) using [brew](https://brew.sh/): - -```sh -$ brew install macmon -``` - -## 🖥️ Install via MacPorts - -You can also install [`macmon`](https://ports.macports.org/port/macmon/) using [MacPorts](https://macports.org/): - -```sh -$ sudo port install macmon -``` - -## 📦 Install from source - -1. Install [Rust toolchain](https://www.rust-lang.org/tools/install) - -2. Clone the repo: - -```sh -git clone https://github.com/vladkens/macmon.git && cd macmon -``` - -3. Build and run: - -```sh -cargo run -r -``` - -4. (Optionally) Binary can be moved to bin folder: - -```sh -sudo cp target/release/macmon /usr/local/bin -``` - -## 🚀 Usage - -```sh -Usage: macmon [OPTIONS] [COMMAND] - -Commands: - pipe Output metrics in JSON format - debug Print debug information - help Print this message or the help of the given subcommand(s) - -Options: - -i, --interval Update interval in milliseconds [default: 1000] - -h, --help Print help - -V, --version Print version - -Controls: - c - change color - v - switch charts view: gauge / sparkline - q - quit -``` - -## 🚰 Piping - -You can use the pipe subcommand to output metrics in JSON format, which is suitable for piping into other tools or scripts. For example: - -```sh -macmon pipe | jq -``` - -This command runs `macmon` in "pipe" mode and navigate output to `jq` for pretty-printing. - -You can also specify the number of samples to run using `-s` or `--samples` parameter (default: `0`, which runs indefinitely), and set update interval in milliseconds using the `-i` or `--interval` parameter (default: `1000` ms). For example: - -```sh -macmon pipe -s 10 -i 500 | jq -``` - -This will collect 10 samples with an update interval of 500 milliseconds. - -### Output - -```jsonc -{ - "timestamp": "2025-02-24T20:38:15.427569+00:00", - "temp": { - "cpu_temp_avg": 43.73614, // Celsius - "gpu_temp_avg": 36.95167 // Celsius - }, - "memory": { - "ram_total": 25769803776, // Bytes - "ram_usage": 20985479168, // Bytes - "swap_total": 4294967296, // Bytes - "swap_usage": 2602434560 // Bytes - }, - "ecpu_usage": [1181, 0.082656614], // (Frequency MHz, Usage %) - "pcpu_usage": [1974, 0.015181795], // (Frequency MHz, Usage %) - "gpu_usage": [461, 0.021497859], // (Frequency MHz, Usage %) - "cpu_power": 0.20486385, // Watts - "gpu_power": 0.017451683, // Watts - "ane_power": 0.0, // Watts - "all_power": 0.22231553, // Watts - "sys_power": 5.876533, // Watts - "ram_power": 0.11635789, // Watts - "gpu_ram_power": 0.0009615385 // Watts (not sure what it means) -} -``` - -## 🤝 Contributing -We love contributions! Whether you have ideas, suggestions, or bug reports, feel free to open an issue or submit a pull request. Your input is essential in helping us improve `macmon` 💪 - -## 📝 License -`macmon` is distributed under the MIT License. For more details, check out the LICENSE. - -## 🔍 See also -- [tlkh/asitop](https://github.com/tlkh/asitop) – Original tool. Python, requires sudo. -- [dehydratedpotato/socpowerbud](https://github.com/dehydratedpotato/socpowerbud) – ObjectiveC, sudoless, no TUI. -- [op06072/NeoAsitop](https://github.com/op06072/NeoAsitop) – Swift, sudoless. -- [graelo/pumas](https://github.com/graelo/pumas) – Rust, requires sudo. -- [context-labs/mactop](https://github.com/context-labs/mactop) – Go, requires sudo. - ---- - -*PS: One More Thing... Remember, monitoring your Mac's performance with `macmon` is like having a personal trainer for your processor — keeping those cores in shape! 💪* diff --git a/worker/utils/macmon/macmon.py b/worker/utils/macmon/macmon.py index 26b18416..8814fbd9 100644 --- a/worker/utils/macmon/macmon.py +++ b/worker/utils/macmon/macmon.py @@ -1,8 +1,7 @@ import asyncio -import os import platform import subprocess -from pathlib import Path +import shutil from typing import Optional, Tuple from pydantic import BaseModel, ConfigDict, ValidationError @@ -12,16 +11,10 @@ class MacMonError(Exception): """Exception raised for errors in the MacMon functions.""" -def _get_binary_path(binary_path: Optional[str] = None) -> str: +def _get_binary_path() -> str: """ Get the path to the macmon binary. - Args: - binary_path: Optional path to the binary. If not provided, will use the bundled binary. - - Returns: - The path to the macmon binary. - Raises: MacMonError: If the binary doesn't exist or can't be made executable. """ @@ -34,23 +27,11 @@ def _get_binary_path(binary_path: Optional[str] = None) -> str: ): raise MacMonError("MacMon only supports macOS with Apple Silicon (ARM) chips") - if binary_path: - path = binary_path - else: - # Get the directory where this module is located - module_dir = Path(os.path.dirname(os.path.abspath(__file__))) - path = str(module_dir / "bin" / "macmon") - # Ensure the binary exists and is executable - if not os.path.isfile(path): - raise MacMonError(f"Binary not found at: {path}") + path = shutil.which("macmon") - # Make the binary executable if it's not already - if not os.access(path, os.X_OK): - try: - os.chmod(path, 0o755) # rwx r-x r-x - except OSError as e: - raise MacMonError(f"Failed to make binary executable: {e}") from e + if path is None: + raise MacMonError(f"MacMon not found in PATH") return path @@ -109,20 +90,17 @@ class Metrics(BaseModel): # --------------------------------------------------------------------------- -def get_metrics(binary_path: Optional[str] = None) -> Metrics: +def get_metrics() -> Metrics: """ Run the binary and return the metrics as a Python dictionary. - Args: - binary_path: Optional path to the binary. If not provided, will use the bundled binary. - Returns: A mapping containing system metrics. Raises: MacMonError: If there's an error running the binary. """ - path = _get_binary_path(binary_path) + path = _get_binary_path() try: # Run the binary with the argument -s 1 and capture its output @@ -138,7 +116,7 @@ def get_metrics(binary_path: Optional[str] = None) -> Metrics: raise MacMonError(f"Error parsing JSON output: {e}") from e -async def get_metrics_async(binary_path: Optional[str] = None) -> Metrics: +async def get_metrics_async() -> Metrics: """ Asynchronously run the binary and return the metrics as a Python dictionary. @@ -151,7 +129,7 @@ async def get_metrics_async(binary_path: Optional[str] = None) -> Metrics: Raises: MacMonError: If there's an error running the binary. """ - path = _get_binary_path(binary_path) + path = _get_binary_path() try: proc = await asyncio.create_subprocess_exec(