servly 0.2.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
@@ -1,52 +1,40 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Command-line interface for Servly process manager.
4
+ 使用Rich库进行终端输出格式化
4
5
  """
5
6
  import os
6
7
  import sys
7
8
  import argparse
8
9
  import logging
9
10
  from pathlib import Path
10
- from typing import List, Optional
11
+ from typing import List, Dict, Optional
11
12
 
12
13
  from servly.service import ServiceManager
13
- from servly.logs import LogManager
14
- from servly.logs import Colors, Emojis # 导入颜色和表情符号定义
15
- from servly.logs import align_colored_text # 导入对齐工具函数
14
+ from servly.logs import LogManager, Emojis
15
+ from servly.logs import console, print_header, print_info, print_warning, print_error, print_success, print_service_table
16
16
 
17
- # 配置彩色日志记录器
18
- class ColoredFormatter(logging.Formatter):
19
- """添加颜色支持的日志格式化器"""
20
-
21
- def format(self, record):
22
- log_message = super().format(record)
23
-
24
- if record.levelno >= logging.ERROR:
25
- return f"{Emojis.ERROR} {Colors.BRIGHT_RED}{log_message}{Colors.RESET}"
26
- elif record.levelno >= logging.WARNING:
27
- return f"{Emojis.WARNING} {Colors.YELLOW}{log_message}{Colors.RESET}"
28
- elif record.levelno >= logging.INFO:
29
- return f"{Emojis.INFO} {Colors.GREEN}{log_message}{Colors.RESET}"
30
- else:
31
- return f"{Colors.BRIGHT_BLACK}{log_message}{Colors.RESET}"
17
+ from rich.logging import RichHandler
18
+ from rich.traceback import install
19
+ from rich.markup import escape
32
20
 
33
- # 配置日志
34
- handler = logging.StreamHandler()
35
- handler.setFormatter(ColoredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
36
- logger = logging.getLogger('servly')
37
- logger.setLevel(logging.INFO)
38
- logger.addHandler(handler)
21
+ # 安装Rich的异常格式化器
22
+ install()
39
23
 
40
- def print_header():
41
- """打印美化的 Servly 头部"""
42
- print(f"\n{Colors.CYAN}{Colors.BOLD}{'=' * 50}")
43
- print(f"{Emojis.SERVICE} SERVLY {Colors.BRIGHT_BLACK}- Modern Process Manager")
44
- print(f"{Colors.CYAN}{'=' * 50}{Colors.RESET}\n")
24
+ # 配置Rich日志处理
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format="%(message)s",
28
+ datefmt="[%X]",
29
+ handlers=[RichHandler(rich_tracebacks=True, markup=True)]
30
+ )
31
+
32
+ logger = logging.getLogger("servly")
45
33
 
46
34
  def setup_arg_parser() -> argparse.ArgumentParser:
47
35
  """设置命令行参数解析器"""
48
36
  parser = argparse.ArgumentParser(
49
- description=f"{Colors.CYAN}{Colors.BOLD}{Emojis.SERVICE} Servly - Modern process manager{Colors.RESET}",
37
+ description=f"{Emojis.SERVICE} Servly - Modern process manager",
50
38
  formatter_class=argparse.RawDescriptionHelpFormatter
51
39
  )
52
40
 
@@ -91,30 +79,30 @@ def handle_start(manager: ServiceManager, service_name: str) -> bool:
91
79
  if service_name == 'all':
92
80
  service_names = manager.get_service_names()
93
81
  if not service_names:
94
- print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
82
+ print_warning("配置中没有定义任何服务。")
95
83
  return False
96
84
 
97
- print(f"{Emojis.START} {Colors.BRIGHT_GREEN}正在启动所有服务...{Colors.RESET}")
85
+ console.print(f"{Emojis.START} 正在启动所有服务...", style="running")
98
86
  success = True
99
87
  for name in service_names:
100
88
  if not manager.start(name):
101
- print(f"{Emojis.ERROR} {Colors.RED}启动服务 '{name}' 失败{Colors.RESET}")
89
+ print_error(f"启动服务 '{name}' 失败")
102
90
  success = False
103
91
  else:
104
- print(f"{Emojis.RUNNING} {Colors.GREEN}服务 '{name}' 已成功启动{Colors.RESET}")
92
+ print_success(f"服务 '{name}' 已成功启动")
105
93
 
106
94
  if success:
107
- print(f"\n{Emojis.RUNNING} {Colors.BRIGHT_GREEN}所有服务已成功启动!{Colors.RESET}")
95
+ print_success("所有服务已成功启动!")
108
96
  else:
109
- print(f"\n{Emojis.WARNING} {Colors.YELLOW}有些服务启动失败,请检查日志获取详情。{Colors.RESET}")
97
+ print_warning("有些服务启动失败,请检查日志获取详情。")
110
98
 
111
99
  return success
112
100
  else:
113
101
  result = manager.start(service_name)
114
102
  if result:
115
- print(f"{Emojis.RUNNING} {Colors.GREEN}服务 '{service_name}' 已成功启动{Colors.RESET}")
103
+ print_success(f"服务 '{service_name}' 已成功启动")
116
104
  else:
117
- print(f"{Emojis.ERROR} {Colors.RED}启动服务 '{service_name}' 失败{Colors.RESET}")
105
+ print_error(f"启动服务 '{service_name}' 失败")
118
106
  return result
119
107
 
120
108
  def handle_stop(manager: ServiceManager, service_name: str) -> bool:
@@ -122,30 +110,30 @@ def handle_stop(manager: ServiceManager, service_name: str) -> bool:
122
110
  if service_name == 'all':
123
111
  service_names = manager.get_running_services()
124
112
  if not service_names:
125
- print(f"{Emojis.INFO} {Colors.BRIGHT_BLACK}当前没有正在运行的服务。{Colors.RESET}")
113
+ console.print(f"{Emojis.INFO} 当前没有正在运行的服务。", style="dim")
126
114
  return True
127
115
 
128
- print(f"{Emojis.STOP} {Colors.YELLOW}正在停止所有服务...{Colors.RESET}")
116
+ console.print(f"{Emojis.STOP} 正在停止所有服务...", style="warning")
129
117
  success = True
130
118
  for name in service_names:
131
119
  if not manager.stop(name):
132
- print(f"{Emojis.ERROR} {Colors.RED}停止服务 '{name}' 失败{Colors.RESET}")
120
+ print_error(f"停止服务 '{name}' 失败")
133
121
  success = False
134
122
  else:
135
- print(f"{Emojis.STOPPED} {Colors.YELLOW}服务 '{name}' 已停止{Colors.RESET}")
123
+ console.print(f"{Emojis.STOPPED} 服务 '{name}' 已停止", style="stopped")
136
124
 
137
125
  if success:
138
- print(f"\n{Emojis.STOPPED} {Colors.YELLOW}所有服务已成功停止!{Colors.RESET}")
126
+ console.print(f"\n{Emojis.STOPPED} 所有服务已成功停止!", style="warning")
139
127
  else:
140
- print(f"\n{Emojis.WARNING} {Colors.YELLOW}有些服务停止失败,请检查日志获取详情。{Colors.RESET}")
128
+ print_warning("有些服务停止失败,请检查日志获取详情。")
141
129
 
142
130
  return success
143
131
  else:
144
132
  result = manager.stop(service_name)
145
133
  if result:
146
- print(f"{Emojis.STOPPED} {Colors.YELLOW}服务 '{service_name}' 已成功停止{Colors.RESET}")
134
+ console.print(f"{Emojis.STOPPED} 服务 '{service_name}' 已成功停止", style="warning")
147
135
  else:
148
- print(f"{Emojis.ERROR} {Colors.RED}停止服务 '{service_name}' 失败{Colors.RESET}")
136
+ print_error(f"停止服务 '{service_name}' 失败")
149
137
  return result
150
138
 
151
139
  def handle_restart(manager: ServiceManager, service_name: str) -> bool:
@@ -153,30 +141,30 @@ def handle_restart(manager: ServiceManager, service_name: str) -> bool:
153
141
  if service_name == 'all':
154
142
  service_names = manager.get_service_names()
155
143
  if not service_names:
156
- print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
144
+ print_warning("配置中没有定义任何服务。")
157
145
  return False
158
146
 
159
- print(f"{Emojis.RESTART} {Colors.MAGENTA}正在重启所有服务...{Colors.RESET}")
147
+ console.print(f"{Emojis.RESTART} 正在重启所有服务...", style="restart")
160
148
  success = True
161
149
  for name in service_names:
162
150
  if not manager.restart(name):
163
- print(f"{Emojis.ERROR} {Colors.RED}重启服务 '{name}' 失败{Colors.RESET}")
151
+ print_error(f"重启服务 '{name}' 失败")
164
152
  success = False
165
153
  else:
166
- print(f"{Emojis.RUNNING} {Colors.MAGENTA}服务 '{name}' 已成功重启{Colors.RESET}")
154
+ console.print(f"{Emojis.RUNNING} 服务 '{name}' 已成功重启", style="restart")
167
155
 
168
156
  if success:
169
- print(f"\n{Emojis.RUNNING} {Colors.BRIGHT_GREEN}所有服务已成功重启!{Colors.RESET}")
157
+ print_success("所有服务已成功重启!")
170
158
  else:
171
- print(f"\n{Emojis.WARNING} {Colors.YELLOW}有些服务重启失败,请检查日志获取详情。{Colors.RESET}")
159
+ print_warning("有些服务重启失败,请检查日志获取详情。")
172
160
 
173
161
  return success
174
162
  else:
175
163
  result = manager.restart(service_name)
176
164
  if result:
177
- print(f"{Emojis.RUNNING} {Colors.MAGENTA}服务 '{service_name}' 已成功重启{Colors.RESET}")
165
+ console.print(f"{Emojis.RUNNING} 服务 '{service_name}' 已成功重启", style="restart")
178
166
  else:
179
- print(f"{Emojis.ERROR} {Colors.RED}重启服务 '{service_name}' 失败{Colors.RESET}")
167
+ print_error(f"重启服务 '{service_name}' 失败")
180
168
  return result
181
169
 
182
170
  def handle_log(manager: ServiceManager, log_manager: LogManager, args) -> bool:
@@ -188,15 +176,15 @@ def handle_log(manager: ServiceManager, log_manager: LogManager, args) -> bool:
188
176
  if service_name == 'all':
189
177
  services = manager.get_service_names()
190
178
  if not services:
191
- print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
179
+ print_warning("配置中没有定义任何服务。")
192
180
  return False
193
181
  else:
194
182
  if service_name not in manager.get_service_names():
195
- print(f"{Emojis.ERROR} {Colors.RED}服务 '{service_name}' 在配置中未找到。{Colors.RESET}")
183
+ print_error(f"服务 '{service_name}' 在配置中未找到。")
196
184
  return False
197
185
  services = [service_name]
198
186
 
199
- # 已经在 LogManager 中添加了彩色输出
187
+ # 使用LogManager查看日志
200
188
  log_manager.tail_logs(services, follow=follow, lines=lines)
201
189
  return True
202
190
 
@@ -205,50 +193,53 @@ def handle_list(manager: ServiceManager) -> bool:
205
193
  service_names = manager.get_service_names()
206
194
 
207
195
  if not service_names:
208
- print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
196
+ print_warning("配置中没有定义任何服务。")
209
197
  return True
210
198
 
211
- # 表头
212
- print(f"\n{Colors.CYAN}{Colors.BOLD}{Emojis.SERVICE} 服务列表{Colors.RESET}")
213
- print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}")
214
-
215
- # 列标题
216
- print(f"{Colors.BOLD}{align_colored_text(' 名称', 25)} {align_colored_text('状态', 20)} {align_colored_text('PID', 10)}{Colors.RESET}")
217
- print(f"{Colors.BRIGHT_BLACK}{'─' * 60}{Colors.RESET}")
199
+ print_header("服务列表")
218
200
 
219
- # 服务列表
201
+ # 构建服务列表数据
202
+ services = []
220
203
  for name in service_names:
221
204
  is_running = manager.is_running(name)
222
- pid = manager.get_service_pid(name) or '-'
205
+ pid = manager.get_service_pid(name)
223
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 = {}
224
213
  if is_running:
225
- status_text = f"{Emojis.RUNNING} {Colors.GREEN}RUNNING{Colors.RESET}"
226
- pid_text = f"{Colors.GREEN}{pid}{Colors.RESET}"
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"])
227
217
  else:
228
- status_text = f"{Emojis.STOPPED} {Colors.BRIGHT_BLACK}STOPPED{Colors.RESET}"
229
- pid_text = f"{Colors.BRIGHT_BLACK}{pid}{Colors.RESET}"
230
-
231
- # 为不同服务使用不同颜色
232
- service_color = Colors.CYAN if is_running else Colors.BRIGHT_BLACK
233
- service_text = f"{service_color}{name}{Colors.RESET}"
218
+ cpu_mem_stats["cpu"] = "0%"
219
+ cpu_mem_stats["memory"] = "0b"
234
220
 
235
- # 使用对齐辅助函数确保正确对齐包含颜色代码的文本
236
- aligned_service = align_colored_text(f" {service_text}", 25)
237
- aligned_status = align_colored_text(status_text, 20)
238
- aligned_pid = align_colored_text(pid_text, 10)
239
-
240
- print(f"{aligned_service}{aligned_status}{aligned_pid}")
221
+ services.append({
222
+ "name": name,
223
+ "status": "running" if is_running else "stopped",
224
+ "pid": pid,
225
+ "uptime": uptime,
226
+ "cpu": cpu_mem_stats["cpu"],
227
+ "memory": cpu_mem_stats["memory"]
228
+ })
229
+
230
+ # 使用Rich表格显示服务列表
231
+ print_service_table(services)
241
232
 
242
- print(f"\n{Colors.BRIGHT_BLACK}配置文件: {manager.config_path}{Colors.RESET}")
243
- print(f"{Colors.BRIGHT_BLACK}运行中服务: {len(manager.get_running_services())}/{len(service_names)}{Colors.RESET}")
244
- print()
233
+ console.print(f"[dim]配置文件: {manager.config_path}[/]")
234
+ console.print(f"[dim]运行中服务: {len(manager.get_running_services())}/{len(service_names)}[/]")
235
+ console.print()
245
236
 
246
237
  return True
247
238
 
248
239
  def main():
249
240
  """CLI 应用程序入口点"""
250
241
  # 显示头部
251
- print_header()
242
+ print_header("SERVLY - Modern Process Manager")
252
243
 
253
244
  parser = setup_arg_parser()
254
245
  args = parser.parse_args()
@@ -261,7 +252,7 @@ def main():
261
252
  try:
262
253
  service_manager = ServiceManager(config_path=args.config)
263
254
  except Exception as e:
264
- print(f"{Emojis.ERROR} {Colors.RED}加载配置文件时出错: {e}{Colors.RESET}")
255
+ print_error(f"加载配置文件时出错: {escape(str(e))}")
265
256
  return 1
266
257
 
267
258
  # 创建日志管理器
@@ -283,10 +274,11 @@ def main():
283
274
  parser.print_help()
284
275
  return 1
285
276
  except KeyboardInterrupt:
286
- print(f"\n{Emojis.STOP} {Colors.BRIGHT_BLACK}操作被用户中断{Colors.RESET}")
277
+ console.print(f"\n{Emojis.STOP} 操作被用户中断", style="dim")
287
278
  return 1
288
279
  except Exception as e:
289
- print(f"\n{Emojis.ERROR} {Colors.RED}执行命令时出错: {e}{Colors.RESET}")
280
+ print_error(f"执行命令时出错: {escape(str(e))}")
281
+ logger.exception("命令执行异常")
290
282
  return 1
291
283
 
292
284
  return 0 if success else 1
servly/logs.py CHANGED
@@ -1,56 +1,44 @@
1
1
  """
