servly 0.3.0__py3-none-any.whl → 0.3.1__py3-none-any.whl
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.
- servly/cli.py +18 -1
- servly/logs.py +19 -12
- servly/service.py +136 -2
- {servly-0.3.0.dist-info → servly-0.3.1.dist-info}/METADATA +2 -1
- servly-0.3.1.dist-info/RECORD +9 -0
- servly-0.3.0.dist-info/RECORD +0 -9
- {servly-0.3.0.dist-info → servly-0.3.1.dist-info}/WHEEL +0 -0
- {servly-0.3.0.dist-info → servly-0.3.1.dist-info}/entry_points.txt +0 -0
- {servly-0.3.0.dist-info → servly-0.3.1.dist-info}/top_level.txt +0 -0
servly/cli.py
CHANGED
@@ -204,10 +204,27 @@ def handle_list(manager: ServiceManager) -> bool:
|
|
204
204
|
is_running = manager.is_running(name)
|
205
205
|
pid = manager.get_service_pid(name)
|
206
206
|
|
207
|
+
# 获取服务运行时间
|
208
|
+
uptime_seconds = manager.get_uptime(name) if is_running else None
|
209
|
+
uptime = manager.format_uptime(uptime_seconds) if is_running else "-"
|
210
|
+
|
211
|
+
# 获取 CPU 和内存使用情况
|
212
|
+
cpu_mem_stats = {}
|
213
|
+
if is_running:
|
214
|
+
stats = manager.get_process_stats(name)
|
215
|
+
cpu_mem_stats["cpu"] = manager.format_cpu_percent(stats["cpu_percent"])
|
216
|
+
cpu_mem_stats["memory"] = manager.format_memory(stats["memory_mb"], stats["memory_percent"])
|
217
|
+
else:
|
218
|
+
cpu_mem_stats["cpu"] = "0%"
|
219
|
+
cpu_mem_stats["memory"] = "0b"
|
220
|
+
|
207
221
|
services.append({
|
208
222
|
"name": name,
|
209
223
|
"status": "running" if is_running else "stopped",
|
210
|
-
"pid": pid
|
224
|
+
"pid": pid,
|
225
|
+
"uptime": uptime,
|
226
|
+
"cpu": cpu_mem_stats["cpu"],
|
227
|
+
"memory": cpu_mem_stats["memory"]
|
211
228
|
})
|
212
229
|
|
213
230
|
# 使用Rich表格显示服务列表
|
servly/logs.py
CHANGED
@@ -15,6 +15,7 @@ from rich.text import Text
|
|
15
15
|
from rich.panel import Panel
|
16
16
|
from rich.table import Table
|
17
17
|
from rich.live import Live
|
18
|
+
from rich import box
|
18
19
|
|
19
20
|
# 自定义Rich主题
|
20
21
|
custom_theme = Theme({
|
@@ -77,25 +78,31 @@ def print_success(message: str):
|
|
77
78
|
console.print(f"{Emojis.RUNNING} {message}", style="running")
|
78
79
|
|
79
80
|
def print_service_table(services: List[Dict]):
|
80
|
-
"""
|
81
|
-
table = Table(show_header=True, header_style="header", expand=True)
|
82
|
-
|
83
|
-
|
84
|
-
table.add_column("
|
81
|
+
"""打印服务状态表格,PM2风格紧凑布局"""
|
82
|
+
table = Table(show_header=True, header_style="header", expand=True, box=box.SIMPLE)
|
83
|
+
|
84
|
+
# PM2风格紧凑表头
|
85
|
+
table.add_column("name", style="cyan")
|
86
|
+
table.add_column("pid", justify="right")
|
87
|
+
table.add_column("uptime", justify="right")
|
88
|
+
table.add_column("cpu", justify="right")
|
89
|
+
table.add_column("mem", justify="right")
|
85
90
|
|
86
91
|
for service in services:
|
87
92
|
name = service["name"]
|
88
|
-
status = service["status"]
|
89
93
|
pid = service["pid"] or "-"
|
94
|
+
uptime = service.get("uptime", "-")
|
95
|
+
cpu = service.get("cpu", "0%")
|
96
|
+
memory = service.get("memory", "0b")
|
90
97
|
|
91
|
-
status_style = "running" if status == "running" else "stopped"
|
92
|
-
status_emoji = Emojis.RUNNING if status == "running" else Emojis.STOPPED
|
93
|
-
status_text = f"{status_emoji} {status.upper()}"
|
98
|
+
status_style = "running" if service["status"] == "running" else "stopped"
|
94
99
|
|
95
100
|
table.add_row(
|
96
|
-
name,
|
97
|
-
Text(
|
98
|
-
Text(str(
|
101
|
+
Text(name, style=status_style),
|
102
|
+
Text(str(pid), style=status_style),
|
103
|
+
Text(str(uptime), style=status_style),
|
104
|
+
Text(str(cpu), style=status_style),
|
105
|
+
Text(str(memory), style=status_style)
|
99
106
|
)
|
100
107
|
|
101
108
|
console.print(table)
|
servly/service.py
CHANGED
@@ -7,8 +7,10 @@ import signal
|
|
7
7
|
import yaml
|
8
8
|
import logging
|
9
9
|
import time
|
10
|
+
import psutil
|
10
11
|
from pathlib import Path
|
11
|
-
from
|
12
|
+
from datetime import datetime
|
13
|
+
from typing import Dict, Union, List, Optional, Any, Tuple
|
12
14
|
|
13
15
|
logger = logging.getLogger(__name__)
|
14
16
|
|
@@ -22,13 +24,35 @@ class ServiceManager:
|
|
22
24
|
self.servly_dir = Path(config_dir) / servly_dir
|
23
25
|
self.pid_dir = self.servly_dir / "pids"
|
24
26
|
self.log_dir = self.servly_dir / "logs"
|
27
|
+
# 存储服务启动时间,用于计算运行时长
|
28
|
+
self.start_times = {}
|
25
29
|
self._ensure_dirs()
|
26
30
|
self.services = self._load_config()
|
31
|
+
# 加载现有的服务启动时间
|
32
|
+
self._load_start_times()
|
27
33
|
|
28
34
|
def _ensure_dirs(self):
|
29
35
|
"""Create required directories if they don't exist."""
|
30
36
|
self.pid_dir.mkdir(parents=True, exist_ok=True)
|
31
37
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
38
|
+
|
39
|
+
def _load_start_times(self):
|
40
|
+
"""加载已运行服务的启动时间"""
|
41
|
+
for service_name in self.get_service_names():
|
42
|
+
if self.is_running(service_name):
|
43
|
+
# 尝试从文件获取启动时间,如果没有则使用当前时间
|
44
|
+
start_time_file = self.pid_dir / f"{service_name}.time"
|
45
|
+
if start_time_file.exists():
|
46
|
+
try:
|
47
|
+
with open(start_time_file, "r") as f:
|
48
|
+
timestamp = float(f.read().strip())
|
49
|
+
self.start_times[service_name] = timestamp
|
50
|
+
except (ValueError, IOError):
|
51
|
+
# 如果文件无法读取或格式不正确,使用当前时间
|
52
|
+
self.start_times[service_name] = time.time()
|
53
|
+
else:
|
54
|
+
# 如果没有时间文件,使用当前时间
|
55
|
+
self.start_times[service_name] = time.time()
|
32
56
|
|
33
57
|
def _load_config(self) -> Dict[str, Any]:
|
34
58
|
"""Load service configurations from servly.yml."""
|
@@ -83,10 +107,45 @@ class ServiceManager:
|
|
83
107
|
else:
|
84
108
|
# Clean up stale PID file
|
85
109
|
os.remove(pid_file)
|
110
|
+
# 删除相关的启动时间记录
|
111
|
+
if service_name in self.start_times:
|
112
|
+
del self.start_times[service_name]
|
113
|
+
start_time_file = self.pid_dir / f"{service_name}.time"
|
114
|
+
if start_time_file.exists():
|
115
|
+
os.remove(start_time_file)
|
86
116
|
return None
|
87
117
|
except (ValueError, FileNotFoundError):
|
88
118
|
return None
|
89
119
|
|
120
|
+
def get_uptime(self, service_name: str) -> Optional[float]:
|
121
|
+
"""获取服务运行时间(以秒为单位)"""
|
122
|
+
if service_name in self.start_times and self.is_running(service_name):
|
123
|
+
return time.time() - self.start_times[service_name]
|
124
|
+
return None
|
125
|
+
|
126
|
+
def format_uptime(self, uptime_seconds: Optional[float]) -> str:
|
127
|
+
"""将运行时间格式化为易读格式"""
|
128
|
+
if uptime_seconds is None:
|
129
|
+
return "-"
|
130
|
+
|
131
|
+
# 转换为整数秒
|
132
|
+
seconds = int(uptime_seconds)
|
133
|
+
|
134
|
+
# 计算天、小时、分钟、秒
|
135
|
+
days, seconds = divmod(seconds, 86400)
|
136
|
+
hours, seconds = divmod(seconds, 3600)
|
137
|
+
minutes, seconds = divmod(seconds, 60)
|
138
|
+
|
139
|
+
# 格式化为易读字符串
|
140
|
+
if days > 0:
|
141
|
+
return f"{days}d {hours}h"
|
142
|
+
elif hours > 0:
|
143
|
+
return f"{hours}h {minutes}m"
|
144
|
+
elif minutes > 0:
|
145
|
+
return f"{minutes}m {seconds}s"
|
146
|
+
else:
|
147
|
+
return f"{seconds}s"
|
148
|
+
|
90
149
|
def _is_process_running(self, pid: int) -> bool:
|
91
150
|
"""Check if process with given PID is running."""
|
92
151
|
try:
|
@@ -160,6 +219,15 @@ class ServiceManager:
|
|
160
219
|
with open(self.get_pid_file(service_name), 'w') as f:
|
161
220
|
f.write(str(process.pid))
|
162
221
|
|
222
|
+
# 记录启动时间
|
223
|
+
start_time = time.time()
|
224
|
+
self.start_times[service_name] = start_time
|
225
|
+
|
226
|
+
# 保存启动时间到文件
|
227
|
+
start_time_file = self.pid_dir / f"{service_name}.time"
|
228
|
+
with open(start_time_file, 'w') as f:
|
229
|
+
f.write(str(start_time))
|
230
|
+
|
163
231
|
logger.info(f"Started service '{service_name}' with PID {process.pid}")
|
164
232
|
return True
|
165
233
|
|
@@ -194,6 +262,15 @@ class ServiceManager:
|
|
194
262
|
if os.path.exists(pid_file):
|
195
263
|
os.remove(pid_file)
|
196
264
|
|
265
|
+
# 清理启动时间记录
|
266
|
+
if service_name in self.start_times:
|
267
|
+
del self.start_times[service_name]
|
268
|
+
|
269
|
+
# 删除启动时间文件
|
270
|
+
start_time_file = self.pid_dir / f"{service_name}.time"
|
271
|
+
if start_time_file.exists():
|
272
|
+
os.remove(start_time_file)
|
273
|
+
|
197
274
|
logger.info(f"Stopped service '{service_name}'")
|
198
275
|
return True
|
199
276
|
|
@@ -202,6 +279,16 @@ class ServiceManager:
|
|
202
279
|
pid_file = self.get_pid_file(service_name)
|
203
280
|
if os.path.exists(pid_file):
|
204
281
|
os.remove(pid_file)
|
282
|
+
|
283
|
+
# 清理启动时间记录
|
284
|
+
if service_name in self.start_times:
|
285
|
+
del self.start_times[service_name]
|
286
|
+
|
287
|
+
# 删除启动时间文件
|
288
|
+
start_time_file = self.pid_dir / f"{service_name}.time"
|
289
|
+
if start_time_file.exists():
|
290
|
+
os.remove(start_time_file)
|
291
|
+
|
205
292
|
logger.info(f"Service '{service_name}' was not running")
|
206
293
|
return True
|
207
294
|
|
@@ -212,4 +299,51 @@ class ServiceManager:
|
|
212
299
|
def restart(self, service_name: str) -> bool:
|
213
300
|
"""Restart a service."""
|
214
301
|
self.stop(service_name)
|
215
|
-
return self.start(service_name)
|
302
|
+
return self.start(service_name)
|
303
|
+
|
304
|
+
def get_process_stats(self, service_name: str) -> Dict[str, Any]:
|
305
|
+
"""获取进程的 CPU 和内存使用情况"""
|
306
|
+
pid = self.get_service_pid(service_name)
|
307
|
+
stats = {"cpu_percent": None, "memory_percent": None, "memory_mb": None}
|
308
|
+
|
309
|
+
if pid:
|
310
|
+
try:
|
311
|
+
process = psutil.Process(pid)
|
312
|
+
# 获取 CPU 使用百分比 (非阻塞模式)
|
313
|
+
stats["cpu_percent"] = process.cpu_percent(interval=0)
|
314
|
+
|
315
|
+
# 获取内存使用情况
|
316
|
+
memory_info = process.memory_info()
|
317
|
+
stats["memory_mb"] = memory_info.rss / (1024 * 1024) # 转换为 MB
|
318
|
+
|
319
|
+
# 计算内存使用百分比
|
320
|
+
stats["memory_percent"] = process.memory_percent()
|
321
|
+
|
322
|
+
return stats
|
323
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
324
|
+
# 如果进程已不存在或无法访问,返回默认值
|
325
|
+
pass
|
326
|
+
|
327
|
+
return stats
|
328
|
+
|
329
|
+
def format_cpu_percent(self, cpu_percent: Optional[float]) -> str:
|
330
|
+
"""格式化 CPU 使用百分比"""
|
331
|
+
if cpu_percent is None:
|
332
|
+
return "0%"
|
333
|
+
return f"{cpu_percent:.1f}%"
|
334
|
+
|
335
|
+
def format_memory(self, memory_mb: Optional[float], memory_percent: Optional[float]) -> str:
|
336
|
+
"""格式化内存使用情况"""
|
337
|
+
if memory_mb is None:
|
338
|
+
return "0b"
|
339
|
+
|
340
|
+
# 如果小于 1MB,显示为 KB
|
341
|
+
if memory_mb < 1:
|
342
|
+
return f"{int(memory_mb * 1024)}kb"
|
343
|
+
|
344
|
+
# 如果大于 1GB,显示为 GB
|
345
|
+
if memory_mb > 1024:
|
346
|
+
return f"{memory_mb/1024:.1f}gb"
|
347
|
+
|
348
|
+
# 否则显示为 MB
|
349
|
+
return f"{int(memory_mb)}mb"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: servly
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.1
|
4
4
|
Summary: simple process manager
|
5
5
|
Author-email: simpxx <simpxx@gmail.com>
|
6
6
|
License: MIT
|
@@ -10,6 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
11
11
|
Requires-Python: >=3.12
|
12
12
|
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: psutil>=7.0.0
|
13
14
|
Requires-Dist: pyyaml>=6.0.2
|
14
15
|
Requires-Dist: rich>=14.0.0
|
15
16
|
|
@@ -0,0 +1,9 @@
|
|
1
|
+
servly/__init__.py,sha256=HT8I94RyP6_gNoVJWOVSkeOEQIl3QnT60zCP2PJVon0,73
|
2
|
+
servly/cli.py,sha256=2x8jI8k24_U6JiNgTbHcoJDjRXuC7fgVDG1rEprc-tw,10337
|
3
|
+
servly/logs.py,sha256=MWnQbSR_PjLRDm9JLIO3uvEkw57DZey0Ocbu6glFJpc,8109
|
4
|
+
servly/service.py,sha256=-qm7slEELm8PG_NErviezI6iKuG08FID2wvXSc1NBYg,13722
|
5
|
+
servly-0.3.1.dist-info/METADATA,sha256=NHsI2XN3X13y_8NmLjj-KNJeWzX16U278tzSlzEHtoI,2687
|
6
|
+
servly-0.3.1.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
7
|
+
servly-0.3.1.dist-info/entry_points.txt,sha256=enHpE2d9DYdzfIWBcnIjuIkcNVk118xghL0AeTUL0Yg,43
|
8
|
+
servly-0.3.1.dist-info/top_level.txt,sha256=JLi7GX0KwWF-mh8i1-lVJ2q4SO1KfI2CNxQk_VNfNZE,7
|
9
|
+
servly-0.3.1.dist-info/RECORD,,
|
servly-0.3.0.dist-info/RECORD
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
servly/__init__.py,sha256=HT8I94RyP6_gNoVJWOVSkeOEQIl3QnT60zCP2PJVon0,73
|
2
|
-
servly/cli.py,sha256=YmaKXHRZm77MJnMZ400EkuNayjlBw5UDfM5Y6T2z5x0,9583
|
3
|
-
servly/logs.py,sha256=pGU91fnWKHL5NprGrke7jNrLea23TXr3K3AF7J1KEQM,7802
|
4
|
-
servly/service.py,sha256=c4UrS1smK3NqEsERbCwjQUo1SKSQb1TaIbAzM1lrHeM,8253
|
5
|
-
servly-0.3.0.dist-info/METADATA,sha256=PD-b0BWdppEy69aREDOCDfWvpYf6G5elvHNF28MdPhY,2658
|
6
|
-
servly-0.3.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
7
|
-
servly-0.3.0.dist-info/entry_points.txt,sha256=enHpE2d9DYdzfIWBcnIjuIkcNVk118xghL0AeTUL0Yg,43
|
8
|
-
servly-0.3.0.dist-info/top_level.txt,sha256=JLi7GX0KwWF-mh8i1-lVJ2q4SO1KfI2CNxQk_VNfNZE,7
|
9
|
-
servly-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|