servly 0.2.0__py3-none-any.whl → 0.3.0__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,36 @@ 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
 
224
- if is_running:
225
- status_text = f"{Emojis.RUNNING} {Colors.GREEN}RUNNING{Colors.RESET}"
226
- pid_text = f"{Colors.GREEN}{pid}{Colors.RESET}"
227
- 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}"
234
-
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}")
207
+ services.append({
208
+ "name": name,
209
+ "status": "running" if is_running else "stopped",
210
+ "pid": pid
211
+ })
212
+
213
+ # 使用Rich表格显示服务列表
214
+ print_service_table(services)
241
215
 
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()
216
+ console.print(f"[dim]配置文件: {manager.config_path}[/]")
217
+ console.print(f"[dim]运行中服务: {len(manager.get_running_services())}/{len(service_names)}[/]")
218
+ console.print()
245
219
 
246
220
  return True
247
221
 
248
222
  def main():
249
223
  """CLI 应用程序入口点"""
250
224
  # 显示头部
251
- print_header()
225
+ print_header("SERVLY - Modern Process Manager")
252
226
 
253
227
  parser = setup_arg_parser()
254
228
  args = parser.parse_args()
@@ -261,7 +235,7 @@ def main():
261
235
  try:
262
236
  service_manager = ServiceManager(config_path=args.config)
263
237
  except Exception as e:
264
- print(f"{Emojis.ERROR} {Colors.RED}加载配置文件时出错: {e}{Colors.RESET}")
238
+ print_error(f"加载配置文件时出错: {escape(str(e))}")
265
239
  return 1
266
240
 
267
241
  # 创建日志管理器
@@ -283,10 +257,11 @@ def main():
283
257
  parser.print_help()
284
258
  return 1
285
259
  except KeyboardInterrupt:
286
- print(f"\n{Emojis.STOP} {Colors.BRIGHT_BLACK}操作被用户中断{Colors.RESET}")
260
+ console.print(f"\n{Emojis.STOP} 操作被用户中断", style="dim")
287
261
  return 1
288
262
  except Exception as e:
289
- print(f"\n{Emojis.ERROR} {Colors.RED}执行命令时出错: {e}{Colors.RESET}")
263
+ print_error(f"执行命令时出错: {escape(str(e))}")
264
+ logger.exception("命令执行异常")
290
265
  return 1
291
266
 
292
267
  return 0 if success else 1
servly/logs.py CHANGED
@@ -1,56 +1,43 @@
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
16
18
 
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"
19
+ # 自定义Rich主题
20
+ custom_theme = Theme({
21
+ "warning": "yellow",
22
+ "error": "bold red",
23
+ "info": "green",
24
+ "dim": "dim",
25
+ "stdout_service": "green",
26
+ "stderr_service": "red",
27
+ "header": "cyan bold",
28
+ "subheader": "bright_black",
29
+ "running": "green",
30
+ "stopped": "bright_black",
31
+ "restart": "magenta",
32
+ "separator": "cyan",
33
+ })
50
34
 
35
+ # 创建Rich控制台对象
36
+ console = Console(theme=custom_theme)
51
37
 
52
38
  # 服务相关的 emoji
53
39
  class Emojis:
40
+ """服务状态相关的emoji图标"""
54
41
  SERVICE = "🔧"
55
42
  START = "🟢"
56
43
  STOP = "🔴"
@@ -66,132 +53,102 @@ class Emojis:
66
53
  STOPPED = "⛔"
67
54
  LOADING = "⏳"
68
55
 
56
+ # Rich格式化输出工具函数
57
+ def print_header(title: str):
58
+ """打印美化的标题"""
59
+ console.print()
60
+ console.rule(f"[header]{Emojis.SERVICE} {title}[/]", style="separator")
61
+ console.print()
69
62
 
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)
63
+ def print_info(message: str):
64
+ """打印信息消息"""
65
+ console.print(f"{Emojis.INFO} {message}", style="info")
75
66
 
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
67
+ def print_warning(message: str):
68
+ """打印警告消息"""
69
+ console.print(f"{Emojis.WARNING} {message}", style="warning")
87
70
 