2
2
  Log management functionality for Servly.
3
+ 使用Rich库进行日志格式化和展示,实现PM2风格的日志效果。
3
4
  """
4
5
  import os
5
6
  import sys
6
7
  import time
7
- import select
8
- import fcntl
9
- import termios
10
- import subprocess
11
8
  import re
12
9
  from pathlib import Path
13
10
  from typing import List, Dict, Optional, Tuple
14
- import random
15
11
 
12
+ from rich.console import Console
13
+ from rich.theme import Theme
14
+ from rich.text import Text
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from rich.live import Live
18
+ from rich import box
16
19
 
17
- # ANSI 颜色代码
18
- class Colors:
19
- RESET = "\033[0m"
20
- BOLD = "\033[1m"
21
- UNDERLINE = "\033[4m"
22
-
23
- BLACK = "\033[30m"
24
- RED = "\033[31m"
25
- GREEN = "\033[32m"
26
- YELLOW = "\033[33m"
27
- BLUE = "\033[34m"
28
- MAGENTA = "\033[35m"
29
- CYAN = "\033[36m"
30
- WHITE = "\033[37m"
31
-
32
- BRIGHT_BLACK = "\033[90m"
33
- BRIGHT_RED = "\033[91m"
34
- BRIGHT_GREEN = "\033[92m"
35
- BRIGHT_YELLOW = "\033[93m"
36
- BRIGHT_BLUE = "\033[94m"
37
- BRIGHT_MAGENTA = "\033[95m"
38
- BRIGHT_CYAN = "\033[96m"
39
- BRIGHT_WHITE = "\033[97m"
40
-
41
- # 背景色
42
- BG_BLACK = "\033[40m"
43
- BG_RED = "\033[41m"
44
- BG_GREEN = "\033[42m"
45
- BG_YELLOW = "\033[43m"
46
- BG_BLUE = "\033[44m"
47
- BG_MAGENTA = "\033[45m"
48
- BG_CYAN = "\033[46m"
49
- BG_WHITE = "\033[47m"
20
+ # 自定义Rich主题
21
+ custom_theme = Theme({
22
+ "warning": "yellow",
23
+ "error": "bold red",
24
+ "info": "green",
25
+ "dim": "dim",
26
+ "stdout_service": "green",
27
+ "stderr_service": "red",
28
+ "header": "cyan bold",
29
+ "subheader": "bright_black",
30
+ "running": "green",
31
+ "stopped": "bright_black",
32
+ "restart": "magenta",
33
+ "separator": "cyan",
34
+ })
50
35
 
36
+ # 创建Rich控制台对象
37
+ console = Console(theme=custom_theme)
51
38
 
52
39
  # 服务相关的 emoji
53
40
  class Emojis:
41
+ """服务状态相关的emoji图标"""
54
42
  SERVICE = "🔧"
55
43
  START = "🟢"
56
44
  STOP = "🔴"
@@ -66,132 +54,108 @@ class Emojis:
66
54
  STOPPED = "⛔"
67
55
  LOADING = "⏳"
68
56
 
57
+ # Rich格式化输出工具函数
58
+ def print_header(title: str):
59
+ """打印美化的标题"""
60
+ console.print()
61
+ console.rule(f"[header]{Emojis.SERVICE} {title}[/]", style="separator")
62
+ console.print()
69
63
 
70
- # 处理彩色文本对齐的辅助函数
71
- def strip_ansi_codes(text: str) -> str:
72
- """删除字符串中的所有 ANSI 转义代码"""
73
- ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
74
- return ansi_escape.sub('', text)
64
+ def print_info(message: str):
65
+ """打印信息消息"""
66
+ console.print(f"{Emojis.INFO} {message}", style="info")
75
67
 
76
- def get_visible_length(text: str) -> int:
77
- """获取文本在终端中的可见长度(排除 ANSI 代码和宽字符)"""
78
- text = strip_ansi_codes(text)
79
- # 处理中文等宽字符(在大多数终端中占用两个字符宽度)
80
- length = 0
81
- for char in text:
82
- if ord(char) > 127: # 简单判断是否是非ASCII字符
83
- length += 2
84
- else:
85
- length += 1
86
- return length
68
+ def print_warning(message: str):
69
+ """打印警告消息"""
70
+ console.print(f"{Emojis.WARNING} {message}", style="warning")
87
71
 
88
- def align_colored_text(text: str, width: int, align='left') -> str:
89
- """
90
- 将彩色文本对齐到指定宽度
72
+ def print_error(message: str):
73
+ """打印错误消息"""
74
+ console.print(f"{Emojis.ERROR} {message}", style="error")
75
+
76
+ def print_success(message: str):
77
+ """打印成功消息"""
78
+ console.print(f"{Emojis.RUNNING} {message}", style="running")
79
+
80
+ def print_service_table(services: List[Dict]):
81
+ """打印服务状态表格,PM2风格紧凑布局"""
82
+ table = Table(show_header=True, header_style="header", expand=True, box=box.SIMPLE)
91
83
 
