monitorify 1.0.3__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.
- {monitorify-1.0.3 → monitorify-1.1.0}/PKG-INFO +4 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/README.md +1 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/pyproject.toml +4 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/__init__.py +1 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config.py +1 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/main.py +28 -34
- monitorify-1.1.0/src/monitorify/remote/__init__.py +4 -0
- monitorify-1.1.0/src/monitorify/remote/addRemote.py +157 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/remote/connector.py +82 -8
- monitorify-1.1.0/src/monitorify/remote/install.sh +81 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/storage/db.py +148 -2
- monitorify-1.1.0/src/monitorify/storage/keyring_helper.py +48 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/components/menu.py +95 -54
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/css/tui.css +64 -6
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/tui.py +86 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/programmListWidget.py +71 -8
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/PKG-INFO +4 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/SOURCES.txt +7 -1
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/requires.txt +2 -0
- monitorify-1.1.0/tests/test_menu.py +75 -0
- monitorify-1.1.0/tests/test_programm_list_search.py +138 -0
- monitorify-1.1.0/tests/test_remote_db.py +207 -0
- monitorify-1.0.3/src/monitorify/remote/__init__.py +0 -3
- {monitorify-1.0.3 → monitorify-1.1.0}/setup.cfg +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/__main__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/manager.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/schema.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/worker.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config/configManager.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config_manager.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/daemon.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/DiskStats.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/cpuStats.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/networkStats.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/programmList.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/ramStats.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/storage/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/components/widgetManager.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/__init__.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/brailleGraph.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/cpuWidget.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/diskWidget.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/networkWidget.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/procInfoWidget.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/ramWidget.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/statusWidget.py +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/dependency_links.txt +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/entry_points.txt +0 -0
- {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monitorify
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A lightweight Linux terminal system monitor built with Textual
|
|
5
5
|
Author: Leon
|
|
6
6
|
License: MIT
|
|
@@ -9,6 +9,8 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
Requires-Dist: textual>=0.80.0
|
|
10
10
|
Requires-Dist: platformdirs>=4.0.0
|
|
11
11
|
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Requires-Dist: paramiko>=3.0.0
|
|
13
|
+
Requires-Dist: keyring>=24.0.0
|
|
12
14
|
|
|
13
15
|
# Monitorify
|
|
14
16
|
### A terminal montioring tool for linux
|
|
@@ -123,6 +125,7 @@ To use Monitorify remotely over SSH on your server, you can set the following pa
|
|
|
123
125
|
- `--username` – Set the username for the remote host.
|
|
124
126
|
- `--password` – Set the password for the SSH server.
|
|
125
127
|
- `--key_filename` – Set the path to an SSH key *(optional)*.
|
|
128
|
+
- `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
|
|
126
129
|
|
|
127
130
|
## Tech Stack
|
|
128
131
|
|
|
@@ -111,6 +111,7 @@ To use Monitorify remotely over SSH on your server, you can set the following pa
|
|
|
111
111
|
- `--username` – Set the username for the remote host.
|
|
112
112
|
- `--password` – Set the password for the SSH server.
|
|
113
113
|
- `--key_filename` – Set the path to an SSH key *(optional)*.
|
|
114
|
+
- `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
|
|
114
115
|
|
|
115
116
|
## Tech Stack
|
|
116
117
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "monitorify"
|
|
7
|
-
version = "1.0
|
|
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"]
|
|
@@ -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
|
|
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.
|
|
57
|
-
from monitorify.remote.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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,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(
|
|
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
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
REPO_URL="https://github.com/leonst036/Monitorify.git"
|
|
6
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
7
|
+
|
|
8
|
+
install_via_pipx() {
|
|
9
|
+
if command -v pipx >/dev/null 2>&1; then
|
|
10
|
+
echo "[1/3] Attempting installation via pipx..."
|
|
11
|
+
pipx ensurepath >/dev/null 2>&1 || true
|
|
12
|
+
if pipx install --force "git+${REPO_URL}"; then
|
|
13
|
+
echo "Successfully installed via pipx"
|
|
14
|
+
return 0
|
|
15
|
+
fi
|
|
16
|
+
fi
|
|
17
|
+
return 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
install_via_pip() {
|
|
21
|
+
echo "[2/3] Attempting installation via pip..."
|
|
22
|
+
if command -v pip >/dev/null 2>&1; then
|
|
23
|
+
if pip install --upgrade "git+${REPO_URL}" || pip install --break-system-packages --upgrade "git+${REPO_URL}"; then
|
|
24
|
+
echo "Successfully installed via pip"
|
|
25
|
+
return 0
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
if python3 -m pip --version >/dev/null 2>&1; then
|
|
29
|
+
if python3 -m pip install --upgrade "git+${REPO_URL}" || python3 -m pip install --break-system-packages --upgrade "git+${REPO_URL}"; then
|
|
30
|
+
echo "Successfully installed via python3 -m pip"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
install_deps() {
|
|
38
|
+
echo "[3/3] pipx and pip not found. Attempting to install package manager dependencies..."
|
|
39
|
+
SUDO_CMD=""
|
|
40
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
41
|
+
if command -v sudo >/dev/null 2>&1; then
|
|
42
|
+
SUDO_CMD="sudo"
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if command -v apt-get >/dev/null 2>&1; then
|
|
47
|
+
$SUDO_CMD apt-get update && $SUDO_CMD apt-get install -y pipx git || $SUDO_CMD apt-get install -y python3-pip python3-venv git
|
|
48
|
+
elif command -v dnf >/dev/null 2>&1; then
|
|
49
|
+
$SUDO_CMD dnf install -y pipx git || $SUDO_CMD dnf install -y python3-pip git
|
|
50
|
+
elif command -v pacman >/dev/null 2>&1; then
|
|
51
|
+
$SUDO_CMD pacman -Sy --noconfirm python-pipx git || $SUDO_CMD pacman -Sy --noconfirm python-pip git
|
|
52
|
+
elif command -v zypper >/dev/null 2>&1; then
|
|
53
|
+
$SUDO_CMD zypper install -y python3-pipx git || $SUDO_CMD zypper install -y python3-pip git
|
|
54
|
+
elif command -v apk >/dev/null 2>&1; then
|
|
55
|
+
$SUDO_CMD apk add pipx git || $SUDO_CMD apk add py3-pip git
|
|
56
|
+
else
|
|
57
|
+
echo "No supported package manager found."
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if install_via_pipx; then
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if install_via_pip; then
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if install_deps; then
|
|
71
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
72
|
+
if install_via_pipx; then
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
if install_via_pip; then
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo "All installation methods failed."
|
|
81
|
+
exit 1
|