88
- def align_colored_text(text: str, width: int, align='left') -> str:
89
- """
90
- 将彩色文本对齐到指定宽度
71
+ def print_error(message: str):
72
+ """打印错误消息"""
73
+ console.print(f"{Emojis.ERROR} {message}", style="error")
74
+
75
+ def print_success(message: str):
76
+ """打印成功消息"""
77
+ console.print(f"{Emojis.RUNNING} {message}", style="running")
78
+
79
+ 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")
91
85
 
92
- Args:
93
- text: 可能包含 ANSI 颜色代码的文本
94
- width: 期望的显示宽度
95
- align: 对齐方式,'left', 'right' 'center'
86
+ for service in services:
87
+ name = service["name"]
88
+ status = service["status"]
89
+ pid = service["pid"] or "-"
96
90
 
97
- Returns:
98
- 对齐后的文本,保留颜色代码
99
- """
100
- visible_length = get_visible_length(text)
101
- padding = max(0, width - visible_length)
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()}"
94
+
95
+ table.add_row(
96
+ name,
97
+ Text(status_text, style=status_style),
98
+ Text(str(pid), style=status_style)
99
+ )
102
100
 
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
101
+ console.print(table)
102
+ console.print()
111
103
 
112
104
 
113
105
  class LogManager:
114
- """Handles viewing and managing logs for servly services."""
106
+ """管理和显示服务日志"""
115
107
 
116
108
  def __init__(self, log_dir: Path):
117
109
  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
- ]
110
+ self.default_tail_lines = 15 # 默认展示最后15行日志
125
111
 
126
112
  def get_log_files(self, service_name: str) -> Dict[str, Path]:
127
- """Get the stdout and stderr log files for a service."""
113
+ """获取服务的stdoutstderr日志文件路径"""
128
114
  return {
129
115
  'stdout': self.log_dir / f"{service_name}-out.log",
130
116
  'stderr': self.log_dir / f"{service_name}-error.log"
131
117
  }
132
118
 
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
119
+ def _parse_log_line(self, line: str) -> Tuple[str, str]:
120
+ """解析日志行,提取时间戳和内容"""
121
+ timestamp = ""
122
+ content = line.rstrip()
151
123
 
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 ""
161
-
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
124
+ # 尝试提取时间戳
125
+ timestamp_match = re.search(r'(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})', line)
126
+ if timestamp_match:
127
+ timestamp = timestamp_match.group(1)
128
+ # 移除行中已有的时间戳部分
129
+ content = line.replace(timestamp, "", 1).lstrip().rstrip()
170
130
  else:
171
- prefix = f"{Emojis.INFO} "
131
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
172
132
 
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}"
133
+ return timestamp, content
180
134
 
181
- def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = 10):
135
+ def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = None):
182
136
  """
183
- Display logs for specified services in real-time.
137
+ 显示服务日志
184
138
 
185
139
  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
140
+ service_names: 要查看的服务名称列表
141
+ follow: 是否实时跟踪日志(类似tail -f
142
+ lines: 初始显示的行数,默认为self.default_tail_lines
189
143
  """
144
+ if lines is None:
145
+ lines = self.default_tail_lines
146
+
190
147
  if not service_names:
191
- print(f"{Emojis.WARNING} {Colors.YELLOW}No services specified for log viewing.{Colors.RESET}")
148
+ print_warning("未指定要查看日志的服务。")
192
149
  return
193
150
 
194
- # Check if the logs exist
151
+ # 检查日志文件是否存在
195
152
  log_files = []
196
153
  for service in service_names:
197
154
  service_logs = self.get_log_files(service)
@@ -199,52 +156,53 @@ class LogManager:
199
156
  if log_path.exists():
200
157
  log_files.append((service, log_type, log_path))
201
158
  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}.")
159
+ style = "stderr_service" if log_type == "stderr" else "stdout_service"
160
+ console.print(f"{Emojis.WARNING} 未找到服务 [{style}]{service}[/] {log_type} 日志。", style="warning")
204
161
 
205
162
  if not log_files:
206
- print(f"{Emojis.WARNING} {Colors.YELLOW}No log files found for specified services.{Colors.RESET}")
163
+ print_warning("未找到指定服务的日志文件。")
207
164
  return
208
165
 
209
166
  if follow:
167
+ # 首先显示最后几行,然后再开始跟踪
168
+ self._display_recent_logs(log_files, lines)
210
169
  self._follow_logs(log_files)
211
170
  else:
212
171
  self._display_recent_logs(log_files, lines)
213
172
 
214
173
  def _display_recent_logs(self, log_files: List[Tuple[str, str, Path]], lines: int):
215
- """Display the most recent lines from log files."""
174
+ """显示最近的日志行"""
216
175
  for service, log_type, log_path in log_files:
217
- print(self._format_log_header(service, log_type))
176
+ # PM2风格的标题
177
+ console.print(f"\n[dim]{log_path} last {lines} lines:[/]")
178
+
218
179
  try:
219
180
  # 读取最后N行
220
181
  with open(log_path, 'r') as f:
221
182
  content = f.readlines()
222
183
  last_lines = content[-lines:] if len(content) >= lines else content
223
184
 
224
- # 打印每一行,增加格式
185
+ # 打印每一行,PM2格式
225
186
  for line in last_lines:
226
- print(self._format_log_line(service, log_type, line, show_timestamp=False))
187
+ timestamp, message = self._parse_log_line(line)
188
+ style = "stderr_service" if log_type == "stderr" else "stdout_service"
189
+ console.print(f"[{style}]{service}[/] | {timestamp}: {message}")
227
190
  except Exception as e:
228
- print(f"{Emojis.ERROR} {Colors.RED}Error reading logs: {str(e)}{Colors.RESET}")
191
+ print_error(f"读取日志文件出错: {str(e)}")
229
192
 
230
193
  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
194
+ """实时跟踪日志(类似tail -f"""
233
195
  file_handlers = {}
234
196
 
235
197
  try:
236
- # Open all log files
198
+ # 打开所有日志文件
237
199
  for service, log_type, log_path in log_files:
238
200
  f = open(log_path, 'r')
239
- # Move to the end of the file
201
+ # 移动到文件末尾
240
202
  f.seek(0, os.SEEK_END)
241
203
  file_handlers[(service, log_type)] = f
242
204
 
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}")
205
+ console.print(f"\n[dim]正在跟踪日志... (按Ctrl+C停止)[/]")
248
206
 
249
207
  while True:
250
208
  has_new_data = False
@@ -253,14 +211,16 @@ class LogManager:
253
211
  line = f.readline()
254
212
  if line:
255
213
  has_new_data = True
256
- print(self._format_log_line(service, log_type, line))
214
+ timestamp, message = self._parse_log_line(line)
215
+ style = "stderr_service" if log_type == "stderr" else "stdout_service"
216
+ console.print(f"[{style}]{service}[/] | {timestamp}: {message}")
257
217
 
258
218
  if not has_new_data:
259
219
  time.sleep(0.1)
260
220
 
261
221
  except KeyboardInterrupt:
262
- print(f"\n{Emojis.STOP} {Colors.BRIGHT_BLACK}Stopped following logs.{Colors.RESET}")
222
+ console.print(f"\n[dim]已停止日志跟踪[/]")
263
223
  finally:
264
- # Close all file handlers
224
+ # 关闭所有文件
265
225
  for f in file_handlers.values():
266
226
  f.close()
servly/service.py CHANGED
@@ -17,7 +17,9 @@ class ServiceManager:
17
17
 
18
18
  def __init__(self, config_path: str = "servly.yml", servly_dir: str = ".servly"):
19
19
  self.config_path = config_path
20
- self.servly_dir = Path(servly_dir)
20
+ # 修改为使用配置文件所在目录
21
+ config_dir = os.path.dirname(os.path.abspath(config_path))
22
+ self.servly_dir = Path(config_dir) / servly_dir
21
23
  self.pid_dir = self.servly_dir / "pids"
22
24
  self.log_dir = self.servly_dir / "logs"
23
25
  self._ensure_dirs()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: servly
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: simple process manager
5
5
  Author-email: simpxx <simpxx@gmail.com>
6
6
  License: MIT
@@ -11,6 +11,7 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.12
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: pyyaml>=6.0.2
14
+ Requires-Dist: rich>=14.0.0
14
15
 
15
16
  # SERVLY
16
17
 
@@ -0,0 +1,9 @@
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,,
@@ -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