92
- Args:
93
- text: 可能包含 ANSI 颜色代码的文本
94
- width: 期望的显示宽度
95
- align: 对齐方式,'left', 'right' 或 'center'
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")
90
+
91
+ for service in services:
92
+ name = service["name"]
93
+ pid = service["pid"] or "-"
94
+ uptime = service.get("uptime", "-")
95
+ cpu = service.get("cpu", "0%")
96
+ memory = service.get("memory", "0b")
97
+
98
+ status_style = "running" if service["status"] == "running" else "stopped"
96
99
 
97
- Returns:
98
- 对齐后的文本,保留颜色代码
99
- """
100
- visible_length = get_visible_length(text)
101
- padding = max(0, width - visible_length)
100
+ table.add_row(
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)
106
+ )
102
107
 
103
- if align == 'right':
104
- return ' ' * padding + text
105
- elif align == 'center':
106
- left_padding = padding // 2
107
- right_padding = padding - left_padding
108
- return ' ' * left_padding + text + ' ' * right_padding
109
- else: # left alignment
110
- return text + ' ' * padding
108
+ console.print(table)
109
+ console.print()
111
110
 
112
111
 
113
112
  class LogManager:
114
- """Handles viewing and managing logs for servly services."""
113
+ """管理和显示服务日志"""
115
114
 
116
115
  def __init__(self, log_dir: Path):
117
116
  self.log_dir = log_dir
118
- # 为每个服务分配一个固定颜色,使日志更易读
119
- self.service_colors = {}
120
- self.available_colors = [
121
- Colors.GREEN, Colors.YELLOW, Colors.BLUE, Colors.MAGENTA, Colors.CYAN,
122
- Colors.BRIGHT_GREEN, Colors.BRIGHT_YELLOW, Colors.BRIGHT_BLUE,
123
- Colors.BRIGHT_MAGENTA, Colors.BRIGHT_CYAN
124
- ]
117
+ self.default_tail_lines = 15 # 默认展示最后15行日志
125
118
 
126
119
  def get_log_files(self, service_name: str) -> Dict[str, Path]:
127
- """Get the stdout and stderr log files for a service."""
120
+ """获取服务的stdoutstderr日志文件路径"""
128
121
  return {
129
122
  'stdout': self.log_dir / f"{service_name}-out.log",
130
123
  'stderr': self.log_dir / f"{service_name}-error.log"
131
124
  }
132
125
 
133
- def _get_service_color(self, service_name: str) -> str:
134
- """为服务分配一个固定的颜色"""
135
- if service_name not in self.service_colors:
136
- if not self.available_colors:
137
- # 如果颜色用完了,就随机分配
138
- self.service_colors[service_name] = random.choice([
139
- Colors.GREEN, Colors.YELLOW, Colors.BLUE, Colors.MAGENTA, Colors.CYAN
140
- ])
141
- else:
142
- # 从可用颜色中选择一个
143
- self.service_colors[service_name] = self.available_colors.pop(0)
144
- return self.service_colors[service_name]
145
-
146
- def _format_log_header(self, service: str, log_type: str) -> str:
147
- """格式化日志头部,带颜色和 emoji"""
148
- service_color = self._get_service_color(service)
149
- emoji = Emojis.STDOUT if log_type == 'stdout' else Emojis.STDERR
150
- type_color = Colors.GREEN if log_type == 'stdout' else Colors.YELLOW
151
-
152
- return (f"\n{Colors.BOLD}{Colors.WHITE}{Emojis.LOG} Log Stream: "
153
- f"{service_color}{service}{Colors.RESET} "
154
- f"{Colors.BRIGHT_BLACK}({type_color}{emoji} {log_type}{Colors.BRIGHT_BLACK}){Colors.RESET}\n"
155
- f"{Colors.BRIGHT_BLACK}{'─' * 60}{Colors.RESET}")
156
-
157
- def _format_log_line(self, service: str, log_type: str, line: str, show_timestamp: bool = True) -> str:
158
- """格式化单行日志,带颜色和 emoji"""
159
- service_color = self._get_service_color(service)
160
- timestamp = time.strftime("%Y-%m-%d %H:%M:%S") if show_timestamp else ""
126
+ def _parse_log_line(self, line: str) -> Tuple[str, str]:
127
+ """解析日志行,提取时间戳和内容"""
128
+ timestamp = ""
129
+ content = line.rstrip()
161
130
 
162
- # 根据日志类型选择颜色
163
- line_color = Colors.RESET
164
- if log_type == 'stderr' and ('error' in line.lower() or 'exception' in line.lower()):
165
- prefix = f"{Emojis.ERROR} "
166
- line_color = Colors.BRIGHT_RED
167
- elif log_type == 'stderr':
168
- prefix = f"{Emojis.WARNING} "
169
- line_color = Colors.YELLOW
131
+ # 尝试提取时间戳
132
+ timestamp_match = re.search(r'(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})', line)
133
+ if timestamp_match:
134
+ timestamp = timestamp_match.group(1)
135
+ # 移除行中已有的时间戳部分
136
+ content = line.replace(timestamp, "", 1).lstrip().rstrip()
170
137
  else:
171
- prefix = f"{Emojis.INFO} "
138
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
172
139
 
173
- # 格式化输出
174
- if show_timestamp:
175
- return (f"{Colors.BRIGHT_BLACK}[{Emojis.TIME} {timestamp}]{Colors.RESET} "
176
- f"{service_color}{service}{Colors.RESET} "
177
- f"{prefix}{line_color}{line.rstrip()}{Colors.RESET}")
178
- else:
179
- return f"{line_color}{line.rstrip()}{Colors.RESET}"
140
+ return timestamp, content
180
141
 
181
- def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = 10):
142
+ def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = None):
182
143
  """
