monitorify 1.0.2__tar.gz → 1.1.0__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.
Files changed (57) hide show
  1. monitorify-1.1.0/PKG-INFO +145 -0
  2. monitorify-1.1.0/README.md +131 -0
  3. {monitorify-1.0.2 → monitorify-1.1.0}/pyproject.toml +4 -1
  4. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/__init__.py +1 -1
  5. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config.py +1 -1
  6. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/main.py +28 -34
  7. monitorify-1.1.0/src/monitorify/remote/__init__.py +4 -0
  8. monitorify-1.1.0/src/monitorify/remote/addRemote.py +157 -0
  9. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/remote/connector.py +82 -8
  10. monitorify-1.1.0/src/monitorify/remote/install.sh +81 -0
  11. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/storage/db.py +148 -2
  12. monitorify-1.1.0/src/monitorify/storage/keyring_helper.py +48 -0
  13. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/components/menu.py +95 -54
  14. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/css/tui.css +82 -6
  15. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/tui.py +94 -1
  16. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/programmListWidget.py +145 -14
  17. monitorify-1.1.0/src/monitorify.egg-info/PKG-INFO +145 -0
  18. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/SOURCES.txt +7 -1
  19. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/requires.txt +2 -0
  20. monitorify-1.1.0/tests/test_menu.py +75 -0
  21. monitorify-1.1.0/tests/test_programm_list_search.py +138 -0
  22. monitorify-1.1.0/tests/test_remote_db.py +207 -0
  23. monitorify-1.0.2/PKG-INFO +0 -62
  24. monitorify-1.0.2/README.md +0 -50
  25. monitorify-1.0.2/src/monitorify/remote/__init__.py +0 -3
  26. monitorify-1.0.2/src/monitorify.egg-info/PKG-INFO +0 -62
  27. {monitorify-1.0.2 → monitorify-1.1.0}/setup.cfg +0 -0
  28. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/__main__.py +0 -0
  29. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/__init__.py +0 -0
  30. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/manager.py +0 -0
  31. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/schema.py +0 -0
  32. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/worker.py +0 -0
  33. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config/__init__.py +0 -0
  34. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config/configManager.py +0 -0
  35. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config_manager.py +0 -0
  36. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/daemon.py +0 -0
  37. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/DiskStats.py +0 -0
  38. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/__init__.py +0 -0
  39. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/cpuStats.py +0 -0
  40. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/networkStats.py +0 -0
  41. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/programmList.py +0 -0
  42. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/ramStats.py +0 -0
  43. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/storage/__init__.py +0 -0
  44. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/__init__.py +0 -0
  45. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/__init__.py +0 -0
  46. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/components/widgetManager.py +0 -0
  47. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/__init__.py +0 -0
  48. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/brailleGraph.py +0 -0
  49. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/cpuWidget.py +0 -0
  50. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/diskWidget.py +0 -0
  51. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/networkWidget.py +0 -0
  52. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/procInfoWidget.py +0 -0
  53. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/ramWidget.py +0 -0
  54. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/statusWidget.py +0 -0
  55. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/dependency_links.txt +0 -0
  56. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/entry_points.txt +0 -0
  57. {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/top_level.txt +0 -0
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: monitorify
3
+ Version: 1.1.0
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
+ Requires-Dist: paramiko>=3.0.0
13
+ Requires-Dist: keyring>=24.0.0
14
+
15
+ # Monitorify
16
+ ### A terminal montioring tool for linux
17
+
18
+ - Features
19
+ - CPU monitoring
20
+ - RAM monitoring
21
+ - Network monitoring
22
+ - Disk monitoring (I/O and storage capacity)
23
+ - Process monitoring
24
+ - Background daemon
25
+ - Customization menu (toggle widgets, set intervals, and change history window)
26
+ - Historical graphs
27
+
28
+ ## Requirements
29
+
30
+ * **Python:** 3.10+
31
+ * **OS:** Linux (relies on `/proc` and `/sys` filesystems)
32
+ * **Terminal:** Minimum 80 columns × 24 lines recommended
33
+
34
+ ## Installation
35
+ ### Using pipx
36
+ ```bash
37
+ pipx install git+https://github.com/leonst036/Monitorify.git
38
+ ```
39
+ Or from PyPI:
40
+ ```bash
41
+ pipx install monitorify
42
+ ```
43
+
44
+ ### Using pip
45
+ ```bash
46
+ pip install git+https://github.com/leonst036/Monitorify.git
47
+ ```
48
+
49
+ ### Using source
50
+ ```bash
51
+ git clone https://github.com/leonst036/Monitorify.git
52
+ cd Monitorify
53
+ pip install .
54
+ ```
55
+
56
+ ## Usage
57
+ ```bash
58
+ monitorify
59
+ ```
60
+
61
+ ## Background Daemon
62
+
63
+ The daemon collects metrics continuously to SQLite, even when the dashboard is closed.
64
+
65
+ ### Manually (foreground)
66
+
67
+ ```bash
68
+ monitorify-daemon
69
+ ```
70
+
71
+ ### Always-On via systemd --user
72
+
73
+ Create the service file at `~/.config/systemd/user/monitorify.service`:
74
+
75
+ ```ini
76
+ [Unit]
77
+ Description=Monitorify Metrics Collector Daemon
78
+
79
+ [Service]
80
+ ExecStart=%h/.local/bin/monitorify-daemon
81
+ Restart=always
82
+
83
+ [Install]
84
+ WantedBy=default.target
85
+ ```
86
+
87
+ Enable and start it:
88
+
89
+ ```bash
90
+ systemctl --user daemon-reload
91
+ systemctl --user enable --now monitorify
92
+ ```
93
+
94
+ Check status or logs:
95
+
96
+ ```bash
97
+ systemctl --user status monitorify
98
+ journalctl --user -u monitorify -f
99
+ ```
100
+
101
+ Stop or disable:
102
+
103
+ ```bash
104
+ systemctl --user stop monitorify
105
+ systemctl --user disable monitorify
106
+ ```
107
+
108
+ ## File Locations
109
+
110
+ Monitorify follows XDG Base Directory conventions — nothing is written to the install directory.
111
+
112
+ | Purpose | Default Path | Override |
113
+ |---|---|---|
114
+ | **Database** | `~/.local/share/monitorify/monitorify.db` | `MONITORIFY_DB_PATH` env var |
115
+ | **Daemon PID** | `/run/user/<uid>/monitorify/daemon.pid` | `MONITORIFY_PID_PATH` env var |
116
+ | **Collection interval** | `1.0` seconds | `MONITORIFY_INTERVAL` env var |
117
+ | **Data retention** | `604800` s (7 days) | `MONITORIFY_RETENTION` env var |
118
+
119
+ ## Remote
120
+
121
+ To use Monitorify remotely over SSH on your server, you can set the following parameters:
122
+
123
+ - `--ip` – Set the target IP address.
124
+ - `--port` – Set the SSH port.
125
+ - `--username` – Set the username for the remote host.
126
+ - `--password` – Set the password for the SSH server.
127
+ - `--key_filename` – Set the path to an SSH key *(optional)*.
128
+ - `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
129
+
130
+ ## Tech Stack
131
+
132
+ * **Language & TUI:** Python, Textual
133
+ * **Database:** SQLite
134
+ * **Target OS:** Linux
135
+
136
+ ## Uninstalling
137
+
138
+ ```bash
139
+ # If installed via pipx
140
+ pipx uninstall monitorify
141
+
142
+ # Remove stored data
143
+ rm -rf ~/.local/share/monitorify
144
+ rm -rf /run/user/$(id -u)/monitorify
145
+ ```
@@ -0,0 +1,131 @@
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
+
14
+ ## Requirements
15
+
16
+ * **Python:** 3.10+
17
+ * **OS:** Linux (relies on `/proc` and `/sys` filesystems)
18
+ * **Terminal:** Minimum 80 columns × 24 lines recommended
19
+
20
+ ## Installation
21
+ ### Using pipx
22
+ ```bash
23
+ pipx install git+https://github.com/leonst036/Monitorify.git
24
+ ```
25
+ Or from PyPI:
26
+ ```bash
27
+ pipx install monitorify
28
+ ```
29
+
30
+ ### Using pip
31
+ ```bash
32
+ pip install git+https://github.com/leonst036/Monitorify.git
33
+ ```
34
+
35
+ ### Using source
36
+ ```bash
37
+ git clone https://github.com/leonst036/Monitorify.git
38
+ cd Monitorify
39
+ pip install .
40
+ ```
41
+
42
+ ## Usage
43
+ ```bash
44
+ monitorify
45
+ ```
46
+
47
+ ## Background Daemon
48
+
49
+ The daemon collects metrics continuously to SQLite, even when the dashboard is closed.
50
+
51
+ ### Manually (foreground)
52
+
53
+ ```bash
54
+ monitorify-daemon
55
+ ```
56
+
57
+ ### Always-On via systemd --user
58
+
59
+ Create the service file at `~/.config/systemd/user/monitorify.service`:
60
+
61
+ ```ini
62
+ [Unit]
63
+ Description=Monitorify Metrics Collector Daemon
64
+
65
+ [Service]
66
+ ExecStart=%h/.local/bin/monitorify-daemon
67
+ Restart=always
68
+
69
+ [Install]
70
+ WantedBy=default.target
71
+ ```
72
+
73
+ Enable and start it:
74
+
75
+ ```bash
76
+ systemctl --user daemon-reload
77
+ systemctl --user enable --now monitorify
78
+ ```
79
+
80
+ Check status or logs:
81
+
82
+ ```bash
83
+ systemctl --user status monitorify
84
+ journalctl --user -u monitorify -f
85
+ ```
86
+
87
+ Stop or disable:
88
+
89
+ ```bash
90
+ systemctl --user stop monitorify
91
+ systemctl --user disable monitorify
92
+ ```
93
+
94
+ ## File Locations
95
+
96
+ Monitorify follows XDG Base Directory conventions — nothing is written to the install directory.
97
+
98
+ | Purpose | Default Path | Override |
99
+ |---|---|---|
100
+ | **Database** | `~/.local/share/monitorify/monitorify.db` | `MONITORIFY_DB_PATH` env var |
101
+ | **Daemon PID** | `/run/user/<uid>/monitorify/daemon.pid` | `MONITORIFY_PID_PATH` env var |
102
+ | **Collection interval** | `1.0` seconds | `MONITORIFY_INTERVAL` env var |
103
+ | **Data retention** | `604800` s (7 days) | `MONITORIFY_RETENTION` env var |
104
+
105
+ ## Remote
106
+
107
+ To use Monitorify remotely over SSH on your server, you can set the following parameters:
108
+
109
+ - `--ip` – Set the target IP address.
110
+ - `--port` – Set the SSH port.
111
+ - `--username` – Set the username for the remote host.
112
+ - `--password` – Set the password for the SSH server.
113
+ - `--key_filename` – Set the path to an SSH key *(optional)*.
114
+ - `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
115
+
116
+ ## Tech Stack
117
+
118
+ * **Language & TUI:** Python, Textual
119
+ * **Database:** SQLite
120
+ * **Target OS:** Linux
121
+
122
+ ## Uninstalling
123
+
124
+ ```bash
125
+ # If installed via pipx
126
+ pipx uninstall monitorify
127
+
128
+ # Remove stored data
129
+ rm -rf ~/.local/share/monitorify
130
+ rm -rf /run/user/$(id -u)/monitorify
131
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "monitorify"
7
- version = "1.0.2"
7
+ version = "1.1.0"
8
8
  description = "A lightweight Linux terminal system monitor built with Textual"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -16,6 +16,8 @@ dependencies = [
16
16
  "textual>=0.80.0",
17
17
  "platformdirs>=4.0.0",
18
18
  "rich>=13.0.0",
19
+ "paramiko>=3.0.0",
20
+ "keyring>=24.0.0",
19
21
  ]
20
22
 
21
23
  [project.scripts]
@@ -27,3 +29,4 @@ where = ["src"]
27
29
 
28
30
  [tool.setuptools.package-data]
29
31
  "monitorify.ui.tui" = ["css/*.css"]
32
+ "monitorify.remote" = ["*.sh"]
@@ -1,3 +1,3 @@
1
1
  """Monitorify - A lightweight Linux terminal system monitor."""
2
2
 
3
- __version__ = "1.0.2"
3
+ __version__ = "1.1.0"
@@ -22,7 +22,7 @@ CONFIG_PATH = Path(os.getenv("MONITORIFY_CONFIG_PATH", str(DATA_DIR / "config.js
22
22
  # Daemon PID file path
23
23
  DAEMON_PIDFILE = Path(os.getenv("MONITORIFY_PID_PATH", str(RUNTIME_DIR / "daemon.pid")))
24
24
 
25
- # Collection settings (seconds)
25
+ # Collection settings
26
26
  DEFAULT_COLLECTION_INTERVAL = float(os.getenv("MONITORIFY_INTERVAL", "1.0"))
27
27
 
28
28
  # Retention settings (default: 7 days in seconds)
@@ -13,9 +13,17 @@ def parse_args():
13
13
  parser.add_argument("--username", type=str, default=None, help="Username for the remote machine")
14
14
  parser.add_argument("--password", type=str, default=None, help="Password for the remote machine")
15
15
  parser.add_argument("--key_filename", type=str, default=None, help="Path to the key file for the remote machine")
16
+ parser.add_argument("--command", type=str, default=None, help="Command to run on the remote machine")
17
+ parser.add_argument(
18
+ "--host",
19
+ type=str,
20
+ choices=["add", "list", "remove", "delete"],
21
+ default=None,
22
+ help="Manage remote hosts (e.g. add, list, remove)",
23
+ )
16
24
  args = parser.parse_args()
17
25
 
18
- if not args.ip and (args.port is not None or args.username or args.password or args.key_filename):
26
+ if not args.ip and not args.host and (args.port is not None or args.username or args.password or args.key_filename):
19
27
  parser.error("--ip is required when specifying remote connection arguments")
20
28
 
21
29
  return args
@@ -50,42 +58,28 @@ def start_daemon() -> subprocess.Popen | None:
50
58
 
51
59
 
52
60
  def cli():
53
- """Main CLI entrypoint"""
61
+ """Main CLI entrypoint."""
54
62
  args = parse_args()
55
63
 
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}")
64
+ if args.host:
65
+ from monitorify.remote.addRemote import handle_host_action
66
+
67
+ handle_host_action(args.host, args)
68
+ return
69
+ elif args.ip:
70
+ from monitorify.remote.connector import connect_and_run
71
+
72
+ success = connect_and_run(
73
+ host=args.ip,
74
+ port=args.port or 22,
75
+ username=args.username,
76
+ password=args.password,
77
+ key_filename=args.key_filename,
78
+ command=args.command,
79
+ pause_on_exit=False,
80
+ )
81
+ if not success:
86
82
  sys.exit(1)
87
- except KeyboardInterrupt:
88
- pass
89
83
  return
90
84
 
91
85
  # Local mode
@@ -0,0 +1,4 @@
1
+ from monitorify.remote.connector import RemoteConnector
2
+ from monitorify.remote.addRemote import add_remote, list_hosts, remove_host, handle_host_action
3
+
4
+ __all__ = ["RemoteConnector", "add_remote", "list_hosts", "remove_host", "handle_host_action"]
@@ -0,0 +1,157 @@
1
+ import getpass
2
+ import os
3
+ from typing import Optional
4
+ from monitorify.storage.db import Database
5
+
6
+
7
+ def add_remote(
8
+ ip: Optional[str] = None,
9
+ port: Optional[int] = None,
10
+ username: Optional[str] = None,
11
+ password: Optional[str] = None,
12
+ key_filename: Optional[str] = None,
13
+ db: Optional[Database] = None,
14
+ ) -> int:
15
+ """Prompt for remote host details if not provided, and save to database."""
16
+ if not ip:
17
+ ip = input("Enter IP address: ").strip()
18
+ if not ip:
19
+ print("Error: IP address cannot be empty.")
20
+ return -1
21
+
22
+ if port is None:
23
+ port_input = input("Enter port (default 22): ").strip()
24
+ port = int(port_input) if port_input.isdigit() else 22
25
+ elif isinstance(port, str):
26
+ port = int(port) if port.isdigit() else 22
27
+
28
+ if not (1 <= port <= 65535):
29
+ port = 22
30
+
31
+ if username is None:
32
+ username_input = input("Enter username (default root): ").strip()
33
+ username = username_input if username_input else "root"
34
+ else:
35
+ username = username.strip() or "root"
36
+
37
+ if password is None and key_filename is None:
38
+ password_input = getpass.getpass("Enter password (leave blank to use SSH keys): ").strip()
39
+ if password_input:
40
+ password = password_input
41
+ key_filename = None
42
+ else:
43
+ password = None
44
+ key_input = input("Enter key filename: ").strip()
45
+ key_filename = key_input if key_input else None
46
+ else:
47
+ password = password if password else None
48
+ key_filename = key_filename if key_filename else None
49
+
50
+ if key_filename:
51
+ key_filename = os.path.expanduser(key_filename)
52
+
53
+ close_db = False
54
+ if db is None:
55
+ db = Database()
56
+ close_db = True
57
+
58
+ try:
59
+ host_id = db.save_host(
60
+ ip=ip,
61
+ port=port,
62
+ username=username,
63
+ password=password,
64
+ key_filename=key_filename,
65
+ )
66
+ if host_id != -1:
67
+ print(f"Host '{username}@{ip}:{port}' saved to database (ID: {host_id}).")
68
+ else:
69
+ print("Failed to save host to database.")
70
+ return host_id
71
+ finally:
72
+ if close_db:
73
+ db.disconnect()
74
+
75
+
76
+ def list_hosts(db: Optional[Database] = None) -> list[dict]:
77
+ """List all saved remote hosts."""
78
+ close_db = False
79
+ if db is None:
80
+ db = Database()
81
+ close_db = True
82
+
83
+ try:
84
+ hosts = db.get_hosts()
85
+ if not hosts:
86
+ print("No saved hosts found.")
87
+ return []
88
+
89
+ print(f"{'ID':<5} {'Host':<35} {'Auth':<20}")
90
+ print("-" * 65)
91
+ for h in hosts:
92
+ host_str = f"{h['username']}@{h['ip']}:{h['port']}"
93
+ auth_str = f"Key: {h['key_filename']}" if h['key_filename'] else ("Password: ***" if h['password'] else "None")
94
+ print(f"{h['id']:<5} {host_str:<35} {auth_str:<20}")
95
+ return hosts
96
+ finally:
97
+ if close_db:
98
+ db.disconnect()
99
+
100
+
101
+ def remove_host(host_id: Optional[int] = None, db: Optional[Database] = None) -> bool:
102
+ """Remove a saved remote host by ID."""
103
+ close_db = False
104
+ if db is None:
105
+ db = Database()
106
+ close_db = True
107
+
108
+ try:
109
+ if host_id is None:
110
+ hosts = db.get_hosts()
111
+ if not hosts:
112
+ print("No saved hosts to remove.")
113
+ return False
114
+ list_hosts(db=db)
115
+ id_input = input("Enter host ID to remove: ").strip()
116
+ if not id_input.isdigit():
117
+ print("Invalid host ID.")
118
+ return False
119
+ host_id = int(id_input)
120
+
121
+ if db.delete_host(host_id):
122
+ print(f"Host ID {host_id} successfully deleted.")
123
+ return True
124
+ else:
125
+ print(f"Failed to delete host ID {host_id}.")
126
+ return False
127
+ finally:
128
+ if close_db:
129
+ db.disconnect()
130
+
131
+
132
+ def handle_host_action(action: str, args=None) -> None:
133
+ """Handle --host subcommands."""
134
+ action = (action or "").lower()
135
+ if action == "add":
136
+ ip = getattr(args, "ip", None)
137
+ port = getattr(args, "port", None)
138
+ username = getattr(args, "username", None)
139
+ password = getattr(args, "password", None)
140
+ key_filename = getattr(args, "key_filename", None)
141
+ add_remote(
142
+ ip=ip,
143
+ port=port,
144
+ username=username,
145
+ password=password,
146
+ key_filename=key_filename,
147
+ )
148
+ elif action == "list":
149
+ list_hosts()
150
+ elif action in ("remove", "delete"):
151
+ remove_host()
152
+ else:
153
+ print(f"Unknown host action: '{action}'. Supported actions: add, list, remove")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ add_remote()
@@ -1,5 +1,7 @@
1
+ import os
1
2
  import paramiko
2
3
 
4
+
3
5
  class RemoteConnector:
4
6
  def __init__(
5
7
  self,
@@ -8,18 +10,27 @@ class RemoteConnector:
8
10
  username: str | None = None,
9
11
  password: str | None = None,
10
12
  key_filename: str | None = None,
13
+ timeout: float = 10.0,
11
14
  ):
12
15
  self.host = host
13
16
  self.port = port
14
17
  self.username = username
15
18
  self.password = password
16
- self.key_filename = key_filename
19
+ self.key_filename = os.path.expanduser(key_filename) if key_filename else None
20
+ self.timeout = timeout
17
21
 
18
- def connect(self):
22
+ def connect(self) -> paramiko.SSHClient:
19
23
  client = paramiko.SSHClient()
20
24
  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)
25
+ try:
26
+ client.connect(
27
+ hostname=self.host,
28
+ port=self.port,
29
+ username=self.username,
30
+ password=self.password,
31
+ key_filename=self.key_filename,
32
+ timeout=self.timeout,
33
+ )
23
34
  except paramiko.AuthenticationException as e:
24
35
  raise ConnectionError(f"Authentication failed: {e}")
25
36
  except paramiko.SSHException as e:
@@ -32,7 +43,7 @@ class RemoteConnector:
32
43
  """Check if monitorify is installed on the remote host."""
33
44
  try:
34
45
  _, stdout, _ = client.exec_command(
35
- "bash -l -c 'command -v monitorify >/dev/null 2>&1 || python3 -c \"import monitorify\" >/dev/null 2>&1'"
46
+ "bash -l -c 'export PATH=\"$HOME/.local/bin:$PATH\"; command -v monitorify >/dev/null 2>&1 || [ -x \"$HOME/.local/bin/monitorify\" ] || python3 -c \"import monitorify\" >/dev/null 2>&1'"
36
47
  )
37
48
  return stdout.channel.recv_exit_status() == 0
38
49
  except Exception:
@@ -51,7 +62,6 @@ class RemoteConnector:
51
62
 
52
63
  def run_interactive(self, client: paramiko.SSHClient, command: str = "monitorify") -> None:
53
64
  """Run a command interactively on the remote host with a PTY."""
54
- import os
55
65
  import select
56
66
  import shutil
57
67
  import signal
@@ -81,8 +91,9 @@ class RemoteConnector:
81
91
  except Exception:
82
92
  pass
83
93
 
94
+ old_winch_handler = None
84
95
  try:
85
- signal.signal(signal.SIGWINCH, handle_resize)
96
+ old_winch_handler = signal.signal(signal.SIGWINCH, handle_resize)
86
97
  except Exception:
87
98
  pass
88
99
 
@@ -110,6 +121,69 @@ class RemoteConnector:
110
121
  except Exception:
111
122
  break
112
123
  finally:
124
+ if old_winch_handler is not None:
125
+ try:
126
+ signal.signal(signal.SIGWINCH, old_winch_handler)
127
+ except Exception:
128
+ pass
113
129
  if is_atty and old_tty is not None:
114
130
  termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty)
115
- channel.close()
131
+ channel.close()
132
+
133
+
134
+ def connect_and_run(
135
+ host: str,
136
+ port: int = 22,
137
+ username: str | None = None,
138
+ password: str | None = None,
139
+ key_filename: str | None = None,
140
+ command: str | None = None,
141
+ pause_on_exit: bool = False,
142
+ timeout: float = 10.0,
143
+ ) -> bool:
144
+ """Connects to a remote host and runs the interactive session."""
145
+ connector = RemoteConnector(
146
+ host=host,
147
+ port=port or 22,
148
+ username=username,
149
+ password=password,
150
+ key_filename=key_filename,
151
+ timeout=timeout,
152
+ )
153
+ try:
154
+ print(f"\nConnecting to {username or 'root'}@{host}:{port or 22}...")
155
+ client = connector.connect()
156
+ try:
157
+ if not command and not connector.is_installed(client):
158
+ print(f"Error: Monitorify is not installed on the remote host ({host}).")
159
+ response = input("Do you want to install it? (y/n): ").strip().lower()
160
+ if response == "y":
161
+ success = connector.install(client)
162
+ if not success:
163
+ print("Installation failed.")
164
+ if pause_on_exit:
165
+ input("Press Enter to return...")
166
+ return False
167
+ else:
168
+ if pause_on_exit:
169
+ input("Press Enter to return...")
170
+ return False
171
+
172
+ remote_cmd = command or "bash -l -c 'export PATH=\"$HOME/.local/bin:$PATH\"; monitorify || python3 -m monitorify'"
173
+ connector.run_interactive(client, command=remote_cmd)
174
+ return True
175
+ finally:
176
+ client.close()
177
+ except ConnectionError as e:
178
+ print(f"Connection failed: {e}")
179
+ if pause_on_exit:
180
+ input("Press Enter to return...")
181
+ return False
182
+ except KeyboardInterrupt:
183
+ print("\nSession cancelled.")
184
+ return False
185
+ except Exception as e:
186
+ print(f"Unexpected error: {e}")
187
+ if pause_on_exit:
188
+ input("Press Enter to return...")
189
+ return False