init
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
bin/
|
||||||
|
.vscode/
|
||||||
|
*.class
|
||||||
|
*TestFile.java
|
||||||
|
*.jar
|
||||||
|
doc/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Jürg Georg Hallenbarter
|
||||||
|
|
||||||
|
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.
|
||||||
134
README.md
Normal file
134
README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# jtop - A Java-based System Monitoring Tool
|
||||||
|
|
||||||
|
\=============================================
|
||||||
|
|
||||||
|
Note: This project is not related to the Python-based jtop for NVIDIA Jetson devices ([https://rnext.it/jetson\_stats/](https://rnext.it/jetson_stats/))
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://www.java.com/en/)
|
||||||
|
[](https://www.linux.org/)
|
||||||
|
[](https://en.wikipedia.org/wiki/Software_release_life_cycle#Alpha)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
jtop is a lightweight, terminal-based system monitoring tool written in Java. It aims to replicate the basic functionality of the `top` command, providing a modular interface for easy system monitoring.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Resource-efficient design
|
||||||
|
* No external build tools required
|
||||||
|
* Modular and tab-based interface
|
||||||
|
* Supports Java 21+
|
||||||
|
|
||||||
|
## Target Platform
|
||||||
|
|
||||||
|
* Linux (primary target)
|
||||||
|
* Other platforms (e.g. macOS, freeBSD) may be supported in the future, pending compatibility testing and development.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
* Java 21+ installed on your system
|
||||||
|
* Linux platform
|
||||||
|
|
||||||
|
### Building and Running Locally
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/JGH0/jtop.git
|
||||||
|
cd jtop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Compile the code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
javac src/*.java
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java src/Main.java
|
||||||
|
```
|
||||||
|
|
||||||
|
### System-wide Installation
|
||||||
|
|
||||||
|
To install jtop globally so that it can be run from any terminal:
|
||||||
|
|
||||||
|
1. Clone the repository (if not done already):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/JGH0/jtop.git
|
||||||
|
cd jtop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make the build and install scripts executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x build.sh install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the project using the provided build script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install globally (requires root privileges):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run jtop from anywhere:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jtop
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
* The `build.sh` script compiles all Java source files and creates an executable `jtop.jar`.
|
||||||
|
* The `install.sh` script copies `jtop.jar` to `/usr/local/lib/jtop` and installs a wrapper script in `/usr/local/bin` for easy execution.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
jtop provides a simple and intuitive interface for system monitoring. Use the following keys to navigate:
|
||||||
|
|
||||||
|
* `j`/`k`: Scroll up/down
|
||||||
|
* `Enter`: Scroll entire row
|
||||||
|
* `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions from the community! To contribute:
|
||||||
|
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Create a new branch for your feature or bug fix.
|
||||||
|
3. Make your changes with proper testing.
|
||||||
|
4. Submit a pull request detailing your modifications.
|
||||||
|
|
||||||
|
### Developer Documentation
|
||||||
|
|
||||||
|
Developer documentation is generated using JavaDoc. To generate and view the documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x generate_javadoc.sh
|
||||||
|
./generate_javadoc.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a `docs/` folder with HTML documentation you can view in your browser.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
jtop is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
* Jürg Georg Hallenbarter
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
jtop is currently in **Alpha** stage, which means it is still a work-in-progress and may contain bugs or incomplete features. Use at your own risk!
|
||||||
99
build.sh
Executable file
99
build.sh
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# --- Build script for jtop ---
|
||||||
|
# This script compiles Java sources into a JAR.
|
||||||
|
# If JDK is missing, it prompts the user to install it using the detected package manager.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
JTOP_NAME="jtop"
|
||||||
|
SRC_DIR="src"
|
||||||
|
BIN_DIR="bin"
|
||||||
|
JAR_FILE="${JTOP_NAME}.jar"
|
||||||
|
MAIN_CLASS="${JTOP_NAME}.Main"
|
||||||
|
|
||||||
|
# --- Detect package manager ---
|
||||||
|
detect_package_manager() {
|
||||||
|
if command -v pacman >/dev/null 2>&1; then
|
||||||
|
echo "pacman"
|
||||||
|
elif command -v apt >/dev/null 2>&1; then
|
||||||
|
echo "apt"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
echo "dnf"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
echo "yum"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then
|
||||||
|
echo "zypper"
|
||||||
|
elif command -v brew >/dev/null 2>&1; then
|
||||||
|
echo "brew"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
echo "apk"
|
||||||
|
elif command -v emerge >/dev/null 2>&1; then
|
||||||
|
echo "emerge"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Generate JDK install command ---
|
||||||
|
jdk_install_command() {
|
||||||
|
local pm="$1"
|
||||||
|
case "$pm" in
|
||||||
|
pacman) echo "pacman -Sy --noconfirm jdk-openjdk" ;;
|
||||||
|
apt) echo "apt update && apt install -y openjdk-21-jdk || apt install -y default-jdk" ;;
|
||||||
|
dnf) echo "dnf install -y java-21-openjdk-devel || dnf install -y java-latest-openjdk-devel" ;;
|
||||||
|
yum) echo "yum install -y java-21-openjdk-devel || yum install -y java-latest-openjdk-devel" ;;
|
||||||
|
zypper) echo "zypper install -y java-21-openjdk-devel || zypper install -y java-latest-openjdk-devel" ;;
|
||||||
|
brew) echo "brew install openjdk" ;;
|
||||||
|
apk) echo "apk add openjdk21" ;;
|
||||||
|
emerge) echo "emerge dev-java/openjdk-bin" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check for javac ---
|
||||||
|
if ! command -v javac >/dev/null 2>&1; then
|
||||||
|
echo "Java Development Kit (JDK) not found."
|
||||||
|
PM=$(detect_package_manager)
|
||||||
|
|
||||||
|
if [[ -z "$PM" ]]; then
|
||||||
|
echo "Please install the latest JDK manually and rerun this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CMD=$(jdk_install_command "$PM")
|
||||||
|
if [[ $EUID -ne 0 && "$PM" != "brew" ]]; then
|
||||||
|
echo "Please run this script as root to install the JDK, or install it manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt the user
|
||||||
|
read -rp "Do you want to run the following command to install the JDK? [$CMD] (y/n): " ANSWER
|
||||||
|
case "$ANSWER" in
|
||||||
|
y|Y)
|
||||||
|
echo "Installing JDK..."
|
||||||
|
eval "$CMD"
|
||||||
|
echo "JDK installed successfully."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "JDK installation cancelled. Please install manually and rerun."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build process ---
|
||||||
|
echo "Cleaning previous build..."
|
||||||
|
rm -rf "$BIN_DIR" "$JAR_FILE"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
|
echo "Compiling Java sources..."
|
||||||
|
# The -d flag preserves the package directory structure
|
||||||
|
find "$SRC_DIR" -name "*.java" > sources.txt
|
||||||
|
javac -d "$BIN_DIR" @sources.txt
|
||||||
|
rm sources.txt
|
||||||
|
|
||||||
|
echo "Creating JAR file..."
|
||||||
|
jar cfe "$JAR_FILE" "$MAIN_CLASS" -C "$BIN_DIR" .
|
||||||
|
|
||||||
|
echo "Build completed successfully: ${JAR_FILE}"
|
||||||
120
config/default.conf
Normal file
120
config/default.conf
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# /$$
|
||||||
|
# | $$
|
||||||
|
# /$$ /$$$$$$ /$$$$$$ /$$$$$$
|
||||||
|
# |__/|_ $$_/ /$$__ $$ /$$__ $$
|
||||||
|
# /$$ | $$ | $$ \ $$| $$ \ $$
|
||||||
|
# | $$ | $$ /$$| $$ | $$| $$ | $$
|
||||||
|
# | $$ | $$$$/| $$$$$$/| $$$$$$$/
|
||||||
|
# | $$ \___/ \______/ | $$____/
|
||||||
|
# /$$ | $$ | $$
|
||||||
|
#| $$$$$$/ | $$
|
||||||
|
# \______/ |__/
|
||||||
|
# jtop - default.conf
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
# --- Table Layout ---
|
||||||
|
table.header.content = PID,NAME,PATH,USER,CPU,MEMORY,DISK_READ,DISK_WRITE,NETWORK # header content available: PID, NAME, PATH, USER, CPU, MEMORY, DISK_READ, DISK_WRITE, NETWORK
|
||||||
|
|
||||||
|
# --- Sorting ---
|
||||||
|
table.sorting.ASC = false # default sorting order false for "DESC" and true for "ASC"
|
||||||
|
table.sorting.default.header = "PID" # default sorting header available: "PID", "NAME", "PATH", "USER", "CPU", "MEMORY", "DISK_READ", "DISK_WRITE", "NETWORK"
|
||||||
|
|
||||||
|
# --- CPU Column ---
|
||||||
|
table.value.CPU.accuracy = 3 # decimal places for CPU usage
|
||||||
|
|
||||||
|
# --- Memory Column ---
|
||||||
|
table.value.MEMORY.accuracy = 3 # decimal places for memory
|
||||||
|
|
||||||
|
# --- Network Column ---
|
||||||
|
table.value.NETWORK.format = "Mb" # valid: b,B,Kb,KB,Mb,MB,Gb,GB,Tb,TB
|
||||||
|
table.value.NETWORK.accuracy = 3 # decimal places for network speed
|
||||||
|
table.value.NETWORK.scientificNotation = false # use scientitic notation instead of standart notation
|
||||||
|
|
||||||
|
# --- Disk I/O Columns ---
|
||||||
|
table.value.DISK_READ.format = "Mb" # valid: b,B,Kb,KB,Mb,MB,Gb,GB,Tb,TB
|
||||||
|
table.value.DISK_READ.accuracy = 3 # decimal places for disk reaq speed
|
||||||
|
table.value.DISK_WRITE.format = "Mb" # valid: b,B,Kb,KB,Mb,MB,Gb,GB,Tb,TB
|
||||||
|
table.value.DISK_WRITE.accuracy = 3 # decimal places for disk write speed
|
||||||
|
|
||||||
|
# --- Keybinding Config ---
|
||||||
|
footer.text.keyBindings = "Use j/k to scroll, Enter to scroll entire row, 'q' or Ctrl+C to quit"
|
||||||
|
|
||||||
|
# --- Design Config ---
|
||||||
|
# ANSI color codes let you style terminal output (text & background).
|
||||||
|
# Codes start with "\033[" and end with "m". Combine multiple codes by adding them.
|
||||||
|
|
||||||
|
header.color = "\033[47m" + "\033[30m" # White background, black text
|
||||||
|
footer.color = "\033[41m" + "\033[37m" # Red background, white text
|
||||||
|
table.color = "\033[40m" + "\033[37m" # Black background, white text
|
||||||
|
|
||||||
|
# --- Base Color Codes ---
|
||||||
|
# Foreground (Text): 30–37
|
||||||
|
# Background: 40–47
|
||||||
|
# Bright Foreground: 90–97
|
||||||
|
# Bright Background: 100–107
|
||||||
|
|
||||||
|
# --- Foreground Colors ---
|
||||||
|
# \033[30m = Black
|
||||||
|
# \033[31m = Red
|
||||||
|
# \033[32m = Green
|
||||||
|
# \033[33m = Yellow
|
||||||
|
# \033[34m = Blue
|
||||||
|
# \033[35m = Magenta
|
||||||
|
# \033[36m = Cyan
|
||||||
|
# \033[37m = White
|
||||||
|
|
||||||
|
# --- Background Colors ---
|
||||||
|
# \033[40m = Black
|
||||||
|
# \033[41m = Red
|
||||||
|
# \033[42m = Green
|
||||||
|
# \033[43m = Yellow
|
||||||
|
# \033[44m = Blue
|
||||||
|
# \033[45m = Magenta
|
||||||
|
# \033[46m = Cyan
|
||||||
|
# \033[47m = White
|
||||||
|
|
||||||
|
# --- Bright Foreground Colors ---
|
||||||
|
# \033[90m = Bright Black (Gray)
|
||||||
|
# \033[91m = Bright Red
|
||||||
|
# \033[92m = Bright Green
|
||||||
|
# \033[93m = Bright Yellow
|
||||||
|
# \033[94m = Bright Blue
|
||||||
|
# \033[95m = Bright Magenta
|
||||||
|
# \033[96m = Bright Cyan
|
||||||
|
# \033[97m = Bright White
|
||||||
|
|
||||||
|
# --- Bright Background Colors ---
|
||||||
|
# \033[100m = Bright Black (Gray)
|
||||||
|
# \033[101m = Bright Red
|
||||||
|
# \033[102m = Bright Green
|
||||||
|
# \033[103m = Bright Yellow
|
||||||
|
# \033[104m = Bright Blue
|
||||||
|
# \033[105m = Bright Magenta
|
||||||
|
# \033[106m = Bright Cyan
|
||||||
|
# \033[107m = Bright White
|
||||||
|
|
||||||
|
# --- Text Formatting (Styles) ---
|
||||||
|
# \033[0m = Reset all styles and colors
|
||||||
|
# \033[1m = Bold / Bright text
|
||||||
|
# \033[2m = Dim text
|
||||||
|
# \033[3m = Italic (may not work in all terminals)
|
||||||
|
# \033[4m = Underline
|
||||||
|
# \033[5m = Blink (slow)
|
||||||
|
# \033[6m = Blink (rapid)
|
||||||
|
# \033[7m = Reverse (swap foreground and background)
|
||||||
|
# \033[8m = Hidden / Conceal text
|
||||||
|
# \033[9m = Strikethrough
|
||||||
|
# \033[21m = Double underline (rarely supported)
|
||||||
|
# \033[22m = Normal intensity (cancel bold/dim)
|
||||||
|
|
||||||
|
# --- Combining Styles ---
|
||||||
|
# Example: bold + underline + red text
|
||||||
|
# footer.color = "\033[1m" + "\033[4m" + "\033[31m"
|
||||||
|
|
||||||
|
# --- 256-Color and TrueColor (Modern Terminals like Kitty) ---
|
||||||
|
# 256-Color Foreground: \033[38;5;{n}m (0–255)
|
||||||
|
# 256-Color Background: \033[48;5;{n}m
|
||||||
|
# TrueColor (RGB) Foreground: \033[38;2;R;G;B m
|
||||||
|
# TrueColor (RGB) Background: \033[48;2;R;G;B m
|
||||||
|
# Example: = orange text
|
||||||
|
# footer.color = "\033[38;2;255;128;0m"
|
||||||
96
generate_javadoc.sh
Executable file
96
generate_javadoc.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# --- Generate JavaDoc for jtop ---
|
||||||
|
# This script generates developer documentation using javadoc.
|
||||||
|
# If JDK is missing, it prompts the user to install it using the detected package manager.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SRC_DIR="src"
|
||||||
|
DOC_DIR="doc"
|
||||||
|
|
||||||
|
# --- Detect package manager ---
|
||||||
|
detect_package_manager() {
|
||||||
|
if command -v pacman >/dev/null 2>&1; then
|
||||||
|
echo "pacman"
|
||||||
|
elif command -v apt >/dev/null 2>&1; then
|
||||||
|
echo "apt"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
echo "dnf"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
echo "yum"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then
|
||||||
|
echo "zypper"
|
||||||
|
elif command -v brew >/dev/null 2>&1; then
|
||||||
|
echo "brew"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
echo "apk"
|
||||||
|
elif command -v emerge >/dev/null 2>&1; then
|
||||||
|
echo "emerge"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Generate JDK install command ---
|
||||||
|
jdk_install_command() {
|
||||||
|
local pm="$1"
|
||||||
|
case "$pm" in
|
||||||
|
pacman) echo "pacman -Sy --noconfirm jdk-openjdk" ;;
|
||||||
|
apt) echo "apt update && apt install -y openjdk-21-jdk || apt install -y default-jdk" ;;
|
||||||
|
dnf) echo "dnf install -y java-21-openjdk-devel || dnf install -y java-latest-openjdk-devel" ;;
|
||||||
|
yum) echo "yum install -y java-21-openjdk-devel || yum install -y java-latest-openjdk-devel" ;;
|
||||||
|
zypper) echo "zypper install -y java-21-openjdk-devel || zypper install -y java-latest-openjdk-devel" ;;
|
||||||
|
brew) echo "brew install openjdk" ;;
|
||||||
|
apk) echo "apk add openjdk21" ;;
|
||||||
|
emerge) echo "emerge dev-java/openjdk-bin" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check for javadoc ---
|
||||||
|
if ! command -v javadoc >/dev/null 2>&1; then
|
||||||
|
echo "Java Development Kit (JDK) with javadoc not found."
|
||||||
|
PM=$(detect_package_manager)
|
||||||
|
|
||||||
|
if [[ -z "$PM" ]]; then
|
||||||
|
echo "Please install the latest JDK manually and rerun this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CMD=$(jdk_install_command "$PM")
|
||||||
|
if [[ $EUID -ne 0 && "$PM" != "brew" ]]; then
|
||||||
|
echo "Please rerun this script with sudo if you want automatic JDK installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt the user
|
||||||
|
read -rp "Do you want to run the following command to install the JDK? [$CMD] (y/n): " ANSWER
|
||||||
|
case "$ANSWER" in
|
||||||
|
y|Y)
|
||||||
|
echo "Installing JDK..."
|
||||||
|
eval "$CMD"
|
||||||
|
echo "JDK installed successfully."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "JDK installation cancelled. Please install manually and rerun."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Generate JavaDoc ---
|
||||||
|
echo "Generating JavaDoc..."
|
||||||
|
rm -rf "$DOC_DIR"
|
||||||
|
mkdir -p "$DOC_DIR"
|
||||||
|
|
||||||
|
if javadoc -quiet -d "$DOC_DIR" \
|
||||||
|
-sourcepath "$SRC_DIR" \
|
||||||
|
-subpackages jtop \
|
||||||
|
-private \
|
||||||
|
-author \
|
||||||
|
-version > /dev/null 2>&1; then
|
||||||
|
echo -e "\033[32mJavaDoc generated successfully!"
|
||||||
|
echo -e "\033[0m→ Open file://$PWD/$DOC_DIR/index.html to view it."
|
||||||
|
else
|
||||||
|
echo -e "\033[31mJavaDoc generation failed (check your paths or source)."
|
||||||
|
fi
|
||||||
16
install.sh
Executable file
16
install.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install script for system-wide usage
|
||||||
|
|
||||||
|
# Ensure running as root
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Please run as root to install globally."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create installation directories
|
||||||
|
mkdir -p /usr/local/lib/jtop
|
||||||
|
cp jtop.jar /usr/local/lib/jtop/
|
||||||
|
cp jtop.sh /usr/local/bin/jtop
|
||||||
|
chmod +x /usr/local/bin/jtop
|
||||||
|
|
||||||
|
echo "jtop installed successfully! You can now run 'jtop' from anywhere."
|
||||||
54
jtop.sh
Executable file
54
jtop.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# jtop launcher with self-update via GitHub + rebuild (requires sudo for update)
|
||||||
|
|
||||||
|
JTOP_DIR="/usr/local/lib/jtop"
|
||||||
|
JTOP_JAR="$JTOP_DIR/jtop.jar"
|
||||||
|
GIT_REPO="https://github.com/JGH0/jtop.git"
|
||||||
|
|
||||||
|
update_jtop() {
|
||||||
|
# Check for sudo/root
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "jtop --update requires root privileges. Please run with sudo."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating jtop from GitHub..."
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
echo "Cloning repository into $TMP_DIR..."
|
||||||
|
if ! git clone --depth 1 "$GIT_REPO" "$TMP_DIR"; then
|
||||||
|
echo "Failed to clone repository."
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building jtop..."
|
||||||
|
pushd "$TMP_DIR" >/dev/null
|
||||||
|
if ! ./build.sh; then
|
||||||
|
echo "Build failed."
|
||||||
|
popd >/dev/null
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing new version..."
|
||||||
|
if ! ./install.sh; then
|
||||||
|
echo "Install failed."
|
||||||
|
popd >/dev/null
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
popd >/dev/null
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
echo "Update completed successfully!"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle --update argument
|
||||||
|
if [[ "$1" == "--update" ]]; then
|
||||||
|
update_jtop
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run jtop normally
|
||||||
|
java -jar "$JTOP_JAR" "$@"
|
||||||
88
report.md
Normal file
88
report.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# jtop Project Report
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**jtop** is a terminal-based system monitoring tool written in Java. It provides a lightweight alternative to the `top` command, featuring a modular and extensible interface.
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
* CPU, memory, disk, and network usage monitoring
|
||||||
|
* Process list with scrollable interface
|
||||||
|
* Sorting by CPU, memory, PID, name, path, and other headers
|
||||||
|
* Temperature monitoring (via `/sys/class/hwmon` or `/sys/class/thermal`)
|
||||||
|
* Terminal-size adaptive display
|
||||||
|
* Keyboard navigation: `j/k` to scroll, `Enter` for page scroll, `q` or `Ctrl+C` to quit
|
||||||
|
|
||||||
|
## Class Structure
|
||||||
|
|
||||||
|
* `Main` — Entry point; sets up the terminal and refresh loop
|
||||||
|
* `ShowProcesses` — Collects and manages running process data
|
||||||
|
* `ProcessRow` — Represents a single process entry
|
||||||
|
* `ProcessTableRenderer` — Draws the process table in the terminal
|
||||||
|
* `ProcessSorter` — Provides comparators for sorting
|
||||||
|
* `MemoryInfo`, `CpuInfo`, `DiskInfo`, `NetworkInfo`, `TemperatureInfo`, `Uptime`, etc. — System metrics modules
|
||||||
|
* `Header` — Displays header information
|
||||||
|
* `InputHandler` — Reads and handles keyboard and mouse input
|
||||||
|
* `TerminalSize` — Detects terminal dimensions
|
||||||
|
* `RefreshThread` — Handles background refresh of process data
|
||||||
|
* `PathInfo` — Retrieves process path and name
|
||||||
|
|
||||||
|
## Design Considerations
|
||||||
|
|
||||||
|
* Modular architecture with clear separation of data collection, rendering, and input handling
|
||||||
|
* Uses Java 21+ features and the ProcessHandle API
|
||||||
|
* Layout adapts dynamically to terminal size to prevent overflow
|
||||||
|
* Graceful handling of missing or inaccessible process information
|
||||||
|
* Keyboard navigation inspired by tools like `less` and `top`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Compile and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
javac src/*.java
|
||||||
|
java src/Main.java
|
||||||
|
```
|
||||||
|
|
||||||
|
For system-wide installation, use the included `build.sh` and `install.sh` scripts.
|
||||||
|
|
||||||
|
**Keyboard shortcuts:**
|
||||||
|
|
||||||
|
* `j/k`: scroll up/down
|
||||||
|
* `Enter`: scroll one page
|
||||||
|
* `q` or `Ctrl+C`: quit
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
* Clear separation of concerns
|
||||||
|
* Proper encapsulation of fields
|
||||||
|
* Use of constructors and method overloading where appropriate
|
||||||
|
* Aggregation/composition used in `ShowProcesses` and rendering classes
|
||||||
|
* Interfaces used via `Comparator` for sorting
|
||||||
|
* Inheritance applied through `Thread` extension in `RefreshThread`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
* JavaDoc documentation can be generated using `./generate_javadoc.sh`
|
||||||
|
* The project is in an **alpha stage** — some metrics may not work on all hardware
|
||||||
|
* Tested primarily on Linux systems with Intel and AMD CPUs
|
||||||
|
|
||||||
|
## TODOs / Known Issues
|
||||||
|
|
||||||
|
### Tab View
|
||||||
|
* Planned implementation of a tab system to allow grouping of process information
|
||||||
|
|
||||||
|
### Config
|
||||||
|
* Config file support is partially implemented — some fields (e.g., `table.header.content`) are not yet parsed or applied
|
||||||
|
* Configs are not yet accessible in system-wide installations
|
||||||
|
|
||||||
|
### Performance / Design
|
||||||
|
* On some terminals with custom themes, colors may display incorrectly
|
||||||
|
* Cursor animations can cause slight lag during refresh
|
||||||
|
* Table caching occasionally fails, causing unnecessary redraws and performance drops
|
||||||
|
* On low-performance machines, keyboard input may experience minor delay
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
* **Jürg Georg Hallenbarter**
|
||||||
|
* Version 1.0 — Date: 2025-10-24
|
||||||
142
src/jtop/App.java
Normal file
142
src/jtop/App.java
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package jtop;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import jtop.config.Config;
|
||||||
|
import jtop.core.InfoType;
|
||||||
|
import jtop.core.RefreshThread;
|
||||||
|
import jtop.core.ShowProcesses;
|
||||||
|
import jtop.terminal.InputHandler;
|
||||||
|
import jtop.terminal.TerminalSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core application class that initializes and coordinates all major components
|
||||||
|
* of the terminal-based process monitor.
|
||||||
|
* <p>
|
||||||
|
* This class is responsible for:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Configuring the terminal (raw input mode and mouse reporting)</li>
|
||||||
|
* <li>Initializing configuration and display components</li>
|
||||||
|
* <li>Starting background refresh and input handling threads</li>
|
||||||
|
* <li>Ensuring proper cleanup and terminal restoration on exit</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class App {
|
||||||
|
|
||||||
|
/** Utility for determining terminal dimensions. */
|
||||||
|
private final TerminalSize terminalSize = new TerminalSize();
|
||||||
|
|
||||||
|
/** Holds configuration data for runtime settings. */
|
||||||
|
private final Config config = new Config();
|
||||||
|
|
||||||
|
/** Handles process retrieval and display logic. */
|
||||||
|
private final ShowProcesses showProcesses;
|
||||||
|
|
||||||
|
/** Shared flag for synchronizing display refreshes. */
|
||||||
|
private final AtomicBoolean refresh = new AtomicBoolean(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code App} instance and initializes the main process display.
|
||||||
|
* <p>
|
||||||
|
* By default, it prepares a {@link ShowProcesses} object displaying
|
||||||
|
* PID, process name, user, CPU usage, and memory usage.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public App() {
|
||||||
|
showProcesses = new ShowProcesses(
|
||||||
|
InfoType.PID,
|
||||||
|
InfoType.NAME,
|
||||||
|
InfoType.USER,
|
||||||
|
InfoType.CPU,
|
||||||
|
InfoType.MEMORY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the main application loop.
|
||||||
|
* <p>
|
||||||
|
* This method:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Enables raw input mode and mouse tracking</li>
|
||||||
|
* <li>Draws the initial process table</li>
|
||||||
|
* <li>Launches the background refresh thread</li>
|
||||||
|
* <li>Delegates user interaction to the {@link InputHandler}</li>
|
||||||
|
* </ul>
|
||||||
|
* When the loop ends, terminal settings are restored and mouse reporting is disabled.
|
||||||
|
*
|
||||||
|
* @throws Exception if an I/O or threading error occurs
|
||||||
|
*/
|
||||||
|
public void run() throws Exception {
|
||||||
|
enableRawMode();
|
||||||
|
enableMouseReporting();
|
||||||
|
|
||||||
|
try {
|
||||||
|
showProcesses.draw(); // initial draw
|
||||||
|
|
||||||
|
// Start background refresh
|
||||||
|
Thread refreshThread = new RefreshThread(showProcesses, refresh);
|
||||||
|
refreshThread.setDaemon(true);
|
||||||
|
refreshThread.start();
|
||||||
|
|
||||||
|
// Handle user input
|
||||||
|
new InputHandler(showProcesses, refresh, terminalSize).start();
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
disableMouseReporting();
|
||||||
|
restoreTerminal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables raw input mode on the terminal.
|
||||||
|
* <p>
|
||||||
|
* This disables canonical input processing and echoing,
|
||||||
|
* allowing single keypress handling without pressing Enter.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws IOException if the process builder fails
|
||||||
|
* @throws InterruptedException if the command execution is interrupted
|
||||||
|
*/
|
||||||
|
private void enableRawMode() throws IOException, InterruptedException {
|
||||||
|
new ProcessBuilder("sh", "-c", "stty raw -echo </dev/tty").inheritIO().start().waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores terminal settings to normal mode.
|
||||||
|
* <p>
|
||||||
|
* This re-enables standard input behavior and character echoing.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws IOException if the process builder fails
|
||||||
|
* @throws InterruptedException if the command execution is interrupted
|
||||||
|
*/
|
||||||
|
private void restoreTerminal() throws IOException, InterruptedException {
|
||||||
|
new ProcessBuilder("sh", "-c", "stty sane </dev/tty").inheritIO().start().waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables mouse reporting mode.
|
||||||
|
* <p>
|
||||||
|
* Allows the application to receive and interpret mouse events
|
||||||
|
* such as clicks and scrolls in the terminal.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private void enableMouseReporting() {
|
||||||
|
System.out.print("\u001B[?1000h");
|
||||||
|
System.out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables mouse reporting mode.
|
||||||
|
* <p>
|
||||||
|
* Ensures that the terminal stops sending mouse event sequences
|
||||||
|
* once the program exits or cleans up.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private void disableMouseReporting() {
|
||||||
|
System.out.print("\u001B[?1000l");
|
||||||
|
System.out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/jtop/Isystem/IBatteryInfo.java
Normal file
52
src/jtop/Isystem/IBatteryInfo.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for retrieving battery information.
|
||||||
|
* <p>
|
||||||
|
* Allows platform-specific implementations to provide battery status, charge,
|
||||||
|
* voltage, energy, and power readings.
|
||||||
|
*/
|
||||||
|
public interface IBatteryInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current battery charge percentage (0–100).
|
||||||
|
*
|
||||||
|
* @return battery percentage, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
int getBatteryPercentage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current battery status (e.g., Charging, Discharging, Full).
|
||||||
|
*
|
||||||
|
* @return battery status string, or null if unavailable
|
||||||
|
*/
|
||||||
|
String getBatteryStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current battery voltage in volts.
|
||||||
|
*
|
||||||
|
* @return voltage in volts, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
double getVoltage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current battery energy in watt-hours.
|
||||||
|
*
|
||||||
|
* @return energy in Wh, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
double getEnergy();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current power draw in watts.
|
||||||
|
*
|
||||||
|
* @return power in W, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
double getPower();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the system has a battery.
|
||||||
|
*
|
||||||
|
* @return true if battery is present and readable, false otherwise
|
||||||
|
*/
|
||||||
|
boolean hasBattery();
|
||||||
|
}
|
||||||
32
src/jtop/Isystem/ICpuInfo.java
Normal file
32
src/jtop/Isystem/ICpuInfo.java
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for retrieving CPU usage information in a cross-platform way.
|
||||||
|
* <p>
|
||||||
|
* Implementations can provide system-specific logic for Linux, macOS, Windows, or FreeBSD.
|
||||||
|
*/
|
||||||
|
public interface ICpuInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the CPU usage percentage of a specific process.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return CPU usage as a percentage, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
double getCpuPercent(long pid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the system load average.
|
||||||
|
*
|
||||||
|
* @return the load average string, or null if unavailable
|
||||||
|
*/
|
||||||
|
String getLoadAverage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the overall CPU usage percentage over a sample period.
|
||||||
|
*
|
||||||
|
* @param sampleMs the sample duration in milliseconds
|
||||||
|
* @return the CPU usage as a percentage over the sample period, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
double getCpuUsage(long sampleMs);
|
||||||
|
}
|
||||||
31
src/jtop/Isystem/IDiskInfo.java
Normal file
31
src/jtop/Isystem/IDiskInfo.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for providing information about disk usage and I/O statistics for mounted devices.
|
||||||
|
* <p>
|
||||||
|
* Implementations should retrieve disk statistics and provide a map of device names to their
|
||||||
|
* read/write counts.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public interface IDiskInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves disk I/O statistics for all block devices.
|
||||||
|
* <p>
|
||||||
|
* Each entry in the returned map contains the device name as the key,
|
||||||
|
* and an array of two long values as the value:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>index 0 - number of reads completed</li>
|
||||||
|
* <li>index 1 - number of writes completed</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return a {@link Map} where the key is the device name and the value is a
|
||||||
|
* long array containing [reads, writes]
|
||||||
|
* @throws IOException if disk statistics cannot be read
|
||||||
|
*/
|
||||||
|
Map<String, long[]> getDiskStats() throws IOException;
|
||||||
|
}
|
||||||
50
src/jtop/Isystem/IMemoryInfo.java
Normal file
50
src/jtop/Isystem/IMemoryInfo.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to gather memory usage statistics.
|
||||||
|
* <p>
|
||||||
|
* Implementations typically read Linux <code>/proc</code> files to determine:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Total and available system memory</li>
|
||||||
|
* <li>Memory usage percentage by the system</li>
|
||||||
|
* <li>Memory usage percentage of a specific process (by PID)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public interface IMemoryInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the memory usage percentage of a process.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return memory usage percentage of the process
|
||||||
|
* @throws IOException if the /proc file cannot be read or is malformed
|
||||||
|
*/
|
||||||
|
double getMemoryPercent(long pid) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the overall memory usage percentage of the system.
|
||||||
|
*
|
||||||
|
* @return memory usage percentage of the system
|
||||||
|
* @throws IOException if /proc/meminfo cannot be read
|
||||||
|
*/
|
||||||
|
double getMemoryUsage() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns total system memory in bytes.
|
||||||
|
*
|
||||||
|
* @return total memory in bytes
|
||||||
|
* @throws IOException if /proc/meminfo cannot be read
|
||||||
|
*/
|
||||||
|
long getTotalMemoryBytes() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns used memory in bytes (total minus available memory).
|
||||||
|
*
|
||||||
|
* @return used memory in bytes
|
||||||
|
* @throws IOException if /proc/meminfo cannot be read
|
||||||
|
*/
|
||||||
|
long getAvailableMemoryBytes() throws IOException;
|
||||||
|
}
|
||||||
27
src/jtop/Isystem/INetworkInfo.java
Normal file
27
src/jtop/Isystem/INetworkInfo.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to collect network usage statistics.
|
||||||
|
* <p>
|
||||||
|
* Reads from the Linux file <code>/proc/net/dev</code> to retrieve
|
||||||
|
* the number of bytes received (RX) and transmitted (TX) per network interface.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public interface INetworkInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current network usage for all network interfaces.
|
||||||
|
*
|
||||||
|
* @return a map where the key is the interface name (e.g., "eth0") and
|
||||||
|
* the value is a long array of size 2:
|
||||||
|
* <ul>
|
||||||
|
* <li>index 0: bytes received (RX)</li>
|
||||||
|
* <li>index 1: bytes transmitted (TX)</li>
|
||||||
|
* </ul>
|
||||||
|
* @throws IOException if /proc/net/dev cannot be read
|
||||||
|
*/
|
||||||
|
Map<String, long[]> getNetworkUsage() throws IOException;
|
||||||
|
}
|
||||||
33
src/jtop/Isystem/IPathInfo.java
Normal file
33
src/jtop/Isystem/IPathInfo.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utilities to retrieve process path information.
|
||||||
|
* <p>
|
||||||
|
* Implementations may use OS-specific mechanisms to fetch details about
|
||||||
|
* a running process, including its command (full path) and executable name.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public interface IPathInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the executable for the given process ID.
|
||||||
|
* <p>
|
||||||
|
* For example, if the command is "/usr/bin/java", this should return "java".
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the executable name, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
String getName(long pid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full command path of the executable for the given process ID.
|
||||||
|
* <p>
|
||||||
|
* For example, "/usr/bin/java".
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the full command path, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
String getPath(long pid);
|
||||||
|
}
|
||||||
19
src/jtop/Isystem/ITemperatureInfo.java
Normal file
19
src/jtop/Isystem/ITemperatureInfo.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for providing system temperature information.
|
||||||
|
*/
|
||||||
|
public interface ITemperatureInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a map of all detected temperatures on the system.
|
||||||
|
*
|
||||||
|
* @return Map where the key is a sensor name
|
||||||
|
* and the value is the temperature in °C.
|
||||||
|
* @throws IOException if the sensor directories cannot be read
|
||||||
|
*/
|
||||||
|
Map<String, Double> getTemperatures() throws IOException;
|
||||||
|
}
|
||||||
22
src/jtop/Isystem/IUptime.java
Normal file
22
src/jtop/Isystem/IUptime.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package jtop.Isystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for retrieving system uptime information.
|
||||||
|
* <p>
|
||||||
|
* Provides the uptime in various units such as seconds, minutes, hours, or days.
|
||||||
|
*/
|
||||||
|
public interface IUptime {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the system uptime in the specified format.
|
||||||
|
*
|
||||||
|
* @param timeFormat a character indicating the desired unit:
|
||||||
|
* 's' for seconds,
|
||||||
|
* 'm' for minutes,
|
||||||
|
* 'h' for hours,
|
||||||
|
* 'd' for days.
|
||||||
|
* @return the system uptime in the requested unit.
|
||||||
|
* @throws Exception if reading /proc/uptime fails or if the timeFormat is invalid.
|
||||||
|
*/
|
||||||
|
double getSystemUptime(char timeFormat) throws Exception;
|
||||||
|
}
|
||||||
23
src/jtop/Main.java
Normal file
23
src/jtop/Main.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package jtop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the jtop system monitoring application.
|
||||||
|
* <p>
|
||||||
|
* This class initializes the application and starts the main execution loop.
|
||||||
|
* All setup and execution logic is delegated to {@link App}.
|
||||||
|
*/
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the jtop application.
|
||||||
|
* <p>
|
||||||
|
* Initializes all necessary components, including process monitoring,
|
||||||
|
* terminal rendering, and input handling.
|
||||||
|
*
|
||||||
|
* @param args Command-line arguments (currently ignored)
|
||||||
|
* @throws Exception If system information cannot be read or if thread operations fail
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new App().run();
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/jtop/config/Config.java
Normal file
141
src/jtop/config/Config.java
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package jtop.config;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents configuration options for the application.
|
||||||
|
* <p>
|
||||||
|
* Handles reading, parsing, and providing configuration values
|
||||||
|
* such as refresh intervals, color modes, sorting options, and lists of items.
|
||||||
|
* Supports inline comments and ANSI escape sequences in configuration values.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class Config {
|
||||||
|
|
||||||
|
/** Stores all loaded configuration properties. */
|
||||||
|
private final Properties properties = new Properties();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code Config} instance using the default configuration file.
|
||||||
|
* The default path is {@code config/default.conf}.
|
||||||
|
* <p>
|
||||||
|
* If the file cannot be loaded, an error message is printed to {@code stderr}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public Config() {
|
||||||
|
String filePath = "config/default.conf";
|
||||||
|
try (FileReader reader = new FileReader(filePath)) {
|
||||||
|
properties.load(reader);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Could not load config file: " + filePath);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code Config} instance using a custom configuration file path.
|
||||||
|
*
|
||||||
|
* @param filePath the path to the configuration file
|
||||||
|
*/
|
||||||
|
public Config(String filePath) {
|
||||||
|
try (FileReader reader = new FileReader(filePath)) {
|
||||||
|
properties.load(reader);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Could not load config file: " + filePath);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans and normalizes a raw configuration value.
|
||||||
|
* <p>
|
||||||
|
* Cleaning includes:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Removing inline comments starting with '#'</li>
|
||||||
|
* <li>Removing quotes and '+' signs</li>
|
||||||
|
* <li>Replacing escape sequences (033 or \033) with ANSI escape character</li>
|
||||||
|
* <li>Trimming whitespace and spaces immediately after escape codes</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param value the raw configuration string
|
||||||
|
* @return the cleaned value, or {@code null} if the input was {@code null}
|
||||||
|
*/
|
||||||
|
private String cleanValue(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
|
||||||
|
int commentIndex = value.indexOf('#');
|
||||||
|
if (commentIndex != -1) {
|
||||||
|
value = value.substring(0, commentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.replace("\"", "").replace("+", "");
|
||||||
|
value = value.replaceAll("\\\\?033", "\u001B");
|
||||||
|
value = value.replaceAll("(\u001B\\[[0-9;]*m)\\s+", "$1");
|
||||||
|
return value.strip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a configuration value as a string.
|
||||||
|
*
|
||||||
|
* @param key the configuration key
|
||||||
|
* @param defaultValue the value to return if the key is missing or empty
|
||||||
|
* @return the string value associated with the key, or {@code defaultValue} if not found
|
||||||
|
*/
|
||||||
|
public String getString(String key, String defaultValue) {
|
||||||
|
String value = cleanValue(properties.getProperty(key, defaultValue));
|
||||||
|
return (value != null) ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a configuration value as an integer.
|
||||||
|
*
|
||||||
|
* @param key the configuration key
|
||||||
|
* @param defaultValue the value to return if the key is missing, invalid, or unparsable
|
||||||
|
* @return the integer value associated with the key, or {@code defaultValue} if not found or invalid
|
||||||
|
*/
|
||||||
|
public int getInt(String key, int defaultValue) {
|
||||||
|
String value = cleanValue(properties.getProperty(key));
|
||||||
|
if (value != null) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println("Invalid int for key " + key + ": " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a configuration value as a boolean.
|
||||||
|
*
|
||||||
|
* @param key the configuration key
|
||||||
|
* @param defaultValue the value to return if the key is missing
|
||||||
|
* @return the boolean value associated with the key, or {@code defaultValue} if not found
|
||||||
|
*/
|
||||||
|
public boolean getBoolean(String key, boolean defaultValue) {
|
||||||
|
String value = cleanValue(properties.getProperty(key));
|
||||||
|
return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a configuration value as a list of strings.
|
||||||
|
*
|
||||||
|
* @param key the configuration key
|
||||||
|
* @param separator the string used to split the value into multiple elements
|
||||||
|
* @param defaultValue the value to return if the key is missing
|
||||||
|
* @return a list of trimmed string values, or {@code defaultValue} if not found
|
||||||
|
*/
|
||||||
|
public List<String> getList(String key, String separator, List<String> defaultValue) {
|
||||||
|
String value = cleanValue(properties.getProperty(key));
|
||||||
|
if (value != null) {
|
||||||
|
String[] parts = value.split(separator);
|
||||||
|
for (int i = 0; i < parts.length; i++) parts[i] = parts[i].strip();
|
||||||
|
return Arrays.asList(parts);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/jtop/core/IRefreshable.java
Normal file
11
src/jtop/core/IRefreshable.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package jtop.core;
|
||||||
|
/**
|
||||||
|
* Interface for objects that can be refreshed periodically.
|
||||||
|
*/
|
||||||
|
public interface IRefreshable {
|
||||||
|
/**
|
||||||
|
* Refresh the object.
|
||||||
|
* Implementations should update internal state or display as needed.
|
||||||
|
*/
|
||||||
|
void refresh();
|
||||||
|
}
|
||||||
9
src/jtop/core/InfoType.java
Normal file
9
src/jtop/core/InfoType.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package jtop.core;
|
||||||
|
/**
|
||||||
|
* Enum that defines different system information types
|
||||||
|
* (e.g., CPU, Memory, Disk, Network).
|
||||||
|
* Used for switching between data displays or processing logic.
|
||||||
|
*/
|
||||||
|
public enum InfoType {
|
||||||
|
PID, NAME, PATH, USER, CPU, MEMORY, DISK_READ, DISK_WRITE, NETWORK
|
||||||
|
}
|
||||||
47
src/jtop/core/ProcessRow.java
Normal file
47
src/jtop/core/ProcessRow.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package jtop.core;
|
||||||
|
/**
|
||||||
|
* Represents a single process entry in the system.
|
||||||
|
* <p>
|
||||||
|
* Holds basic information about a process including its ID, name, executable path,
|
||||||
|
* owner, CPU usage, and memory usage.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class ProcessRow {
|
||||||
|
|
||||||
|
/** Process ID (PID) */
|
||||||
|
public long pid;
|
||||||
|
|
||||||
|
/** Name of the executable (e.g., "java") */
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
/** Full path of the executable (e.g., "/usr/bin/java") */
|
||||||
|
public String path;
|
||||||
|
|
||||||
|
/** User or owner of the process */
|
||||||
|
public String user;
|
||||||
|
|
||||||
|
/** CPU usage as a percentage string (e.g., "12.5") */
|
||||||
|
public String cpu;
|
||||||
|
|
||||||
|
/** Memory usage as a percentage string (e.g., "8.3") */
|
||||||
|
public String memory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a ProcessRow instance.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @param name the process executable name
|
||||||
|
* @param path the full path to the process executable
|
||||||
|
* @param user the owner of the process
|
||||||
|
* @param cpu the CPU usage as a string
|
||||||
|
* @param memory the memory usage as a string
|
||||||
|
*/
|
||||||
|
public ProcessRow(long pid, String name, String path, String user, String cpu, String memory) {
|
||||||
|
this.pid = pid;
|
||||||
|
this.name = name;
|
||||||
|
this.path = path;
|
||||||
|
this.user = user;
|
||||||
|
this.cpu = cpu;
|
||||||
|
this.memory = memory;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/jtop/core/ProcessSorter.java
Normal file
96
src/jtop/core/ProcessSorter.java
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package jtop.core;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import jtop.Isystem.ICpuInfo;
|
||||||
|
import jtop.Isystem.IMemoryInfo;
|
||||||
|
import jtop.Isystem.IPathInfo;
|
||||||
|
import jtop.system.Feature;
|
||||||
|
import jtop.system.SystemInfoFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides sorting utilities for processes.
|
||||||
|
* <p>
|
||||||
|
* Generates comparators to sort {@link ProcessHandle} instances based on
|
||||||
|
* PID, name, path, user, CPU usage, or memory usage. Supports ascending
|
||||||
|
* and descending order.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class ProcessSorter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a comparator for processes based on the specified sort type.
|
||||||
|
*
|
||||||
|
* @param sortBy the {@link InfoType} to sort by (PID, NAME, CPU, MEMORY, etc.)
|
||||||
|
* @param ascending true for ascending order, false for descending
|
||||||
|
* @return a {@link Comparator} for {@link ProcessHandle}
|
||||||
|
*/
|
||||||
|
public static Comparator<ProcessHandle> getComparator(InfoType sortBy, boolean ascending) {
|
||||||
|
// create interface instances from factory
|
||||||
|
Optional<IPathInfo> pathOpt = SystemInfoFactory.getFeature(Feature.PROCESS);
|
||||||
|
Optional<ICpuInfo> cpuOpt = SystemInfoFactory.getFeature(Feature.CPU);
|
||||||
|
Optional<IMemoryInfo> memOpt = SystemInfoFactory.getFeature(Feature.MEMORY);
|
||||||
|
|
||||||
|
return (a, b) -> {
|
||||||
|
int cmp = 0;
|
||||||
|
try {
|
||||||
|
switch (sortBy) {
|
||||||
|
case PID -> cmp = Long.compare(a.pid(), b.pid());
|
||||||
|
case NAME -> cmp = safeCompare(
|
||||||
|
pathOpt.map(p -> p.getName(a.pid())).orElse(""),
|
||||||
|
pathOpt.map(p -> p.getName(b.pid())).orElse("")
|
||||||
|
);
|
||||||
|
case PATH -> cmp = safeCompare(
|
||||||
|
pathOpt.map(p -> p.getPath(a.pid())).orElse(""),
|
||||||
|
pathOpt.map(p -> p.getPath(b.pid())).orElse("")
|
||||||
|
);
|
||||||
|
case USER -> cmp = safeCompare(
|
||||||
|
a.info().user().orElse(""),
|
||||||
|
b.info().user().orElse("")
|
||||||
|
);
|
||||||
|
case CPU -> cmp = Double.compare(
|
||||||
|
cpuOpt.map(c -> safeCpu(c, a.pid())).orElse(0.0),
|
||||||
|
cpuOpt.map(c -> safeCpu(c, b.pid())).orElse(0.0)
|
||||||
|
);
|
||||||
|
case MEMORY -> cmp = Double.compare(
|
||||||
|
memOpt.map(m -> safeMemory(m, a.pid())).orElse(0.0),
|
||||||
|
memOpt.map(m -> safeMemory(m, b.pid())).orElse(0.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
default -> cmp = 0;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
return ascending ? cmp : -cmp;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two strings in a case-insensitive manner, treating null as empty.
|
||||||
|
*
|
||||||
|
* @param a first string
|
||||||
|
* @param b second string
|
||||||
|
* @return comparison result
|
||||||
|
*/
|
||||||
|
private static int safeCompare(String a, String b) {
|
||||||
|
if (a == null) a = "";
|
||||||
|
if (b == null) b = "";
|
||||||
|
return a.compareToIgnoreCase(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double safeMemory(IMemoryInfo mem, long pid) {
|
||||||
|
try {
|
||||||
|
return mem.getMemoryPercent(pid);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double safeCpu(ICpuInfo cpu, long pid) {
|
||||||
|
try {
|
||||||
|
return cpu.getCpuPercent(pid);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/jtop/core/ProcessState.java
Normal file
68
src/jtop/core/ProcessState.java
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package jtop.core;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for retrieving and interpreting a process's current state.
|
||||||
|
* <p>
|
||||||
|
* Reads the process status from <code>/proc/[pid]/stat</code> and maps
|
||||||
|
* the one-letter state code to a human-readable description.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Typical Linux process states:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>R</b> - Running</li>
|
||||||
|
* <li><b>S</b> - Sleeping</li>
|
||||||
|
* <li><b>D</b> - Disk Sleep</li>
|
||||||
|
* <li><b>T</b> - Stopped</li>
|
||||||
|
* <li><b>Z</b> - Zombie</li>
|
||||||
|
* <li><b>X</b> - Dead</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class ProcessState {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current state of the specified process.
|
||||||
|
* <p>
|
||||||
|
* Reads <code>/proc/[pid]/stat</code> and extracts the third field,
|
||||||
|
* which represents the process state as a single-character code.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID whose state should be retrieved
|
||||||
|
* @return a human-readable description of the process state, or <code>"?"</code> if unavailable
|
||||||
|
*/
|
||||||
|
public static String getState(long pid) {
|
||||||
|
String path = "/proc/" + pid + "/stat";
|
||||||
|
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
|
||||||
|
String[] parts = reader.readLine().split("\\s+");
|
||||||
|
// Field 3 is the process state (R, S, D, T, Z, etc.)
|
||||||
|
if (parts.length > 2) {
|
||||||
|
return parseState(parts[2]);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the short one-letter state code from <code>/proc/[pid]/stat</code>
|
||||||
|
* into a descriptive string.
|
||||||
|
*
|
||||||
|
* @param s the single-character state code (e.g. "R", "S", "Z")
|
||||||
|
* @return the full human-readable process state
|
||||||
|
*/
|
||||||
|
private static String parseState(String s) {
|
||||||
|
switch (s) {
|
||||||
|
case "R": return "Running";
|
||||||
|
case "S": return "Sleeping";
|
||||||
|
case "D": return "Disk Sleep";
|
||||||
|
case "T": return "Stopped";
|
||||||
|
case "Z": return "Zombie";
|
||||||
|
case "X": return "Dead";
|
||||||
|
default: return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/jtop/core/ProcessTableRenderer.java
Normal file
163
src/jtop/core/ProcessTableRenderer.java
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package jtop.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import jtop.config.Config;
|
||||||
|
import jtop.terminal.Header;
|
||||||
|
import jtop.terminal.TerminalSize;
|
||||||
|
import jtop.system.linux.SystemSampler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for rendering the process table in the terminal.
|
||||||
|
* <p>
|
||||||
|
* This class handles:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Color formatting for header, footer, and table rows.</li>
|
||||||
|
* <li>Column alignment based on terminal width and cell size.</li>
|
||||||
|
* <li>Displaying keybindings and scrolling status.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class ProcessTableRenderer {
|
||||||
|
private final String tableColor;
|
||||||
|
private final String headerColor;
|
||||||
|
private final String footerColor;
|
||||||
|
private final String clearStyling;
|
||||||
|
private final String sortingArrowColor;
|
||||||
|
private static String keyBindings = "";
|
||||||
|
|
||||||
|
private final int cellWidth;
|
||||||
|
private final int pageSize;
|
||||||
|
private final SystemSampler sampler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the table renderer with configuration and layout settings.
|
||||||
|
*
|
||||||
|
* @param config configuration object containing color settings and footer text
|
||||||
|
* @param cellWidth width of each column in characters
|
||||||
|
* @param pageSize number of rows visible at a time
|
||||||
|
* @param sampler cached system information (CPU, memory, temps)
|
||||||
|
*/
|
||||||
|
public ProcessTableRenderer(Config config, int cellWidth, int pageSize, SystemSampler sampler) {
|
||||||
|
this.tableColor = config.getString("table.color", "\033[40m\033[37m");
|
||||||
|
this.headerColor = config.getString("header.color", "\033[47m\033[30m");
|
||||||
|
this.footerColor = config.getString("footer.color", "\033[41m\033[37m");
|
||||||
|
this.clearStyling = "\033[0m";
|
||||||
|
this.sortingArrowColor = "\033[31m";
|
||||||
|
this.keyBindings = config.getString("footer.text.keybindings",
|
||||||
|
"Use j/k to scroll, Enter to scroll entire row, 'q' or Ctrl+C to quit");
|
||||||
|
this.cellWidth = cellWidth;
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
this.sampler = sampler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of lines used by the header and footer.
|
||||||
|
*
|
||||||
|
* @return the number of lines used by the header and footer
|
||||||
|
*/
|
||||||
|
public static int getHeaderAndFooterLength() {
|
||||||
|
int length = 0;
|
||||||
|
length += Header.getRowsCount(); // header (system information)
|
||||||
|
length += 1; // table header (PID, NAME, etc.)
|
||||||
|
length += "-- Showing abc-def of xyz --".length() / TerminalSize.getColumns() + 1; // footer
|
||||||
|
length += keyBindings.length() / TerminalSize.getColumns() + 1; // keybindings text
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the process table on the terminal.
|
||||||
|
*
|
||||||
|
* @param processes the list of processes to display
|
||||||
|
* @param infoTypes the columns to show (PID, NAME, CPU, etc.)
|
||||||
|
* @param sortBy the column currently used for sorting
|
||||||
|
* @param sortAsc true if sorting ascending, false if descending
|
||||||
|
* @param scrollIndex starting index for visible rows
|
||||||
|
* @param uptime system uptime in hours (cached)
|
||||||
|
* @param load system load average (cached)
|
||||||
|
*/
|
||||||
|
public void draw(List<ProcessRow> processes, List<InfoType> infoTypes, InfoType sortBy, boolean sortAsc, int scrollIndex,
|
||||||
|
double uptime, String load) {
|
||||||
|
TerminalSize terminalSize = new TerminalSize();
|
||||||
|
int total = processes.size();
|
||||||
|
int end = Math.min(scrollIndex + pageSize, total);
|
||||||
|
|
||||||
|
// Clear screen
|
||||||
|
System.out.print("\033[H\033[2J");
|
||||||
|
System.out.flush();
|
||||||
|
|
||||||
|
// Draw header with cached SystemSampler
|
||||||
|
Header.draw(sampler, uptime, load);
|
||||||
|
|
||||||
|
// Print table header
|
||||||
|
printHeader(infoTypes, sortBy, sortAsc);
|
||||||
|
|
||||||
|
// Print visible process rows
|
||||||
|
for (int i = scrollIndex; i < end; i++) {
|
||||||
|
printProcessRow(processes.get(i), infoTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print footer
|
||||||
|
String spaces = " ".repeat(Math.max(0, (terminalSize.getColumns() - 25) / 2));
|
||||||
|
System.out.printf("\r%s%s-- Showing %d-%d of %d --%s\n",
|
||||||
|
spaces, footerColor, scrollIndex + 1, end, total, clearStyling);
|
||||||
|
System.out.print("\r" + keyBindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints the table header with sorting indicators.
|
||||||
|
*/
|
||||||
|
private void printHeader(List<InfoType> infoTypes, InfoType sortBy, boolean sortAsc) {
|
||||||
|
List<String> headers = new ArrayList<>();
|
||||||
|
for (InfoType type : infoTypes) {
|
||||||
|
String name = type.name();
|
||||||
|
if (type == InfoType.CPU || type == InfoType.MEMORY) name += " %";
|
||||||
|
if (type == sortBy) name += sortAsc ? " ^" : " v";
|
||||||
|
headers.add(name);
|
||||||
|
}
|
||||||
|
printRow(headerColor, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a single row of process data.
|
||||||
|
*/
|
||||||
|
private void printProcessRow(ProcessRow row, List<InfoType> infoTypes) {
|
||||||
|
List<String> cells = new ArrayList<>();
|
||||||
|
for (InfoType type : infoTypes) {
|
||||||
|
switch (type) {
|
||||||
|
case PID -> cells.add(String.valueOf(row.pid));
|
||||||
|
case NAME -> cells.add(row.name);
|
||||||
|
case PATH -> cells.add(row.path);
|
||||||
|
case USER -> cells.add(row.user);
|
||||||
|
case CPU -> cells.add(row.cpu);
|
||||||
|
case MEMORY -> cells.add(row.memory);
|
||||||
|
case DISK_READ -> cells.add("TODO_R");
|
||||||
|
case DISK_WRITE -> cells.add("TODO_W");
|
||||||
|
case NETWORK -> cells.add("TODO_NET");
|
||||||
|
default -> cells.add("?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printRow("", cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a row with the given color and cells.
|
||||||
|
*/
|
||||||
|
private void printRow(String color, List<String> cells) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String c : cells) {
|
||||||
|
sb.append(String.format("%-" + cellWidth + "s", truncate(c, cellWidth)));
|
||||||
|
}
|
||||||
|
System.out.println("\r" + tableColor + color + sb + clearStyling);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates a string to the given width.
|
||||||
|
*/
|
||||||
|
private String truncate(String s, int width) {
|
||||||
|
if (s == null) return "";
|
||||||
|
if (s.length() > width - 1) return s.substring(0, width - 1);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/jtop/core/RefreshThread.java
Normal file
52
src/jtop/core/RefreshThread.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package jtop.core;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background thread that periodically refreshes a {@link IRefreshable} component.
|
||||||
|
* <p>
|
||||||
|
* The thread wakes up at a fixed interval (2 seconds) and calls {@link IRefreshable#refresh()}.
|
||||||
|
* Refreshing only occurs if the {@link AtomicBoolean} flag is set to {@code true}.
|
||||||
|
* <p>
|
||||||
|
* This thread runs as a daemon, allowing the application to exit gracefully.
|
||||||
|
*/
|
||||||
|
public class RefreshThread extends Thread {
|
||||||
|
private final IRefreshable refreshable;
|
||||||
|
private final AtomicBoolean refresh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new RefreshThread.
|
||||||
|
*
|
||||||
|
* @param refreshable the component to refresh periodically
|
||||||
|
* @param refresh atomic boolean flag controlling whether a refresh should occur
|
||||||
|
*/
|
||||||
|
public RefreshThread(IRefreshable refreshable, AtomicBoolean refresh) {
|
||||||
|
this.refreshable = refreshable;
|
||||||
|
this.refresh = refresh;
|
||||||
|
setDaemon(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main loop of the thread.
|
||||||
|
* <p>
|
||||||
|
* Sleeps for 2 seconds between updates and refreshes the target object
|
||||||
|
* if the {@code refresh} flag is set to {@code true}.
|
||||||
|
* <p>
|
||||||
|
* Exits cleanly when interrupted.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
if (refresh.get()) {
|
||||||
|
refreshable.refresh();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Stop thread on interrupt
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log or ignore other exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/jtop/core/ShowProcesses.java
Normal file
167
src/jtop/core/ShowProcesses.java
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package jtop.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jtop.Isystem.ICpuInfo;
|
||||||
|
import jtop.Isystem.IMemoryInfo;
|
||||||
|
import jtop.Isystem.IPathInfo;
|
||||||
|
import jtop.Isystem.IUptime;
|
||||||
|
import jtop.Isystem.ITemperatureInfo;
|
||||||
|
import jtop.config.Config;
|
||||||
|
import jtop.terminal.TerminalSize;
|
||||||
|
import jtop.system.Feature;
|
||||||
|
import jtop.system.SystemInfoFactory;
|
||||||
|
import jtop.system.linux.SystemSampler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core class responsible for managing, sorting, and displaying running processes.
|
||||||
|
*/
|
||||||
|
public class ShowProcesses implements IRefreshable {
|
||||||
|
private final List<InfoType> infoTypes;
|
||||||
|
private final Config config = new Config();
|
||||||
|
|
||||||
|
private InfoType sortBy = InfoType.CPU;
|
||||||
|
private boolean sortAsc = config.getBoolean("table.sorting.ASC", false);
|
||||||
|
|
||||||
|
private int scrollIndex = 0;
|
||||||
|
private int pageSize;
|
||||||
|
private int cellWidth;
|
||||||
|
|
||||||
|
private List<ProcessRow> cachedProcesses = new ArrayList<>();
|
||||||
|
|
||||||
|
// system sampler for cached CPU, memory, temps
|
||||||
|
private final SystemSampler sampler = new SystemSampler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a ShowProcesses instance with the specified columns to display.
|
||||||
|
*/
|
||||||
|
public ShowProcesses(InfoType... infos) {
|
||||||
|
infoTypes = List.of(infos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the cached list of process rows and system sampler.
|
||||||
|
*/
|
||||||
|
public void refreshProcesses() throws Exception {
|
||||||
|
// Fetch system features
|
||||||
|
IUptime uptimeInfo = SystemInfoFactory.getFeature(Feature.UPTIME).map(f -> (IUptime) f).orElse(null);
|
||||||
|
ICpuInfo cpuInfo = SystemInfoFactory.getFeature(Feature.CPU).map(f -> (ICpuInfo) f).orElse(null);
|
||||||
|
IMemoryInfo memoryInfo = SystemInfoFactory.getFeature(Feature.MEMORY).map(f -> (IMemoryInfo) f).orElse(null);
|
||||||
|
ITemperatureInfo tempInfo = SystemInfoFactory.getFeature(Feature.TEMPERATURE).map(f -> (ITemperatureInfo) f).orElse(null);
|
||||||
|
IPathInfo pathInfo = SystemInfoFactory.getFeature(Feature.PROCESS).map(f -> (IPathInfo) f).orElse(null);
|
||||||
|
|
||||||
|
if (pathInfo instanceof jtop.system.linux.PathInfo pi) {
|
||||||
|
pi.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update system sampler
|
||||||
|
sampler.refresh(cpuInfo, memoryInfo, tempInfo);
|
||||||
|
|
||||||
|
// cache memory usage per process
|
||||||
|
Map<Long, Double> memCache = memoryInfo != null ? new HashMap<>() : null;
|
||||||
|
|
||||||
|
// fetch all processes and sort
|
||||||
|
List<ProcessHandle> processes = new ArrayList<>(ProcessHandle.allProcesses().toList());
|
||||||
|
processes.sort(ProcessSorter.getComparator(sortBy, sortAsc));
|
||||||
|
|
||||||
|
List<ProcessRow> rows = new ArrayList<>(processes.size());
|
||||||
|
|
||||||
|
for (ProcessHandle ph : processes) {
|
||||||
|
long pid = ph.pid();
|
||||||
|
try {
|
||||||
|
String name = pathInfo != null ? safe(pathInfo.getName(pid)) : "?";
|
||||||
|
String path = pathInfo != null ? safe(pathInfo.getPath(pid)) : "?";
|
||||||
|
String user = ph.info().user().orElse("Unknown");
|
||||||
|
|
||||||
|
String cpuPercent = cpuInfo != null ? String.valueOf(safeCpu(cpuInfo, pid)) : "?";
|
||||||
|
|
||||||
|
String memPercent;
|
||||||
|
if (memoryInfo != null) {
|
||||||
|
Double cached = memCache.get(pid);
|
||||||
|
if (cached == null) {
|
||||||
|
double val = safeMemory(memoryInfo, pid);
|
||||||
|
memCache.put(pid, val);
|
||||||
|
memPercent = String.valueOf(val);
|
||||||
|
} else {
|
||||||
|
memPercent = String.valueOf(cached);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memPercent = "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.add(new ProcessRow(pid, name, path, user, cpuPercent, memPercent));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedProcesses = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the process table to the terminal using cached system sampler.
|
||||||
|
*/
|
||||||
|
public void draw() throws Exception {
|
||||||
|
TerminalSize terminalSize = new TerminalSize();
|
||||||
|
this.pageSize = terminalSize.getRows() - ProcessTableRenderer.getHeaderAndFooterLength();
|
||||||
|
this.cellWidth = terminalSize.getColumns() / infoTypes.size();
|
||||||
|
|
||||||
|
if (cachedProcesses.isEmpty()) {
|
||||||
|
refreshProcesses();
|
||||||
|
}
|
||||||
|
|
||||||
|
double uptime = 0.0;
|
||||||
|
String load = "?";
|
||||||
|
|
||||||
|
try {
|
||||||
|
IUptime uptimeInfo = SystemInfoFactory.getFeature(Feature.UPTIME).map(f -> (IUptime) f).orElse(null);
|
||||||
|
ICpuInfo cpuInfo = SystemInfoFactory.getFeature(Feature.CPU).map(f -> (ICpuInfo) f).orElse(null);
|
||||||
|
if (uptimeInfo != null) uptime = uptimeInfo.getSystemUptime('h');
|
||||||
|
if (cpuInfo != null) load = cpuInfo.getLoadAverage();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
new ProcessTableRenderer(config, cellWidth, pageSize, sampler)
|
||||||
|
.draw(cachedProcesses, infoTypes, sortBy, sortAsc, scrollIndex, uptime, load);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scrollUp() { if (scrollIndex > 0) scrollIndex--; }
|
||||||
|
|
||||||
|
public void scrollDown() {
|
||||||
|
if (scrollIndex + pageSize < cachedProcesses.size()) scrollIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeSortByClick(int charPosition) throws Exception {
|
||||||
|
int columnIndex = charPosition / cellWidth;
|
||||||
|
changeSort(columnIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeSort(int columnIndex) throws Exception {
|
||||||
|
if (columnIndex >= 0 && columnIndex < infoTypes.size()) {
|
||||||
|
InfoType newSort = infoTypes.get(columnIndex);
|
||||||
|
sortAsc = (sortBy == newSort) ? !sortAsc : true;
|
||||||
|
sortBy = newSort;
|
||||||
|
refreshProcesses();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safe(String s) { return s != null ? s : "?"; }
|
||||||
|
|
||||||
|
private static double safeCpu(ICpuInfo cpu, long pid) {
|
||||||
|
try { return cpu.getCpuPercent(pid); } catch (Exception e) { return 0.0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double safeMemory(IMemoryInfo mem, long pid) {
|
||||||
|
try { return mem.getMemoryPercent(pid); } catch (Exception e) { return 0.0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refresh() {
|
||||||
|
try {
|
||||||
|
refreshProcesses();
|
||||||
|
draw();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/jtop/system/Feature.java
Normal file
40
src/jtop/system/Feature.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package jtop.system;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a system feature that can be dynamically implemented for different operating systems.
|
||||||
|
* <p>
|
||||||
|
* Each feature stores the name of its default implementation class, which is used
|
||||||
|
* by {@link SystemInfoFactory} to instantiate the appropriate OS-specific implementation.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public enum Feature {
|
||||||
|
CPU("CpuInfo"),
|
||||||
|
MEMORY("MemoryInfo"),
|
||||||
|
DISK("DiskInfo"),
|
||||||
|
NETWORK("NetworkInfo"),
|
||||||
|
TEMPERATURE("TemperatureInfo"),
|
||||||
|
BATTERY("BatteryInfo"),
|
||||||
|
UPTIME("Uptime"),
|
||||||
|
PROCESS("PathInfo");
|
||||||
|
|
||||||
|
/** Name of the implementation class for this feature. */
|
||||||
|
private final String implClassName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a feature with its associated implementation class name.
|
||||||
|
*
|
||||||
|
* @param implClassName the default class name implementing this feature
|
||||||
|
*/
|
||||||
|
Feature(String implClassName) {
|
||||||
|
this.implClassName = implClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the implementation class name associated with this feature.
|
||||||
|
*
|
||||||
|
* @return the class name of the implementation
|
||||||
|
*/
|
||||||
|
public String getImplementationClassName() {
|
||||||
|
return implClassName;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/jtop/system/FeatureResolver.java
Normal file
36
src/jtop/system/FeatureResolver.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package jtop.system;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
import jtop.system.linux.LinuxFeatures;
|
||||||
|
import jtop.system.freebsd.FreeBsdFeatures;
|
||||||
|
import jtop.system.mac.MacFeatures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves which system features are supported on a given operating system.
|
||||||
|
* <p>
|
||||||
|
* Provides a centralized way to query the supported {@link Feature}s for
|
||||||
|
* Linux, FreeBSD, and macOS without hardcoding OS-specific logic elsewhere.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class FeatureResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation.
|
||||||
|
*/
|
||||||
|
private FeatureResolver() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of features supported by the given operating system.
|
||||||
|
*
|
||||||
|
* @param os the {@link OperatingSystem} to query
|
||||||
|
* @return an {@link EnumSet} of {@link Feature} representing supported features
|
||||||
|
*/
|
||||||
|
public static EnumSet<Feature> supported(OperatingSystem os) {
|
||||||
|
return switch (os) {
|
||||||
|
case LINUX -> LinuxFeatures.SUPPORTED;
|
||||||
|
case FREEBSD -> FreeBsdFeatures.SUPPORTED;
|
||||||
|
case MAC -> MacFeatures.SUPPORTED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/jtop/system/OperatingSystem.java
Normal file
36
src/jtop/system/OperatingSystem.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package jtop.system;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing supported operating systems.
|
||||||
|
* <p>
|
||||||
|
* Provides a method to detect the current operating system at runtime.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public enum OperatingSystem {
|
||||||
|
/** Linux operating system. */
|
||||||
|
LINUX,
|
||||||
|
/** FreeBSD operating system. */
|
||||||
|
FREEBSD,
|
||||||
|
/** macOS operating system. */
|
||||||
|
MAC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the current operating system.
|
||||||
|
* <p>
|
||||||
|
* Uses the system property {@code os.name} to determine the OS.
|
||||||
|
* Returns the corresponding {@link OperatingSystem} enum value.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the detected {@link OperatingSystem}
|
||||||
|
* @throws UnsupportedOperationException if the OS is not supported
|
||||||
|
*/
|
||||||
|
public static OperatingSystem detect() {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
|
||||||
|
if (os.contains("linux")) return LINUX;
|
||||||
|
if (os.contains("freebsd")) return FREEBSD;
|
||||||
|
if (os.contains("mac")) return MAC;
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("Unsupported OS: " + os);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/jtop/system/SystemInfoFactory.java
Normal file
61
src/jtop/system/SystemInfoFactory.java
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package jtop.system;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to provide system information implementations dynamically
|
||||||
|
* based on the current OS and available features.
|
||||||
|
* <p>
|
||||||
|
* Fully enum-driven: no need to manually add getters for each feature.
|
||||||
|
*/
|
||||||
|
public final class SystemInfoFactory {
|
||||||
|
|
||||||
|
private static final OperatingSystem OS = OperatingSystem.detect();
|
||||||
|
private static final Set<Feature> SUPPORTED_FEATURES = FeatureResolver.supported(OS);
|
||||||
|
|
||||||
|
private SystemInfoFactory() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an implementation of the requested feature if available for this OS.
|
||||||
|
*
|
||||||
|
* @param feature the feature to request
|
||||||
|
* @return Optional containing the implementation, empty if not supported
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> Optional<T> getFeature(Feature feature) {
|
||||||
|
if (!SUPPORTED_FEATURES.contains(feature)) return Optional.empty();
|
||||||
|
|
||||||
|
String className = String.format(
|
||||||
|
"jtop.system.%s.%s",
|
||||||
|
OS.name().toLowerCase(),
|
||||||
|
feature.getImplementationClassName()
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> clazz = Class.forName(className);
|
||||||
|
return Optional.of((T) clazz.getDeclaredConstructor().newInstance());
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to load " + className + ": " + e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all supported features for this OS.
|
||||||
|
* Can be used to dynamically display available features in UI or CLI.
|
||||||
|
*/
|
||||||
|
public static Set<Feature> supportedFeatures() {
|
||||||
|
return Collections.unmodifiableSet(SUPPORTED_FEATURES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a map of all available features to their implementations.
|
||||||
|
* Features not supported on this OS are skipped.
|
||||||
|
*/
|
||||||
|
public static Map<Feature, Object> allAvailableFeatures() {
|
||||||
|
return SUPPORTED_FEATURES.stream()
|
||||||
|
.flatMap(f -> getFeature(f).map(inst -> Map.entry(f, inst)).stream())
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/jtop/system/freebsd/FreeBsdFeatures.java
Normal file
28
src/jtop/system/freebsd/FreeBsdFeatures.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package jtop.system.freebsd;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import jtop.system.Feature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the set of system features supported on FreeBSD.
|
||||||
|
* <p>
|
||||||
|
* Each operating system has its own feature class (e.g., {@link jtop.system.linux.LinuxFeatures})
|
||||||
|
* that lists which features are implemented and available.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class FreeBsdFeatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of features currently supported on FreeBSD.
|
||||||
|
* <p>
|
||||||
|
* This is used by {@link jtop.system.FeatureResolver} to determine at runtime
|
||||||
|
* which features can be instantiated via {@link jtop.system.SystemInfoFactory}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final EnumSet<Feature> SUPPORTED = EnumSet.of(
|
||||||
|
Feature.PROCESS
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Private constructor to prevent instantiation of this utility class. */
|
||||||
|
private FreeBsdFeatures() {}
|
||||||
|
}
|
||||||
53
src/jtop/system/freebsd/PathInfo.java
Normal file
53
src/jtop/system/freebsd/PathInfo.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package jtop.system.freebsd;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import jtop.Isystem.IPathInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utilities to retrieve process path information.
|
||||||
|
* <p>
|
||||||
|
* Uses {@link ProcessHandle} to fetch details about a running process,
|
||||||
|
* including its command (full path) and executable name.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class PathInfo implements IPathInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the executable for the given process ID.
|
||||||
|
* <p>
|
||||||
|
* For example, if the command is "/usr/bin/java", this will return "java".
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the executable name, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName(long pid) {
|
||||||
|
Optional<ProcessHandle> ph = ProcessHandle.of(pid);
|
||||||
|
if (ph.isPresent()) {
|
||||||
|
ProcessHandle.Info info = ph.get().info();
|
||||||
|
String command = info.command().orElse("Unknown");
|
||||||
|
return command.substring(command.lastIndexOf("/") + 1);
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full command path of the executable for the given process ID.
|
||||||
|
* <p>
|
||||||
|
* For example, "/usr/bin/java".
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the full command path, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getPath(long pid) {
|
||||||
|
Optional<ProcessHandle> ph = ProcessHandle.of(pid);
|
||||||
|
if (ph.isPresent()) {
|
||||||
|
ProcessHandle.Info info = ph.get().info();
|
||||||
|
return info.command().orElse("Unknown");
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/jtop/system/linux/BatteryInfo.java
Normal file
160
src/jtop/system/linux/BatteryInfo.java
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.Optional;
|
||||||
|
import jtop.Isystem.IBatteryInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for retrieving battery information on Linux systems.
|
||||||
|
* <p>
|
||||||
|
* Automatically detects the battery directory under
|
||||||
|
* <code>/sys/class/power_supply/</code> (e.g. BAT0, BAT1)
|
||||||
|
* and exposes percentage, status, voltage, energy, and power readings.
|
||||||
|
* <p>
|
||||||
|
* Implements {@link IBatteryInfo} so it can be used in a cross-platform interface-based design.
|
||||||
|
*/
|
||||||
|
public class BatteryInfo implements IBatteryInfo {
|
||||||
|
|
||||||
|
/** Path to the power supply directory on Linux */
|
||||||
|
private static final Path POWER_SUPPLY_PATH = Path.of("/sys/class/power_supply");
|
||||||
|
|
||||||
|
/** Path to the detected battery directory (e.g., BAT0) */
|
||||||
|
private Path batteryPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a BatteryInfo instance and tries to detect the battery path.
|
||||||
|
*/
|
||||||
|
public BatteryInfo() {
|
||||||
|
batteryPath = detectBatteryPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the first available battery path under /sys/class/power_supply/.
|
||||||
|
*
|
||||||
|
* @return Path to the battery directory, or null if not found
|
||||||
|
*/
|
||||||
|
private Path detectBatteryPath() {
|
||||||
|
if (!Files.isDirectory(POWER_SUPPLY_PATH)) return null;
|
||||||
|
|
||||||
|
try (var stream = Files.list(POWER_SUPPLY_PATH)) {
|
||||||
|
Optional<Path> battery = stream
|
||||||
|
.filter(p -> p.getFileName().toString().startsWith("BAT"))
|
||||||
|
.findFirst();
|
||||||
|
return battery.orElse(null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a value from a given file path inside the battery directory.
|
||||||
|
*
|
||||||
|
* @param filename the name of the file to read
|
||||||
|
* @return the file's trimmed string contents, or null if unavailable
|
||||||
|
*/
|
||||||
|
private String readBatteryFile(String filename) {
|
||||||
|
if (batteryPath == null) return null;
|
||||||
|
|
||||||
|
Path file = batteryPath.resolve(filename);
|
||||||
|
if (!Files.exists(file)) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Files.readString(file).trim();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current battery charge percentage (0–100).
|
||||||
|
*
|
||||||
|
* @return battery percentage, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getBatteryPercentage() {
|
||||||
|
String content = readBatteryFile("capacity");
|
||||||
|
if (content == null) return -1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(content);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current battery status (e.g. Charging, Discharging, Full).
|
||||||
|
*
|
||||||
|
* @return status string, or "Unknown" if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getBatteryStatus() {
|
||||||
|
String status = readBatteryFile("status");
|
||||||
|
return status != null ? status : "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current voltage in volts (if available).
|
||||||
|
*
|
||||||
|
* @return voltage in volts, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getVoltage() {
|
||||||
|
String content = readBatteryFile("voltage_now");
|
||||||
|
if (content == null) return -1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// value is usually in microvolts
|
||||||
|
return Double.parseDouble(content) / 1_000_000.0;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current energy in watt-hours (if available).
|
||||||
|
*
|
||||||
|
* @return energy in Wh, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getEnergy() {
|
||||||
|
String content = readBatteryFile("energy_now");
|
||||||
|
if (content == null) return -1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// value is usually in microwatt-hours
|
||||||
|
return Double.parseDouble(content) / 1_000_000.0;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current power draw in watts (if available).
|
||||||
|
*
|
||||||
|
* @return power in W, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getPower() {
|
||||||
|
String content = readBatteryFile("power_now");
|
||||||
|
if (content == null) return -1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// value is usually in microwatts
|
||||||
|
return Double.parseDouble(content) / 1_000_000.0;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the system has a readable battery directory.
|
||||||
|
*
|
||||||
|
* @return true if battery is present and readable, false otherwise
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean hasBattery() {
|
||||||
|
return batteryPath != null && Files.exists(batteryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/jtop/system/linux/CpuInfo.java
Normal file
111
src/jtop/system/linux/CpuInfo.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import jtop.Isystem.ICpuInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides CPU usage information and statistics for the system and individual processes.
|
||||||
|
* <p>
|
||||||
|
* Reads data from the <code>/proc</code> filesystem on Linux:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><code>/proc/[pid]/stat</code> for per-process CPU usage</li>
|
||||||
|
* <li><code>/proc/stat</code> for overall CPU usage</li>
|
||||||
|
* <li><code>/proc/loadavg</code> for system load average</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class CpuInfo implements ICpuInfo {
|
||||||
|
|
||||||
|
/** Number of decimal places to round CPU percentage values. */
|
||||||
|
private static final int DECIMALS = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the CPU usage percentage of a specific process.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return CPU usage as a percentage, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getCpuPercent(long pid) {
|
||||||
|
try {
|
||||||
|
String stat = Files.readString(Paths.get("/proc/" + pid + "/stat"));
|
||||||
|
String[] parts = stat.split("\\s+");
|
||||||
|
|
||||||
|
long utime = Long.parseLong(parts[13]);
|
||||||
|
long stime = Long.parseLong(parts[14]);
|
||||||
|
long totalTime = utime + stime;
|
||||||
|
|
||||||
|
double uptimeSeconds = new Uptime().getSystemUptime('s');
|
||||||
|
|
||||||
|
double percent = (100d * totalTime / uptimeSeconds) / Runtime.getRuntime().availableProcessors();
|
||||||
|
|
||||||
|
double factor = Math.pow(10, DECIMALS);
|
||||||
|
return Math.round(percent * factor) / factor;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the system load average as reported by <code>/proc/loadavg</code>.
|
||||||
|
*
|
||||||
|
* @return the load average string, or null if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getLoadAverage() {
|
||||||
|
try {
|
||||||
|
return Files.readString(Path.of("/proc/loadavg")).trim();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the overall CPU usage percentage over a sample period.
|
||||||
|
*
|
||||||
|
* @param sampleMs the sample duration in milliseconds
|
||||||
|
* @return the CPU usage as a percentage over the sample period, or -1 if unavailable
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getCpuUsage(long sampleMs) {
|
||||||
|
try {
|
||||||
|
long[] first = readCpuStat();
|
||||||
|
Thread.sleep(sampleMs);
|
||||||
|
long[] second = readCpuStat();
|
||||||
|
|
||||||
|
long idle1 = first[3];
|
||||||
|
long idle2 = second[3];
|
||||||
|
long total1 = Arrays.stream(first).sum();
|
||||||
|
long total2 = Arrays.stream(second).sum();
|
||||||
|
|
||||||
|
long totalDelta = total2 - total1;
|
||||||
|
long idleDelta = idle2 - idle1;
|
||||||
|
return 100.0 * (totalDelta - idleDelta) / totalDelta;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the system-wide CPU statistics from <code>/proc/stat</code>.
|
||||||
|
*
|
||||||
|
* @return an array of CPU time values (user, nice, system, idle, etc.), or null if unavailable
|
||||||
|
*/
|
||||||
|
private long[] readCpuStat() {
|
||||||
|
try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/stat"))) {
|
||||||
|
String[] parts = br.readLine().trim().split("\\s+");
|
||||||
|
long[] vals = new long[parts.length - 1];
|
||||||
|
for (int i = 1; i < parts.length; i++) {
|
||||||
|
vals[i - 1] = Long.parseLong(parts[i]);
|
||||||
|
}
|
||||||
|
return vals;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/jtop/system/linux/DiskInfo.java
Normal file
54
src/jtop/system/linux/DiskInfo.java
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jtop.Isystem.IDiskInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides information about disk usage and I/O statistics for mounted devices.
|
||||||
|
* <p>
|
||||||
|
* Reads data from <code>/proc/diskstats</code> and parses the number of
|
||||||
|
* reads and writes for each block device. The results can be used to
|
||||||
|
* monitor disk I/O activity or calculate total/used storage if combined
|
||||||
|
* with filesystem information.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class DiskInfo implements IDiskInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves disk I/O statistics for all block devices.
|
||||||
|
* <p>
|
||||||
|
* Each entry in the returned map contains the device name as the key,
|
||||||
|
* and an array of two long values as the value:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>index 0 - number of reads completed</li>
|
||||||
|
* <li>index 1 - number of writes completed</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return a {@link Map} where the key is the device name and the value is a
|
||||||
|
* long array containing [reads, writes]
|
||||||
|
* @throws IOException if reading <code>/proc/diskstats</code> fails
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, long[]> getDiskStats() throws IOException {
|
||||||
|
Map<String, long[]> map = new LinkedHashMap<>();
|
||||||
|
try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/diskstats"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
String[] parts = line.trim().split("\\s+");
|
||||||
|
if (parts.length < 14) continue; // Skip incomplete lines
|
||||||
|
String device = parts[2];
|
||||||
|
long reads = Long.parseLong(parts[3]);
|
||||||
|
long writes = Long.parseLong(parts[7]);
|
||||||
|
map.put(device, new long[]{reads, writes});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/jtop/system/linux/LinuxFeatures.java
Normal file
35
src/jtop/system/linux/LinuxFeatures.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import jtop.system.Feature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the set of system features supported on Linux.
|
||||||
|
* <p>
|
||||||
|
* Each operating system has its own feature class (e.g., {@link jtop.system.freebsd.FreeBsdFeatures})
|
||||||
|
* that lists which features are implemented and available.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class LinuxFeatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of features currently supported on Linux.
|
||||||
|
* <p>
|
||||||
|
* This is used by {@link jtop.system.FeatureResolver} to determine at runtime
|
||||||
|
* which features can be instantiated via {@link jtop.system.SystemInfoFactory}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final EnumSet<Feature> SUPPORTED = EnumSet.of(
|
||||||
|
Feature.CPU,
|
||||||
|
Feature.MEMORY,
|
||||||
|
Feature.DISK,
|
||||||
|
Feature.NETWORK,
|
||||||
|
Feature.TEMPERATURE,
|
||||||
|
Feature.BATTERY,
|
||||||
|
Feature.UPTIME,
|
||||||
|
Feature.PROCESS
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Private constructor to prevent instantiation of this utility class. */
|
||||||
|
private LinuxFeatures() {}
|
||||||
|
}
|
||||||
178
src/jtop/system/linux/MemoryInfo.java
Normal file
178
src/jtop/system/linux/MemoryInfo.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import jtop.Isystem.IMemoryInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to gather memory usage statistics.
|
||||||
|
* <p>
|
||||||
|
* Reads Linux /proc files to determine:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Total and available system memory</li>
|
||||||
|
* <li>Memory usage percentage by the system</li>
|
||||||
|
* <li>Memory usage percentage of a specific process (by PID)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Performance notes:
|
||||||
|
* <ul>
|
||||||
|
* <li>/proc/meminfo is cached for a short time window</li>
|
||||||
|
* <li>No regex usage</li>
|
||||||
|
* <li>No temporary Maps or Lists</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class MemoryInfo implements IMemoryInfo {
|
||||||
|
|
||||||
|
/** Typical memory page size on Linux in bytes. */
|
||||||
|
private static final long PAGE_SIZE = 4096;
|
||||||
|
|
||||||
|
/** Cache validity in milliseconds. */
|
||||||
|
private static final long MEMINFO_CACHE_MS = 500;
|
||||||
|
|
||||||
|
private static long lastRead;
|
||||||
|
|
||||||
|
private static long memTotalKb;
|
||||||
|
private static long memAvailableKb;
|
||||||
|
private static long memFreeKb;
|
||||||
|
private static long buffersKb;
|
||||||
|
private static long cachedKb;
|
||||||
|
private static long sReclaimableKb;
|
||||||
|
private static long shmemKb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the memory usage percentage of a process.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return memory usage percentage of the process
|
||||||
|
* @throws IOException if the /proc file cannot be read or is malformed
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getMemoryPercent(long pid) throws IOException {
|
||||||
|
readMemInfoCached();
|
||||||
|
|
||||||
|
Path statmPath = Path.of("/proc", String.valueOf(pid), "statm");
|
||||||
|
if (!Files.exists(statmPath)) {
|
||||||
|
throw new IOException("Process with PID " + pid + " does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
String statm = Files.readString(statmPath).trim();
|
||||||
|
int space = statm.indexOf(' ');
|
||||||
|
if (space < 0) {
|
||||||
|
throw new IOException("Unexpected format in /proc/" + pid + "/statm");
|
||||||
|
}
|
||||||
|
|
||||||
|
long rssPages = Long.parseLong(statm.substring(space + 1).trim().split(" ")[0]);
|
||||||
|
long processKb = (rssPages * PAGE_SIZE) / 1024;
|
||||||
|
|
||||||
|
double percent = (processKb / (double) memTotalKb) * 100.0;
|
||||||
|
return round(percent, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the overall memory usage percentage of the system.
|
||||||
|
*
|
||||||
|
* @return memory usage percentage of the system
|
||||||
|
* @throws IOException if /proc/meminfo cannot be read
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public double getMemoryUsage() throws IOException {
|
||||||
|
readMemInfoCached();
|
||||||
|
|
||||||
|
long free = memFreeKb
|
||||||
|
+ buffersKb
|
||||||
|
+ cachedKb
|
||||||
|
+ sReclaimableKb
|
||||||
|
- shmemKb;
|
||||||
|
|
||||||
|
double usedPercent = 100.0 * (memTotalKb - free) / memTotalKb;
|
||||||
|
return round(usedPercent, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns total system memory in bytes.
|
||||||
|
*
|
||||||
|
* @return total memory in bytes
|
||||||
|
* @throws IOException if /proc/meminfo cannot be read
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getTotalMemoryBytes() throws IOException {
|
||||||
|
readMemInfoCached();
|
||||||
|
return memTotalKb * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns used memory in bytes (total minus available memory).
|
||||||
|
*
|
||||||
|
* @return used memory in bytes
|
||||||
|
* @throws IOException if /proc/meminfo cannot be read
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getAvailableMemoryBytes() throws IOException {
|
||||||
|
readMemInfoCached();
|
||||||
|
return (memTotalKb - memAvailableKb) * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads /proc/meminfo and caches values for a short time window.
|
||||||
|
*/
|
||||||
|
private static void readMemInfoCached() throws IOException {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - lastRead < MEMINFO_CACHE_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/meminfo"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (line.startsWith("MemTotal:")) {
|
||||||
|
memTotalKb = parseKb(line);
|
||||||
|
} else if (line.startsWith("MemAvailable:")) {
|
||||||
|
memAvailableKb = parseKb(line);
|
||||||
|
} else if (line.startsWith("MemFree:")) {
|
||||||
|
memFreeKb = parseKb(line);
|
||||||
|
} else if (line.startsWith("Buffers:")) {
|
||||||
|
buffersKb = parseKb(line);
|
||||||
|
} else if (line.startsWith("Cached:")) {
|
||||||
|
cachedKb = parseKb(line);
|
||||||
|
} else if (line.startsWith("SReclaimable:")) {
|
||||||
|
sReclaimableKb = parseKb(line);
|
||||||
|
} else if (line.startsWith("Shmem:")) {
|
||||||
|
shmemKb = parseKb(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRead = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a line of /proc/meminfo and returns the value in kB.
|
||||||
|
*/
|
||||||
|
private static long parseKb(String line) {
|
||||||
|
int i = line.indexOf(':') + 1;
|
||||||
|
while (line.charAt(i) == ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
long val = 0;
|
||||||
|
while (i < line.length() && Character.isDigit(line.charAt(i))) {
|
||||||
|
val = val * 10 + (line.charAt(i++) - '0');
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a double value to the given number of decimal places.
|
||||||
|
*
|
||||||
|
* @return returns the value to the desired length
|
||||||
|
*/
|
||||||
|
private static double round(double val, int decimals) {
|
||||||
|
double factor = Math.pow(10, decimals);
|
||||||
|
return Math.round(val * factor) / factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/jtop/system/linux/NetworkInfo.java
Normal file
53
src/jtop/system/linux/NetworkInfo.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jtop.Isystem.INetworkInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to collect network usage statistics.
|
||||||
|
* <p>
|
||||||
|
* Reads from the Linux file <code>/proc/net/dev</code> to retrieve
|
||||||
|
* the number of bytes received (RX) and transmitted (TX) per network interface.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class NetworkInfo implements INetworkInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current network usage for all network interfaces.
|
||||||
|
*
|
||||||
|
* @return a map where the key is the interface name (e.g., "eth0") and
|
||||||
|
* the value is a long array of size 2:
|
||||||
|
* <ul>
|
||||||
|
* <li>index 0: bytes received (RX)</li>
|
||||||
|
* <li>index 1: bytes transmitted (TX)</li>
|
||||||
|
* </ul>
|
||||||
|
* @throws IOException if /proc/net/dev cannot be read
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, long[]> getNetworkUsage() throws IOException {
|
||||||
|
Map<String, long[]> map = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
try (BufferedReader br = Files.newBufferedReader(Path.of("/proc/net/dev"))) {
|
||||||
|
// Skip header lines
|
||||||
|
br.lines().skip(2).forEach(line -> {
|
||||||
|
String[] parts = line.split(":");
|
||||||
|
if (parts.length < 2) return;
|
||||||
|
|
||||||
|
String iface = parts[0].trim();
|
||||||
|
String[] nums = parts[1].trim().split("\\s+");
|
||||||
|
long rx = Long.parseLong(nums[0]);
|
||||||
|
long tx = Long.parseLong(nums[8]);
|
||||||
|
|
||||||
|
map.put(iface, new long[]{rx, tx});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/jtop/system/linux/PathInfo.java
Normal file
90
src/jtop/system/linux/PathInfo.java
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import jtop.Isystem.IPathInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utilities to retrieve process path information.
|
||||||
|
* <p>
|
||||||
|
* Uses {@link ProcessHandle} to fetch details about a running process,
|
||||||
|
* including its command (full path) and executable name.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Performance notes:
|
||||||
|
* <ul>
|
||||||
|
* <li>Results are cached per PID</li>
|
||||||
|
* <li>ProcessHandle is queried only once per PID</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class PathInfo implements IPathInfo {
|
||||||
|
|
||||||
|
private static final String UNKNOWN = "Unknown";
|
||||||
|
|
||||||
|
/** Cache full command path per PID */
|
||||||
|
private final Map<Long, String> pathCache = new HashMap<>();
|
||||||
|
|
||||||
|
/** Cache executable name per PID */
|
||||||
|
private final Map<Long, String> nameCache = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the executable for the given process ID.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the executable name, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName(long pid) {
|
||||||
|
String cached = nameCache.get(pid);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = getPath(pid);
|
||||||
|
if (UNKNOWN.equals(path)) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = path.lastIndexOf('/');
|
||||||
|
String name = idx >= 0 ? path.substring(idx + 1) : path;
|
||||||
|
|
||||||
|
nameCache.put(pid, name);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full command path of the executable for the given process ID.
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the full command path, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getPath(long pid) {
|
||||||
|
String cached = pathCache.get(pid);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<ProcessHandle> ph = ProcessHandle.of(pid);
|
||||||
|
if (ph.isEmpty()) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = ph.get().info().command().orElse(UNKNOWN);
|
||||||
|
pathCache.put(pid, path);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears cached entries.
|
||||||
|
* Should be called periodically to remove dead PIDs.
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
pathCache.clear();
|
||||||
|
nameCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/jtop/system/linux/SystemSampler.java
Normal file
68
src/jtop/system/linux/SystemSampler.java
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import jtop.Isystem.ICpuInfo;
|
||||||
|
import jtop.Isystem.IMemoryInfo;
|
||||||
|
import jtop.Isystem.ITemperatureInfo;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches CPU, memory, and temperature readings to avoid repeated blocking IO.
|
||||||
|
*/
|
||||||
|
public class SystemSampler {
|
||||||
|
|
||||||
|
private double lastCpuUsage;
|
||||||
|
private double lastMemPercent;
|
||||||
|
private Map<String, Double> lastTemps;
|
||||||
|
private double totalMemoryBytes = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize total memory once.
|
||||||
|
*/
|
||||||
|
public void initTotalMemory(IMemoryInfo mem) {
|
||||||
|
try {
|
||||||
|
totalMemoryBytes = mem.getTotalMemoryBytes();
|
||||||
|
} catch (IOException e) {
|
||||||
|
totalMemoryBytes = 0;
|
||||||
|
System.err.println("Failed to read total memory: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes cached CPU, memory, and temperature info.
|
||||||
|
* If total memory was not initialized, it will attempt to cache it now.
|
||||||
|
*/
|
||||||
|
public void refresh(ICpuInfo cpu, IMemoryInfo mem, ITemperatureInfo temps) {
|
||||||
|
try {
|
||||||
|
lastCpuUsage = cpu.getCpuUsage(100); // CPU usage snapshot
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastCpuUsage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastMemPercent = mem.getMemoryUsage();
|
||||||
|
|
||||||
|
// Ensure total memory is cached
|
||||||
|
if (totalMemoryBytes == 0) {
|
||||||
|
totalMemoryBytes = mem.getTotalMemoryBytes();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastMemPercent = 0;
|
||||||
|
if (totalMemoryBytes == 0) {
|
||||||
|
totalMemoryBytes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastTemps = temps != null ? temps.getTemperatures() : Map.of();
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastTemps = Map.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCpu() { return lastCpuUsage; }
|
||||||
|
public double getMem() { return lastMemPercent; }
|
||||||
|
public Map<String, Double> getTemps() { return lastTemps; }
|
||||||
|
public double getTotalMemoryBytes() { return totalMemoryBytes; }
|
||||||
|
}
|
||||||
89
src/jtop/system/linux/TemperatureInfo.java
Normal file
89
src/jtop/system/linux/TemperatureInfo.java
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import jtop.Isystem.ITemperatureInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides system temperature readings from hardware sensors.
|
||||||
|
* <p>
|
||||||
|
* Temperature sources:
|
||||||
|
* <ul>
|
||||||
|
* <li>Primary: /sys/class/hwmon</li>
|
||||||
|
* <li>Fallback: /sys/class/thermal</li>
|
||||||
|
* </ul>
|
||||||
|
* Each temperature is returned in degrees Celsius.
|
||||||
|
*/
|
||||||
|
public class TemperatureInfo implements ITemperatureInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a map of all detected temperatures on the system.
|
||||||
|
*
|
||||||
|
* @return Map where the key is a sensor name (e.g., "coretemp:Core 0")
|
||||||
|
* and the value is the temperature in °C.
|
||||||
|
* @throws IOException if the sensor directories cannot be read
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Double> getTemperatures() throws IOException {
|
||||||
|
Map<String, Double> temps = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// --- Primary source: /sys/class/hwmon ---
|
||||||
|
Path hwmonBase = Path.of("/sys/class/hwmon");
|
||||||
|
if (Files.isDirectory(hwmonBase)) {
|
||||||
|
try (DirectoryStream<Path> hwmons = Files.newDirectoryStream(hwmonBase)) {
|
||||||
|
for (Path hwmon : hwmons) {
|
||||||
|
String name = readTrimmed(hwmon.resolve("name"), "hwmon");
|
||||||
|
|
||||||
|
try (DirectoryStream<Path> files = Files.newDirectoryStream(hwmon, "temp*_input")) {
|
||||||
|
for (Path tempFile : files) {
|
||||||
|
String base = tempFile.getFileName().toString().replace("_input", "");
|
||||||
|
String label = readTrimmed(hwmon.resolve(base + "_label"), base);
|
||||||
|
double value = readTempMilliC(tempFile);
|
||||||
|
temps.put(name + ":" + label, value);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// Ignore unreadable hwmon entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fallback: /sys/class/thermal ---
|
||||||
|
if (temps.isEmpty()) {
|
||||||
|
Path thermalBase = Path.of("/sys/class/thermal");
|
||||||
|
if (Files.isDirectory(thermalBase)) {
|
||||||
|
try (DirectoryStream<Path> zones = Files.newDirectoryStream(thermalBase, "thermal_zone*")) {
|
||||||
|
for (Path zone : zones) {
|
||||||
|
Path typeFile = zone.resolve("type");
|
||||||
|
Path tempFile = zone.resolve("temp");
|
||||||
|
if (Files.exists(typeFile) && Files.exists(tempFile)) {
|
||||||
|
String type = readTrimmed(typeFile, "zone");
|
||||||
|
double temp = readTempMilliC(tempFile);
|
||||||
|
temps.put(type, temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return temps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readTrimmed(Path path, String fallback) {
|
||||||
|
try {
|
||||||
|
return Files.exists(path) ? Files.readString(path).trim() : fallback;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double readTempMilliC(Path path) {
|
||||||
|
try {
|
||||||
|
String str = Files.readString(path).trim();
|
||||||
|
return Double.parseDouble(str) / 1000.0;
|
||||||
|
} catch (IOException | NumberFormatException e) {
|
||||||
|
return Double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/jtop/system/linux/Uptime.java
Normal file
39
src/jtop/system/linux/Uptime.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package jtop.system.linux;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import jtop.Isystem.IUptime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for retrieving system uptime information.
|
||||||
|
* <p>
|
||||||
|
* Reads the uptime from <code>/proc/uptime</code> and returns it
|
||||||
|
* in various units such as seconds, minutes, hours, or days.
|
||||||
|
*/
|
||||||
|
public class Uptime implements IUptime{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the system uptime in the specified format.
|
||||||
|
*
|
||||||
|
* @param timeFormat a character indicating the desired unit:
|
||||||
|
* 's' for seconds,
|
||||||
|
* 'm' for minutes,
|
||||||
|
* 'h' for hours,
|
||||||
|
* 'd' for days.
|
||||||
|
* @return the system uptime in the requested unit.
|
||||||
|
* @throws Exception if reading <code>/proc/uptime</code> fails
|
||||||
|
* or if the timeFormat is invalid.
|
||||||
|
*/
|
||||||
|
public double getSystemUptime(char timeFormat) throws Exception {
|
||||||
|
String content = Files.readString(Path.of("/proc/uptime"));
|
||||||
|
double seconds = Double.parseDouble(content.split(" ")[0]);
|
||||||
|
|
||||||
|
return switch (timeFormat) {
|
||||||
|
case 's' -> seconds;
|
||||||
|
case 'm' -> seconds / 60;
|
||||||
|
case 'h' -> seconds / 3600;
|
||||||
|
case 'd' -> seconds / 86400;
|
||||||
|
default -> throw new IllegalArgumentException("Invalid time format: " + timeFormat);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/jtop/system/mac/MacFeatures.java
Normal file
28
src/jtop/system/mac/MacFeatures.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package jtop.system.mac;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import jtop.system.Feature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the set of system features supported on macOS.
|
||||||
|
* <p>
|
||||||
|
* Each operating system has its own feature class (e.g., {@link jtop.system.linux.LinuxFeatures})
|
||||||
|
* that lists which features are implemented and available.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class MacFeatures {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of features currently supported on macOS.
|
||||||
|
* <p>
|
||||||
|
* This is used by {@link jtop.system.FeatureResolver} to determine at runtime
|
||||||
|
* which features can be instantiated via {@link jtop.system.SystemInfoFactory}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final EnumSet<Feature> SUPPORTED = EnumSet.of(
|
||||||
|
Feature.PROCESS
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Private constructor to prevent instantiation of this utility class. */
|
||||||
|
private MacFeatures() {}
|
||||||
|
}
|
||||||
53
src/jtop/system/mac/PathInfo.java
Normal file
53
src/jtop/system/mac/PathInfo.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package jtop.system.mac;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import jtop.Isystem.IPathInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utilities to retrieve process path information.
|
||||||
|
* <p>
|
||||||
|
* Uses {@link ProcessHandle} to fetch details about a running process,
|
||||||
|
* including its command (full path) and executable name.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class PathInfo implements IPathInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the executable for the given process ID.
|
||||||
|
* <p>
|
||||||
|
* For example, if the command is "/usr/bin/java", this will return "java".
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the executable name, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName(long pid) {
|
||||||
|
Optional<ProcessHandle> ph = ProcessHandle.of(pid);
|
||||||
|
if (ph.isPresent()) {
|
||||||
|
ProcessHandle.Info info = ph.get().info();
|
||||||
|
String command = info.command().orElse("Unknown");
|
||||||
|
return command.substring(command.lastIndexOf("/") + 1);
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full command path of the executable for the given process ID.
|
||||||
|
* <p>
|
||||||
|
* For example, "/usr/bin/java".
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param pid the process ID
|
||||||
|
* @return the full command path, or "Unknown" if the process does not exist
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getPath(long pid) {
|
||||||
|
Optional<ProcessHandle> ph = ProcessHandle.of(pid);
|
||||||
|
if (ph.isPresent()) {
|
||||||
|
ProcessHandle.Info info = ph.get().info();
|
||||||
|
return info.command().orElse("Unknown");
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/jtop/terminal/Header.java
Normal file
50
src/jtop/terminal/Header.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package jtop.terminal;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jtop.system.linux.SystemSampler;
|
||||||
|
|
||||||
|
public class Header {
|
||||||
|
|
||||||
|
private static final String RESET = "\033[0m";
|
||||||
|
private static final String HEADER_BG = "\033[44m";
|
||||||
|
private static final String HEADER_FG = "\033[97m";
|
||||||
|
|
||||||
|
// draw header using cached SystemSampler values
|
||||||
|
public static void draw(SystemSampler sampler, double uptime, String load) {
|
||||||
|
try {
|
||||||
|
double cpuUsage = sampler.getCpu();
|
||||||
|
double memPercent = sampler.getMem();
|
||||||
|
double totalMem = sampler.getTotalMemoryBytes(); // you can cache this too
|
||||||
|
double usedMem = totalMem * (memPercent / 100.0);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(HEADER_BG).append(HEADER_FG);
|
||||||
|
sb.append(String.format(" Uptime: %.1fh ", uptime));
|
||||||
|
sb.append(String.format("| Load: %s ", load));
|
||||||
|
sb.append(String.format("| CPU: %.1f%% ", cpuUsage));
|
||||||
|
sb.append(String.format("| Mem: %.1f%% (%.1f/%.1f GB) ",
|
||||||
|
memPercent, usedMem / 1e9, totalMem / 1e9));
|
||||||
|
|
||||||
|
Map<String, Double> temps = sampler.getTemps();
|
||||||
|
if (temps != null) {
|
||||||
|
int count = 0;
|
||||||
|
for (Map.Entry<String, Double> entry : temps.entrySet()) {
|
||||||
|
sb.append(String.format("| %s: %.1f°C ", entry.getKey(), entry.getValue()));
|
||||||
|
if (++count >= 3) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int terminalWidth = TerminalSize.getColumns();
|
||||||
|
String line = sb.length() > terminalWidth ? sb.substring(0, terminalWidth - 1) : sb.toString();
|
||||||
|
System.out.println(line + RESET);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println(HEADER_BG + HEADER_FG + " Header error: " + e.getMessage() + RESET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getRowsCount() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/jtop/terminal/InputHandler.java
Normal file
134
src/jtop/terminal/InputHandler.java
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package jtop.terminal;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import jtop.core.ShowProcesses;
|
||||||
|
import jtop.core.ProcessTableRenderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles keyboard and mouse input from the user for the process monitor.
|
||||||
|
* <p>
|
||||||
|
* Interprets key presses for:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Scrolling (Arrow keys, 'j'/'k', mouse wheel)</li>
|
||||||
|
* <li>Sorting by column (mouse click on header)</li>
|
||||||
|
* <li>Paging (Enter key)</li>
|
||||||
|
* <li>Exiting the application ('q' or Ctrl+C)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class InputHandler {
|
||||||
|
|
||||||
|
/** The main process display manager. */
|
||||||
|
private final ShowProcesses showProcesses;
|
||||||
|
|
||||||
|
/** Atomic flag indicating whether the process table should be refreshed. */
|
||||||
|
private final AtomicBoolean refresh;
|
||||||
|
|
||||||
|
/** Provides the current terminal size. */
|
||||||
|
private final TerminalSize terminalSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new input handler for a given process table and terminal.
|
||||||
|
*
|
||||||
|
* @param showProcesses the {@link ShowProcesses} instance to control
|
||||||
|
* @param refresh atomic boolean controlling background refresh
|
||||||
|
* @param terminalSize the {@link TerminalSize} instance
|
||||||
|
*/
|
||||||
|
public InputHandler(ShowProcesses showProcesses, AtomicBoolean refresh, TerminalSize terminalSize) {
|
||||||
|
this.showProcesses = showProcesses;
|
||||||
|
this.refresh = refresh;
|
||||||
|
this.terminalSize = terminalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts reading and handling user input.
|
||||||
|
* <p>
|
||||||
|
* This method blocks and continuously interprets keyboard and mouse events
|
||||||
|
* until the user exits the application.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws Exception if an I/O error occurs while reading input
|
||||||
|
*/
|
||||||
|
public void start() throws Exception {
|
||||||
|
int pageSize = terminalSize.getRows() - ProcessTableRenderer.getHeaderAndFooterLength();
|
||||||
|
int c;
|
||||||
|
|
||||||
|
while ((c = System.in.read()) != -1) {
|
||||||
|
switch (c) {
|
||||||
|
case 27: // ESC sequence
|
||||||
|
if (System.in.read() == 91) { // '['
|
||||||
|
int next = System.in.read();
|
||||||
|
switch (next) {
|
||||||
|
case 65 -> showProcesses.scrollUp(); // Arrow Up
|
||||||
|
case 66 -> showProcesses.scrollDown(); // Arrow Down
|
||||||
|
case 77 -> handleMouseEvent(); // Mouse event
|
||||||
|
}
|
||||||
|
showProcesses.draw();
|
||||||
|
refresh.set(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 106: // 'j' key
|
||||||
|
showProcesses.scrollDown();
|
||||||
|
showProcesses.draw();
|
||||||
|
refresh.set(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 107: // 'k' key
|
||||||
|
showProcesses.scrollUp();
|
||||||
|
showProcesses.draw();
|
||||||
|
refresh.set(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 13: // Enter key
|
||||||
|
for (int i = 0; i < pageSize; i++) {
|
||||||
|
showProcesses.scrollDown();
|
||||||
|
}
|
||||||
|
showProcesses.draw();
|
||||||
|
refresh.set(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (c >= 48 && c <= 57) { // 0-9
|
||||||
|
if (c == 48) {
|
||||||
|
c = 58;// 0 acts as 10 and 1 is the first index
|
||||||
|
}
|
||||||
|
showProcesses.changeSort(c - 49);
|
||||||
|
}
|
||||||
|
if (c == 113 || c == 3) { // 'q' or Ctrl+C
|
||||||
|
return; // exit loop
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a mouse event received from the terminal.
|
||||||
|
* <p>
|
||||||
|
* Interprets:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Left click on header row → changes sorting column</li>
|
||||||
|
* <li>Scroll wheel up → scrolls up</li>
|
||||||
|
* <li>Scroll wheel down → scrolls down</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws Exception if an I/O error occurs while reading mouse input
|
||||||
|
*/
|
||||||
|
private void handleMouseEvent() throws Exception {
|
||||||
|
int cb = System.in.read() - 32; // button code
|
||||||
|
int cx = System.in.read() - 32; // column (X)
|
||||||
|
int cy = System.in.read() - 32; // row (Y)
|
||||||
|
|
||||||
|
switch (cb) {
|
||||||
|
case 0 -> { // Left click
|
||||||
|
if (cy == 1 + Header.getRowsCount()) {
|
||||||
|
showProcesses.changeSortByClick(cx - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 64 -> showProcesses.scrollUp(); // wheel up
|
||||||
|
case 65 -> showProcesses.scrollDown(); // wheel down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/jtop/terminal/TerminalSize.java
Normal file
57
src/jtop/terminal/TerminalSize.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package jtop.terminal;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to detect the current terminal window size.
|
||||||
|
* <p>
|
||||||
|
* Provides methods to retrieve the number of rows and columns,
|
||||||
|
* allowing output to dynamically adjust to fit the screen.
|
||||||
|
*/
|
||||||
|
public class TerminalSize {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the terminal size by executing the "stty size" command.
|
||||||
|
*
|
||||||
|
* @return an array of two integers: {rows, columns}.
|
||||||
|
* Defaults to {24, 80} if the size cannot be determined.
|
||||||
|
*/
|
||||||
|
public static int[] getTerminalSize() {
|
||||||
|
try {
|
||||||
|
Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line = reader.readLine();
|
||||||
|
if (line != null) {
|
||||||
|
String[] parts = line.trim().split("\\s+");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
return new int[]{
|
||||||
|
Integer.parseInt(parts[0]),
|
||||||
|
Integer.parseInt(parts[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore and fallback
|
||||||
|
}
|
||||||
|
return new int[]{24, 80}; // default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the number of terminal rows.
|
||||||
|
*
|
||||||
|
* @return the number of rows in the current terminal, or 24 if unknown
|
||||||
|
*/
|
||||||
|
public static int getRows() {
|
||||||
|
return getTerminalSize()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the number of terminal columns.
|
||||||
|
*
|
||||||
|
* @return the number of columns in the current terminal, or 80 if unknown
|
||||||
|
*/
|
||||||
|
public static int getColumns() {
|
||||||
|
return getTerminalSize()[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user