monitorify 1.0.0__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monitorify-1.0.2/PKG-INFO +62 -0
- monitorify-1.0.2/README.md +50 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/pyproject.toml +1 -1
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/__init__.py +1 -1
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/collector/manager.py +2 -2
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/collector/worker.py +14 -3
- monitorify-1.0.2/src/monitorify/config/__init__.py +29 -0
- monitorify-1.0.2/src/monitorify/config/configManager.py +47 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/config.py +3 -0
- monitorify-1.0.2/src/monitorify/config_manager.py +47 -0
- monitorify-1.0.2/src/monitorify/daemon.py +46 -0
- monitorify-1.0.2/src/monitorify/main.py +107 -0
- monitorify-1.0.2/src/monitorify/remote/__init__.py +3 -0
- monitorify-1.0.2/src/monitorify/remote/connector.py +115 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/stats/DiskStats.py +3 -4
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/components/menu.py +30 -8
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/css/tui.css +0 -1
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/tui.py +37 -15
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/brailleGraph.py +7 -10
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/cpuWidget.py +2 -2
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/diskWidget.py +1 -1
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/networkWidget.py +3 -3
- monitorify-1.0.2/src/monitorify.egg-info/PKG-INFO +62 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify.egg-info/SOURCES.txt +5 -0
- monitorify-1.0.0/PKG-INFO +0 -135
- monitorify-1.0.0/README.md +0 -123
- monitorify-1.0.0/src/monitorify/daemon.py +0 -32
- monitorify-1.0.0/src/monitorify/main.py +0 -53
- monitorify-1.0.0/src/monitorify.egg-info/PKG-INFO +0 -135
- {monitorify-1.0.0 → monitorify-1.0.2}/setup.cfg +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/__main__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/collector/__init__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/collector/schema.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/stats/__init__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/stats/cpuStats.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/stats/networkStats.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/stats/programmList.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/stats/ramStats.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/storage/__init__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/storage/db.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/__init__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/__init__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/components/widgetManager.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/__init__.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/procInfoWidget.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/programmListWidget.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/ramWidget.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify/ui/tui/widgets/statusWidget.py +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify.egg-info/dependency_links.txt +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify.egg-info/entry_points.txt +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify.egg-info/requires.txt +0 -0
- {monitorify-1.0.0 → monitorify-1.0.2}/src/monitorify.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monitorify
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: A lightweight Linux terminal system monitor built with Textual
|
|
5
|
+
Author: Leon
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: textual>=0.80.0
|
|
10
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
|
|
13
|
+
# Monitorify
|
|
14
|
+
### A terminal montioring tool for linux
|
|
15
|
+
|
|
16
|
+
- Features
|
|
17
|
+
- CPU monitoring
|
|
18
|
+
- RAM monitoring
|
|
19
|
+
- Network monitoring
|
|
20
|
+
- Disk monitoring (I/O and storage capacity)
|
|
21
|
+
- Process monitoring
|
|
22
|
+
- Background daemon
|
|
23
|
+
- Customization menu (toggle widgets, set intervals, and change history window)
|
|
24
|
+
- Historical graphs
|
|
25
|
+
## Installation
|
|
26
|
+
### Using pipx
|
|
27
|
+
```bash
|
|
28
|
+
pipx install git+https://github.com/leonst036/Monitorify.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Using pip
|
|
32
|
+
```bash
|
|
33
|
+
pip install git+https://github.com/leonst036/Monitorify.git
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Using source
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/leonst036/Monitorify.git
|
|
39
|
+
cd Monitorify
|
|
40
|
+
pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
```bash
|
|
45
|
+
monitorify
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Tech Stack
|
|
49
|
+
|
|
50
|
+
* **Language & TUI:** Python, Textual
|
|
51
|
+
* **Database:** SQLite
|
|
52
|
+
* **Target OS:** Linux
|
|
53
|
+
|
|
54
|
+
## Remote
|
|
55
|
+
|
|
56
|
+
To use Monitorify remotely over SSH on your server, you can set the following parameters:
|
|
57
|
+
|
|
58
|
+
- `--ip` – Set the target IP address.
|
|
59
|
+
- `--port` – Set the SSH port.
|
|
60
|
+
- `--username` – Set the username for the remote host.
|
|
61
|
+
- `--password` – Set the password for the SSH server.
|
|
62
|
+
- `--key_filename` – Set the path to an SSH key *(optional)*.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Monitorify
|
|
2
|
+
### A terminal montioring tool for linux
|
|
3
|
+
|
|
4
|
+
- Features
|
|
5
|
+
- CPU monitoring
|
|
6
|
+
- RAM monitoring
|
|
7
|
+
- Network monitoring
|
|
8
|
+
- Disk monitoring (I/O and storage capacity)
|
|
9
|
+
- Process monitoring
|
|
10
|
+
- Background daemon
|
|
11
|
+
- Customization menu (toggle widgets, set intervals, and change history window)
|
|
12
|
+
- Historical graphs
|
|
13
|
+
## Installation
|
|
14
|
+
### Using pipx
|
|
15
|
+
```bash
|
|
16
|
+
pipx install git+https://github.com/leonst036/Monitorify.git
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Using pip
|
|
20
|
+
```bash
|
|
21
|
+
pip install git+https://github.com/leonst036/Monitorify.git
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Using source
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/leonst036/Monitorify.git
|
|
27
|
+
cd Monitorify
|
|
28
|
+
pip install .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
```bash
|
|
33
|
+
monitorify
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tech Stack
|
|
37
|
+
|
|
38
|
+
* **Language & TUI:** Python, Textual
|
|
39
|
+
* **Database:** SQLite
|
|
40
|
+
* **Target OS:** Linux
|
|
41
|
+
|
|
42
|
+
## Remote
|
|
43
|
+
|
|
44
|
+
To use Monitorify remotely over SSH on your server, you can set the following parameters:
|
|
45
|
+
|
|
46
|
+
- `--ip` – Set the target IP address.
|
|
47
|
+
- `--port` – Set the SSH port.
|
|
48
|
+
- `--username` – Set the username for the remote host.
|
|
49
|
+
- `--password` – Set the password for the SSH server.
|
|
50
|
+
- `--key_filename` – Set the path to an SSH key *(optional)*.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import List, Optional, Any, Callable
|
|
1
|
+
from typing import List, Optional, Any, Callable, Union
|
|
2
2
|
from monitorify.collector.worker import Worker
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Manager:
|
|
6
|
-
def __init__(self, db: Optional[Any] = None, interval: float = 1.0):
|
|
6
|
+
def __init__(self, db: Optional[Any] = None, interval: Union[float, Callable[[], float]] = 1.0):
|
|
7
7
|
self._workers: List[Worker] = []
|
|
8
8
|
self._interval = interval
|
|
9
9
|
if db is None:
|
|
@@ -3,10 +3,10 @@ from threading import Thread
|
|
|
3
3
|
from monitorify.stats import cpuStats, ramStats, networkStats, programmList, DiskStats
|
|
4
4
|
from monitorify.collector.schema import MetricSnapshot
|
|
5
5
|
|
|
6
|
-
from typing import Callable, Optional
|
|
6
|
+
from typing import Callable, Optional, Union
|
|
7
7
|
|
|
8
8
|
class Worker:
|
|
9
|
-
def __init__(self, interval: float = 1.0, on_data: Optional[Callable[[MetricSnapshot], None]] = None):
|
|
9
|
+
def __init__(self, interval: Union[float, Callable[[], float]] = 1.0, on_data: Optional[Callable[[MetricSnapshot], None]] = None):
|
|
10
10
|
self._interval = interval
|
|
11
11
|
self._on_data = on_data
|
|
12
12
|
self._running = False
|
|
@@ -22,6 +22,17 @@ class Worker:
|
|
|
22
22
|
if self._thread:
|
|
23
23
|
self._thread.join()
|
|
24
24
|
|
|
25
|
+
def _get_interval(self) -> float:
|
|
26
|
+
if callable(self._interval):
|
|
27
|
+
try:
|
|
28
|
+
return float(self._interval())
|
|
29
|
+
except Exception:
|
|
30
|
+
return 1.0
|
|
31
|
+
try:
|
|
32
|
+
return float(self._interval)
|
|
33
|
+
except Exception:
|
|
34
|
+
return 1.0
|
|
35
|
+
|
|
25
36
|
def _worker_loop(self):
|
|
26
37
|
while self._running:
|
|
27
38
|
snapshot = self.collect()
|
|
@@ -30,7 +41,7 @@ class Worker:
|
|
|
30
41
|
self._on_data(snapshot)
|
|
31
42
|
except Exception as e:
|
|
32
43
|
print(f"Error handling metric snapshot: {e}")
|
|
33
|
-
time.sleep(self.
|
|
44
|
+
time.sleep(max(0.1, self._get_interval()))
|
|
34
45
|
|
|
35
46
|
def collect(self) -> MetricSnapshot:
|
|
36
47
|
return MetricSnapshot(
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from platformdirs import user_data_dir, user_runtime_dir
|
|
4
|
+
|
|
5
|
+
# Data directory for database storage
|
|
6
|
+
DATA_DIR = Path(user_data_dir("monitorify", appauthor=False))
|
|
7
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
8
|
+
|
|
9
|
+
# Runtime directory for PID files
|
|
10
|
+
try:
|
|
11
|
+
RUNTIME_DIR = Path(user_runtime_dir("monitorify", appauthor=False))
|
|
12
|
+
RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
except Exception:
|
|
14
|
+
RUNTIME_DIR = DATA_DIR
|
|
15
|
+
|
|
16
|
+
# Database settings
|
|
17
|
+
DB_PATH = os.getenv("MONITORIFY_DB_PATH", str(DATA_DIR / "monitorify.db"))
|
|
18
|
+
|
|
19
|
+
# User configuration file path
|
|
20
|
+
CONFIG_PATH = Path(os.getenv("MONITORIFY_CONFIG_PATH", str(DATA_DIR / "config.json")))
|
|
21
|
+
|
|
22
|
+
# Daemon PID file path
|
|
23
|
+
DAEMON_PIDFILE = Path(os.getenv("MONITORIFY_PID_PATH", str(RUNTIME_DIR / "daemon.pid")))
|
|
24
|
+
|
|
25
|
+
# Collection settings (seconds)
|
|
26
|
+
DEFAULT_COLLECTION_INTERVAL = float(os.getenv("MONITORIFY_INTERVAL", "1.0"))
|
|
27
|
+
|
|
28
|
+
# Retention settings (default: 7 days in seconds)
|
|
29
|
+
RETENTION_SECONDS = float(os.getenv("MONITORIFY_RETENTION", str(7 * 24 * 3600)))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class configManager:
|
|
6
|
+
def __init__(self, config_path: str):
|
|
7
|
+
self.config_path = config_path
|
|
8
|
+
self.config_data = {}
|
|
9
|
+
self.load_config()
|
|
10
|
+
|
|
11
|
+
def load_config(self) -> dict:
|
|
12
|
+
path = Path(self.config_path)
|
|
13
|
+
if not path.exists():
|
|
14
|
+
self.config_data = {}
|
|
15
|
+
return self.config_data
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
with path.open("r", encoding="utf-8") as file:
|
|
19
|
+
data = json.load(file)
|
|
20
|
+
if isinstance(data, dict):
|
|
21
|
+
self.config_data = data
|
|
22
|
+
else:
|
|
23
|
+
self.config_data = {}
|
|
24
|
+
except (json.JSONDecodeError, OSError):
|
|
25
|
+
self.config_data = {}
|
|
26
|
+
|
|
27
|
+
return self.config_data
|
|
28
|
+
|
|
29
|
+
def save_config(self, config_data: dict):
|
|
30
|
+
try:
|
|
31
|
+
path = Path(self.config_path)
|
|
32
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
with path.open("w", encoding="utf-8") as file:
|
|
34
|
+
json.dump(config_data, file, indent=4)
|
|
35
|
+
self.config_data = config_data
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error saving configuration: {e}")
|
|
38
|
+
|
|
39
|
+
def get(self, key: str, default=None):
|
|
40
|
+
return self.config_data.get(key, default)
|
|
41
|
+
|
|
42
|
+
def get_connfig(self, key: str):
|
|
43
|
+
return self.get(key, None)
|
|
44
|
+
|
|
45
|
+
def set(self, key: str, value):
|
|
46
|
+
self.config_data[key] = value
|
|
47
|
+
self.save_config(self.config_data)
|
|
@@ -16,6 +16,9 @@ except Exception:
|
|
|
16
16
|
# Database settings
|
|
17
17
|
DB_PATH = os.getenv("MONITORIFY_DB_PATH", str(DATA_DIR / "monitorify.db"))
|
|
18
18
|
|
|
19
|
+
# User configuration file path
|
|
20
|
+
CONFIG_PATH = Path(os.getenv("MONITORIFY_CONFIG_PATH", str(DATA_DIR / "config.json")))
|
|
21
|
+
|
|
19
22
|
# Daemon PID file path
|
|
20
23
|
DAEMON_PIDFILE = Path(os.getenv("MONITORIFY_PID_PATH", str(RUNTIME_DIR / "daemon.pid")))
|
|
21
24
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class configManager:
|
|
6
|
+
def __init__(self, config_path: str):
|
|
7
|
+
self.config_path = config_path
|
|
8
|
+
self.config_data = {}
|
|
9
|
+
self.load_config()
|
|
10
|
+
|
|
11
|
+
def load_config(self) -> dict:
|
|
12
|
+
path = Path(self.config_path)
|
|
13
|
+
if not path.exists():
|
|
14
|
+
self.config_data = {}
|
|
15
|
+
return self.config_data
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
with path.open("r", encoding="utf-8") as file:
|
|
19
|
+
data = json.load(file)
|
|
20
|
+
if isinstance(data, dict):
|
|
21
|
+
self.config_data = data
|
|
22
|
+
else:
|
|
23
|
+
self.config_data = {}
|
|
24
|
+
except (json.JSONDecodeError, OSError):
|
|
25
|
+
self.config_data = {}
|
|
26
|
+
|
|
27
|
+
return self.config_data
|
|
28
|
+
|
|
29
|
+
def save_config(self, config_data: dict):
|
|
30
|
+
try:
|
|
31
|
+
path = Path(self.config_path)
|
|
32
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
with path.open("w", encoding="utf-8") as file:
|
|
34
|
+
json.dump(config_data, file, indent=4)
|
|
35
|
+
self.config_data = config_data
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error saving configuration: {e}")
|
|
38
|
+
|
|
39
|
+
def get(self, key: str, default=None):
|
|
40
|
+
return self.config_data.get(key, default)
|
|
41
|
+
|
|
42
|
+
def get_connfig(self, key: str):
|
|
43
|
+
return self.get(key, None)
|
|
44
|
+
|
|
45
|
+
def set(self, key: str, value):
|
|
46
|
+
self.config_data[key] = value
|
|
47
|
+
self.save_config(self.config_data)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
from monitorify.collector import Manager
|
|
5
|
+
from monitorify.storage import Database
|
|
6
|
+
from monitorify import config
|
|
7
|
+
from monitorify.config.configManager import configManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_collection_interval() -> float:
|
|
11
|
+
try:
|
|
12
|
+
cfg = configManager(str(config.CONFIG_PATH))
|
|
13
|
+
for key in ("update_interval", "collection_interval", "interval"):
|
|
14
|
+
val = cfg.get(key)
|
|
15
|
+
if val is not None:
|
|
16
|
+
return float(val)
|
|
17
|
+
except Exception:
|
|
18
|
+
pass
|
|
19
|
+
return config.DEFAULT_COLLECTION_INTERVAL
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
print("Starting Monitorify background metrics collector daemon...")
|
|
24
|
+
db = Database(config.DB_PATH)
|
|
25
|
+
interval = get_collection_interval()
|
|
26
|
+
manager = Manager(db=db, interval=get_collection_interval)
|
|
27
|
+
|
|
28
|
+
def signal_handler(sig, frame):
|
|
29
|
+
print("\nStopping Monitorify daemon...")
|
|
30
|
+
manager.stop()
|
|
31
|
+
sys.exit(0)
|
|
32
|
+
|
|
33
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
34
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
35
|
+
|
|
36
|
+
manager.start()
|
|
37
|
+
print(
|
|
38
|
+
f"Metrics collector running (Interval: {interval}s, DB: {config.DB_PATH}). Press Ctrl+C to stop."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
while True:
|
|
42
|
+
time.sleep(1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from monitorify import config
|
|
5
|
+
from monitorify.ui.tui.tui import MonitorifyApp
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
def parse_args():
|
|
9
|
+
"""Parse CLI arguments."""
|
|
10
|
+
parser = argparse.ArgumentParser(description="Monitorify")
|
|
11
|
+
parser.add_argument("--ip", type=str, default=None, help="IP address of the remote machine")
|
|
12
|
+
parser.add_argument("--port", type=int, default=None, help="Port of the remote machine (default: 22)")
|
|
13
|
+
parser.add_argument("--username", type=str, default=None, help="Username for the remote machine")
|
|
14
|
+
parser.add_argument("--password", type=str, default=None, help="Password for the remote machine")
|
|
15
|
+
parser.add_argument("--key_filename", type=str, default=None, help="Path to the key file for the remote machine")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
if not args.ip and (args.port is not None or args.username or args.password or args.key_filename):
|
|
19
|
+
parser.error("--ip is required when specifying remote connection arguments")
|
|
20
|
+
|
|
21
|
+
return args
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def daemon_already_running() -> bool:
|
|
25
|
+
"""Check if a daemon process from a previous run is still alive."""
|
|
26
|
+
if not config.DAEMON_PIDFILE.exists():
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
pid = int(config.DAEMON_PIDFILE.read_text().strip())
|
|
30
|
+
# Signal 0 checks if the process exists without killing it
|
|
31
|
+
os.kill(pid, 0)
|
|
32
|
+
return True
|
|
33
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
34
|
+
config.DAEMON_PIDFILE.unlink(missing_ok=True)
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def start_daemon() -> subprocess.Popen | None:
|
|
39
|
+
"""Start the daemon as a background subprocess if not already running."""
|
|
40
|
+
if daemon_already_running():
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
proc = subprocess.Popen(
|
|
44
|
+
[sys.executable, "-m", "monitorify.daemon"],
|
|
45
|
+
stdout=subprocess.DEVNULL,
|
|
46
|
+
stderr=subprocess.DEVNULL,
|
|
47
|
+
)
|
|
48
|
+
config.DAEMON_PIDFILE.write_text(str(proc.pid))
|
|
49
|
+
return proc
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cli():
|
|
53
|
+
"""Main CLI entrypoint"""
|
|
54
|
+
args = parse_args()
|
|
55
|
+
|
|
56
|
+
if args.ip:
|
|
57
|
+
from monitorify.remote.connector import RemoteConnector
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
connector = RemoteConnector(
|
|
61
|
+
host=args.ip,
|
|
62
|
+
port=args.port or 22,
|
|
63
|
+
username=args.username,
|
|
64
|
+
password=args.password,
|
|
65
|
+
key_filename=args.key_filename,
|
|
66
|
+
)
|
|
67
|
+
client = connector.connect()
|
|
68
|
+
try:
|
|
69
|
+
if not args.command and not connector.is_installed(client):
|
|
70
|
+
print(f"Error: Monitorify is not installed on the remote host ({args.ip}).")
|
|
71
|
+
response = input("Do you want to install it? (y/n): ").strip().lower()
|
|
72
|
+
if response == "y":
|
|
73
|
+
success = connector.install(client)
|
|
74
|
+
if not success:
|
|
75
|
+
print("Installation failed.")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
else:
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
remote_cmd = args.command or "bash -l -c 'monitorify || python3 -m monitorify'"
|
|
81
|
+
connector.run_interactive(client, command=remote_cmd)
|
|
82
|
+
finally:
|
|
83
|
+
client.close()
|
|
84
|
+
except ConnectionError as e:
|
|
85
|
+
print(f"Connection failed: {e}")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
except KeyboardInterrupt:
|
|
88
|
+
pass
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Local mode
|
|
92
|
+
daemon = start_daemon()
|
|
93
|
+
try:
|
|
94
|
+
app = MonitorifyApp()
|
|
95
|
+
app.run()
|
|
96
|
+
finally:
|
|
97
|
+
if daemon is not None:
|
|
98
|
+
daemon.terminate()
|
|
99
|
+
try:
|
|
100
|
+
daemon.wait(timeout=2)
|
|
101
|
+
except subprocess.TimeoutExpired:
|
|
102
|
+
daemon.kill()
|
|
103
|
+
config.DAEMON_PIDFILE.unlink(missing_ok=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
cli()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import paramiko
|
|
2
|
+
|
|
3
|
+
class RemoteConnector:
|
|
4
|
+
def __init__(
|
|
5
|
+
self,
|
|
6
|
+
host: str,
|
|
7
|
+
port: int = 22,
|
|
8
|
+
username: str | None = None,
|
|
9
|
+
password: str | None = None,
|
|
10
|
+
key_filename: str | None = None,
|
|
11
|
+
):
|
|
12
|
+
self.host = host
|
|
13
|
+
self.port = port
|
|
14
|
+
self.username = username
|
|
15
|
+
self.password = password
|
|
16
|
+
self.key_filename = key_filename
|
|
17
|
+
|
|
18
|
+
def connect(self):
|
|
19
|
+
client = paramiko.SSHClient()
|
|
20
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
21
|
+
try:
|
|
22
|
+
client.connect(hostname=self.host, port=self.port, username=self.username, password=self.password, key_filename=self.key_filename)
|
|
23
|
+
except paramiko.AuthenticationException as e:
|
|
24
|
+
raise ConnectionError(f"Authentication failed: {e}")
|
|
25
|
+
except paramiko.SSHException as e:
|
|
26
|
+
raise ConnectionError(f"SSH error: {e}")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
raise ConnectionError(f"Error: {e}")
|
|
29
|
+
return client
|
|
30
|
+
|
|
31
|
+
def is_installed(self, client: paramiko.SSHClient) -> bool:
|
|
32
|
+
"""Check if monitorify is installed on the remote host."""
|
|
33
|
+
try:
|
|
34
|
+
_, stdout, _ = client.exec_command(
|
|
35
|
+
"bash -l -c 'command -v monitorify >/dev/null 2>&1 || python3 -c \"import monitorify\" >/dev/null 2>&1'"
|
|
36
|
+
)
|
|
37
|
+
return stdout.channel.recv_exit_status() == 0
|
|
38
|
+
except Exception:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def install(self, client: paramiko.SSHClient) -> bool:
|
|
42
|
+
"""Install monitorify on the remote host using install.sh."""
|
|
43
|
+
import shlex
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
script_path = Path(__file__).parent / "install.sh"
|
|
47
|
+
script_content = script_path.read_text(encoding="utf-8")
|
|
48
|
+
install_cmd = f"bash -l -c {shlex.quote(script_content)}"
|
|
49
|
+
self.run_interactive(client, command=install_cmd)
|
|
50
|
+
return self.is_installed(client)
|
|
51
|
+
|
|
52
|
+
def run_interactive(self, client: paramiko.SSHClient, command: str = "monitorify") -> None:
|
|
53
|
+
"""Run a command interactively on the remote host with a PTY."""
|
|
54
|
+
import os
|
|
55
|
+
import select
|
|
56
|
+
import shutil
|
|
57
|
+
import signal
|
|
58
|
+
import sys
|
|
59
|
+
import termios
|
|
60
|
+
import tty
|
|
61
|
+
|
|
62
|
+
transport = client.get_transport()
|
|
63
|
+
if transport is None:
|
|
64
|
+
raise ConnectionError("SSH transport is not active")
|
|
65
|
+
|
|
66
|
+
channel = transport.open_session()
|
|
67
|
+
cols, rows = shutil.get_terminal_size()
|
|
68
|
+
term = os.environ.get("TERM", "xterm-256color")
|
|
69
|
+
channel.get_pty(term=term, width=cols, height=rows)
|
|
70
|
+
channel.exec_command(command)
|
|
71
|
+
|
|
72
|
+
stdin_fd = sys.stdin.fileno()
|
|
73
|
+
stdout_fd = sys.stdout.fileno()
|
|
74
|
+
is_atty = sys.stdin.isatty()
|
|
75
|
+
old_tty = termios.tcgetattr(stdin_fd) if is_atty else None
|
|
76
|
+
|
|
77
|
+
def handle_resize(signum, frame):
|
|
78
|
+
w, h = shutil.get_terminal_size()
|
|
79
|
+
try:
|
|
80
|
+
channel.resize_pty(width=w, height=h)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
signal.signal(signal.SIGWINCH, handle_resize)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
if is_atty:
|
|
91
|
+
tty.setraw(stdin_fd)
|
|
92
|
+
channel.setblocking(0)
|
|
93
|
+
|
|
94
|
+
while True:
|
|
95
|
+
r, _, _ = select.select([channel, stdin_fd], [], [])
|
|
96
|
+
if channel in r:
|
|
97
|
+
try:
|
|
98
|
+
data = channel.recv(4096)
|
|
99
|
+
if not data:
|
|
100
|
+
break
|
|
101
|
+
os.write(stdout_fd, data)
|
|
102
|
+
except Exception:
|
|
103
|
+
break
|
|
104
|
+
if stdin_fd in r:
|
|
105
|
+
try:
|
|
106
|
+
user_input = os.read(stdin_fd, 4096)
|
|
107
|
+
if not user_input:
|
|
108
|
+
break
|
|
109
|
+
channel.sendall(user_input)
|
|
110
|
+
except Exception:
|
|
111
|
+
break
|
|
112
|
+
finally:
|
|
113
|
+
if is_atty and old_tty is not None:
|
|
114
|
+
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty)
|
|
115
|
+
channel.close()
|
|
@@ -10,7 +10,7 @@ def get_real_disks() -> list:
|
|
|
10
10
|
continue
|
|
11
11
|
|
|
12
12
|
disk_name = parts[2]
|
|
13
|
-
# Check if it is a real physical disk
|
|
13
|
+
# Check if it is a real physical disk
|
|
14
14
|
if not os.path.exists(f"/sys/block/{disk_name}/device"):
|
|
15
15
|
continue
|
|
16
16
|
|
|
@@ -32,7 +32,6 @@ def get_disk_IO(disks: list = None) -> dict:
|
|
|
32
32
|
|
|
33
33
|
disk_name = parts[2]
|
|
34
34
|
if disk_name in disks:
|
|
35
|
-
# Sector size in Linux is 512 bytes
|
|
36
35
|
read_bytes = int(parts[5]) * 512
|
|
37
36
|
write_bytes = int(parts[9]) * 512
|
|
38
37
|
disk_io[disk_name] = {
|
|
@@ -60,7 +59,7 @@ def get_disk_storage(disks: list = None) -> dict:
|
|
|
60
59
|
mounts.setdefault(parts[0], parts[1])
|
|
61
60
|
|
|
62
61
|
for disk in disks:
|
|
63
|
-
# Read total raw hardware capacity from /sys/block/<disk>/size
|
|
62
|
+
# Read total raw hardware capacity from /sys/block/<disk>/size
|
|
64
63
|
total_hw = 0
|
|
65
64
|
size_file = f"/sys/block/{disk}/size"
|
|
66
65
|
if os.path.exists(size_file):
|
|
@@ -76,7 +75,7 @@ def get_disk_storage(disks: list = None) -> dict:
|
|
|
76
75
|
|
|
77
76
|
for dev_path, mount_point in mounts.items():
|
|
78
77
|
dev_name = os.path.basename(dev_path)
|
|
79
|
-
# Match disk or its partitions
|
|
78
|
+
# Match disk or its partitions
|
|
80
79
|
if dev_name == disk or dev_name.startswith(disk):
|
|
81
80
|
try:
|
|
82
81
|
st = os.statvfs(mount_point)
|