183
- Display logs for specified services in real-time.
144
+ 显示服务日志
184
145
 
185
146
  Args:
186
- service_names: List of service names to show logs for
187
- follow: Whether to follow logs in real-time (like tail -f)
188
- lines: Number of recent lines to display initially
147
+ service_names: 要查看的服务名称列表
148
+ follow: 是否实时跟踪日志(类似tail -f
149
+ lines: 初始显示的行数,默认为self.default_tail_lines
189
150
  """
151
+ if lines is None:
152
+ lines = self.default_tail_lines
153
+
190
154
  if not service_names:
191
- print(f"{Emojis.WARNING} {Colors.YELLOW}No services specified for log viewing.{Colors.RESET}")
155
+ print_warning("未指定要查看日志的服务。")
192
156
  return
193
157
 
194
- # Check if the logs exist
158
+ # 检查日志文件是否存在
195
159
  log_files = []
196
160
  for service in service_names:
197
161
  service_logs = self.get_log_files(service)
@@ -199,52 +163,53 @@ class LogManager:
199
163
  if log_path.exists():
200
164
  log_files.append((service, log_type, log_path))
201
165
  else:
202
- service_color = self._get_service_color(service)
203
- print(f"{Emojis.WARNING} {Colors.YELLOW}No {log_type} logs found for {service_color}{service}{Colors.RESET}.")
166
+ style = "stderr_service" if log_type == "stderr" else "stdout_service"
167
+ console.print(f"{Emojis.WARNING} 未找到服务 [{style}]{service}[/] {log_type} 日志。", style="warning")
204
168
 
205
169
  if not log_files:
206
- print(f"{Emojis.WARNING} {Colors.YELLOW}No log files found for specified services.{Colors.RESET}")
170
+ print_warning("未找到指定服务的日志文件。")
207
171
  return
208
172
 
209
173
  if follow:
174
+ # 首先显示最后几行,然后再开始跟踪
175
+ self._display_recent_logs(log_files, lines)
210
176
  self._follow_logs(log_files)
211
177
  else:
212
178
  self._display_recent_logs(log_files, lines)
213
179
 
214
180
  def _display_recent_logs(self, log_files: List[Tuple[str, str, Path]], lines: int):
215
- """Display the most recent lines from log files."""
181
+ """显示最近的日志行"""
216
182
  for service, log_type, log_path in log_files:
217
- print(self._format_log_header(service, log_type))
183
+ # PM2风格的标题
184
+ console.print(f"\n[dim]{log_path} last {lines} lines:[/]")
185
+
218
186
  try:
219
187
  # 读取最后N行
220
188
  with open(log_path, 'r') as f:
221
189
  content = f.readlines()
222
190
  last_lines = content[-lines:] if len(content) >= lines else content
223
191
 
224
- # 打印每一行,增加格式
192
+ # 打印每一行,PM2格式
225
193
  for line in last_lines:
226
- print(self._format_log_line(service, log_type, line, show_timestamp=False))
194
+ timestamp, message = self._parse_log_line(line)
195
+ style = "stderr_service" if log_type == "stderr" else "stdout_service"
196
+ console.print(f"[{style}]{service}[/] | {timestamp}: {message}")
227
197
  except Exception as e:
228
- print(f"{Emojis.ERROR} {Colors.RED}Error reading logs: {str(e)}{Colors.RESET}")
198
+ print_error(f"读取日志文件出错: {str(e)}")
229
199
 
230
200
  def _follow_logs(self, log_files: List[Tuple[str, str, Path]]):
231
- """Follow logs in real-time, similar to tail -f."""
232
- # Dictionary to keep track of file positions
201
+ """实时跟踪日志(类似tail -f"""
233
202
  file_handlers = {}
234
203
 
235
204
  try:
236
- # Open all log files
205
+ # 打开所有日志文件
237
206
  for service, log_type, log_path in log_files:
238
207
  f = open(log_path, 'r')
239
- # Move to the end of the file
208
+ # 移动到文件末尾
240
209
  f.seek(0, os.SEEK_END)
241
210
  file_handlers[(service, log_type)] = f
242
211
 
243
- # 打印日志头部
244
- print(self._format_log_header(service, log_type))
245
-
246
- print(f"\n{Emojis.LOADING} {Colors.BRIGHT_BLACK}Following logs... (Ctrl+C to stop){Colors.RESET}")
247
- print(f"{Colors.BRIGHT_BLACK}{'─' * 60}{Colors.RESET}")
212
+ console.print(f"\n[dim]正在跟踪日志... (按Ctrl+C停止)[/]")
248
213
 
249
214
  while True:
250
215
  has_new_data = False
@@ -253,14 +218,16 @@ class LogManager:
253
218
  line = f.readline()
254
219
  if line:
255
220
  has_new_data = True
256
- print(self._format_log_line(service, log_type, line))
221
+ timestamp, message = self._parse_log_line(line)
222
+ style = "stderr_service" if log_type == "stderr" else "stdout_service"
223
+ console.print(f"[{style}]{service}[/] | {timestamp}: {message}")
257
224
 
258
225
  if not has_new_data:
259
226
  time.sleep(0.1)
260
227
 
261
228
  except KeyboardInterrupt:
262
- print(f"\n{Emojis.STOP} {Colors.BRIGHT_BLACK}Stopped following logs.{Colors.RESET}")
229
+ console.print(f"\n[dim]已停止日志跟踪[/]")
263
230
  finally:
264
- # Close all file handlers
231
+ # 关闭所有文件
265
232
  for f in file_handlers.values():
266
233
  f.close()
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
 
@@ -17,16 +19,40 @@ class ServiceManager:
17
19
 
18
20
  def __init__(self, config_path: str = "servly.yml", servly_dir: str = ".servly"):
19
21
  self.config_path = config_path
20
- self.servly_dir = Path(servly_dir)
22
+ # 修改为使用配置文件所在目录
23
+ config_dir = os.path.dirname(os.path.abspath(config_path))
24
+ self.servly_dir = Path(config_dir) / servly_dir
21
25
  self.pid_dir = self.servly_dir / "pids"
22
26
  self.log_dir = self.servly_dir / "logs"
27
+ # 存储服务启动时间,用于计算运行时长
28
+ self.start_times = {}
23
29
  self._ensure_dirs()
24
30
  self.services = self._load_config()
31
+ # 加载现有的服务启动时间
32
+ self._load_start_times()
25
33
 
26
34
  def _ensure_dirs(self):
27
35
  """Create required directories if they don't exist."""
28
36
  self.pid_dir.mkdir(parents=True, exist_ok=True)
29
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()
30
56
 
31
57
  def _load_config(self) -> Dict[str, Any]:
32
58
  """Load service configurations from servly.yml."""
@@ -81,10 +107,45 @@ class ServiceManager:
81
107
  else:
82
108
  # Clean up stale PID file
83
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)
84
116
  return None
85
117
  except (ValueError, FileNotFoundError):
86
118
  return None
87
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
+
88
149
  def _is_process_running(self, pid: int) -> bool:
89
150
  """Check if process with given PID is running."""
90
151
  try:
@@ -158,6 +219,15 @@ class ServiceManager:
158
219
  with open(self.get_pid_file(service_name), 'w') as f:
159
220
  f.write(str(process.pid))
160
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
+
161
231
  logger.info(f"Started service '{service_name}' with PID {process.pid}")
162
232
  return True
163
233
 
@@ -192,6 +262,15 @@ class ServiceManager:
192
262
  if os.path.exists(pid_file):
193
263
  os.remove(pid_file)
194
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
+
195
274
  logger.info(f"Stopped service '{service_name}'")
196
275
  return True
197
276
 
@@ -200,6 +279,16 @@ class ServiceManager:
200
279
  pid_file = self.get_pid_file(service_name)
201
280
  if os.path.exists(pid_file):
202
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
+
203
292
  logger.info(f"Service '{service_name}' was not running")
204
293
  return True
205
294
 
@@ -210,4 +299,51 @@ class ServiceManager:
210
299
  def restart(self, service_name: str) -> bool:
211
300
  """Restart a service."""
212
301
  self.stop(service_name)
213
- 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.2.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,7 +10,9 @@ 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
15
+ Requires-Dist: rich>=14.0.0
14
16
 
15
17
  # SERVLY
16
18
 
@@ -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=HeOOsnaJGMaiSWQ5Z-hkCLjxpmoUfES37hgfTHBuDMk,12142
3
- servly/logs.py,sha256=HfC_EByTbgz6MpW8IpemEwpRPPE0xME46zUy2CyJvqU,9779
4
- servly/service.py,sha256=5xhSD0HkfQw8xnY5J0GRxyFE8_CHBq5yfatbhL625PM,8123
5
- servly-0.2.0.dist-info/METADATA,sha256=YICRthT2IW92tOXYTUmZN1cCGZ3LtWBhtLPIIZADKeA,2630
6
- servly-0.2.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
7
- servly-0.2.0.dist-info/entry_points.txt,sha256=enHpE2d9DYdzfIWBcnIjuIkcNVk118xghL0AeTUL0Yg,43
8
- servly-0.2.0.dist-info/top_level.txt,sha256=JLi7GX0KwWF-mh8i1-lVJ2q4SO1KfI2CNxQk_VNfNZE,7
9
- servly-0.2.0.dist-info/RECORD,,
File without changes