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 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
- table.add_column("名称", style="cyan")
83
- table.add_column("状态")
84
- table.add_column("PID")
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(status_text, style=status_style),
98
- Text(str(pid), style=status_style)
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 typing import Dict, Union, List, Optional, Any
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.0
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,,
@@ -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