servly 0.2.0__tar.gz → 0.3.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.
- {servly-0.2.0 → servly-0.3.0}/PKG-INFO +2 -1
- {servly-0.2.0 → servly-0.3.0}/pyproject.toml +2 -1
- servly-0.3.0/servly/cli.py +270 -0
- servly-0.3.0/servly/logs.py +226 -0
- {servly-0.2.0 → servly-0.3.0}/servly/service.py +3 -1
- {servly-0.2.0 → servly-0.3.0}/servly.egg-info/PKG-INFO +2 -1
- {servly-0.2.0 → servly-0.3.0}/servly.egg-info/requires.txt +1 -0
- {servly-0.2.0 → servly-0.3.0}/tests/test_advanced.py +82 -0
- servly-0.2.0/servly/cli.py +0 -295
- servly-0.2.0/servly/logs.py +0 -266
- {servly-0.2.0 → servly-0.3.0}/README.md +0 -0
- {servly-0.2.0 → servly-0.3.0}/servly/__init__.py +0 -0
- {servly-0.2.0 → servly-0.3.0}/servly.egg-info/SOURCES.txt +0 -0
- {servly-0.2.0 → servly-0.3.0}/servly.egg-info/dependency_links.txt +0 -0
- {servly-0.2.0 → servly-0.3.0}/servly.egg-info/entry_points.txt +0 -0
- {servly-0.2.0 → servly-0.3.0}/servly.egg-info/top_level.txt +0 -0
- {servly-0.2.0 → servly-0.3.0}/setup.cfg +0 -0
- {servly-0.2.0 → servly-0.3.0}/tests/test_config.py +0 -0
- {servly-0.2.0 → servly-0.3.0}/tests/test_integration.py +0 -0
- {servly-0.2.0 → servly-0.3.0}/tests/test_logs.py +0 -0
- {servly-0.2.0 → servly-0.3.0}/tests/test_service_management.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: servly
|
3
|
-
Version: 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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "servly"
|
7
|
-
version = "0.
|
7
|
+
version = "0.3.0"
|
8
8
|
description = "simple process manager"
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.12"
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
20
20
|
]
|
21
21
|
dependencies = [
|
22
22
|
"pyyaml>=6.0.2",
|
23
|
+
"rich>=14.0.0",
|
23
24
|
]
|
24
25
|
|
25
26
|
[project.scripts]
|
@@ -0,0 +1,270 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Command-line interface for Servly process manager.
|
4
|
+
使用Rich库进行终端输出格式化
|
5
|
+
"""
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
import argparse
|
9
|
+
import logging
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import List, Dict, Optional
|
12
|
+
|
13
|
+
from servly.service import ServiceManager
|
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
|
+
|
17
|
+
from rich.logging import RichHandler
|
18
|
+
from rich.traceback import install
|
19
|
+
from rich.markup import escape
|
20
|
+
|
21
|
+
# 安装Rich的异常格式化器
|
22
|
+
install()
|
23
|
+
|
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")
|
33
|
+
|
34
|
+
def setup_arg_parser() -> argparse.ArgumentParser:
|
35
|
+
"""设置命令行参数解析器"""
|
36
|
+
parser = argparse.ArgumentParser(
|
37
|
+
description=f"{Emojis.SERVICE} Servly - Modern process manager",
|
38
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
39
|
+
)
|
40
|
+
|
41
|
+
# 全局参数
|
42
|
+
parser.add_argument('-f', '--config',
|
43
|
+
help='配置文件路径 (默认: servly.yml)',
|
44
|
+
default='servly.yml')
|
45
|
+
|
46
|
+
subparsers = parser.add_subparsers(dest='command', help='要执行的命令')
|
47
|
+
|
48
|
+
# Start 命令
|
49
|
+
start_parser = subparsers.add_parser('start', help=f'{Emojis.START} 启动服务')
|
50
|
+
start_parser.add_argument('service', nargs='?', default='all',
|
51
|
+
help='服务名称或 "all" 启动所有服务')
|
52
|
+
|
53
|
+
# Stop 命令
|
54
|
+
stop_parser = subparsers.add_parser('stop', help=f'{Emojis.STOP} 停止服务')
|
55
|
+
stop_parser.add_argument('service', nargs='?', default='all',
|
56
|
+
help='服务名称或 "all" 停止所有服务')
|
57
|
+
|
58
|
+
# Restart 命令
|
59
|
+
restart_parser = subparsers.add_parser('restart', help=f'{Emojis.RESTART} 重启服务')
|
60
|
+
restart_parser.add_argument('service', nargs='?', default='all',
|
61
|
+
help='服务名称或 "all" 重启所有服务')
|
62
|
+
|
63
|
+
# Log 命令
|
64
|
+
log_parser = subparsers.add_parser('log', help=f'{Emojis.LOG} 查看服务日志')
|
65
|
+
log_parser.add_argument('service', nargs='?', default='all',
|
66
|
+
help='服务名称或 "all" 查看所有日志')
|
67
|
+
log_parser.add_argument('--no-follow', '-n', action='store_true',
|
68
|
+
help='不实时跟踪日志')
|
69
|
+
log_parser.add_argument('--lines', '-l', type=int, default=10,
|
70
|
+
help='初始显示的行数')
|
71
|
+
|
72
|
+
# List 命令 - 保留原命令名,不改为 ps
|
73
|
+
list_parser = subparsers.add_parser('list', help='列出服务')
|
74
|
+
|
75
|
+
return parser
|
76
|
+
|
77
|
+
def handle_start(manager: ServiceManager, service_name: str) -> bool:
|
78
|
+
"""处理启动命令"""
|
79
|
+
if service_name == 'all':
|
80
|
+
service_names = manager.get_service_names()
|
81
|
+
if not service_names:
|
82
|
+
print_warning("配置中没有定义任何服务。")
|
83
|
+
return False
|
84
|
+
|
85
|
+
console.print(f"{Emojis.START} 正在启动所有服务...", style="running")
|
86
|
+
success = True
|
87
|
+
for name in service_names:
|
88
|
+
if not manager.start(name):
|
89
|
+
print_error(f"启动服务 '{name}' 失败")
|
90
|
+
success = False
|
91
|
+
else:
|
92
|
+
print_success(f"服务 '{name}' 已成功启动")
|
93
|
+
|
94
|
+
if success:
|
95
|
+
print_success("所有服务已成功启动!")
|
96
|
+
else:
|
97
|
+
print_warning("有些服务启动失败,请检查日志获取详情。")
|
98
|
+
|
99
|
+
return success
|
100
|
+
else:
|
101
|
+
result = manager.start(service_name)
|
102
|
+
if result:
|
103
|
+
print_success(f"服务 '{service_name}' 已成功启动")
|
104
|
+
else:
|
105
|
+
print_error(f"启动服务 '{service_name}' 失败")
|
106
|
+
return result
|
107
|
+
|
108
|
+
def handle_stop(manager: ServiceManager, service_name: str) -> bool:
|
109
|
+
"""处理停止命令"""
|
110
|
+
if service_name == 'all':
|
111
|
+
service_names = manager.get_running_services()
|
112
|
+
if not service_names:
|
113
|
+
console.print(f"{Emojis.INFO} 当前没有正在运行的服务。", style="dim")
|
114
|
+
return True
|
115
|
+
|
116
|
+
console.print(f"{Emojis.STOP} 正在停止所有服务...", style="warning")
|
117
|
+
success = True
|
118
|
+
for name in service_names:
|
119
|
+
if not manager.stop(name):
|
120
|
+
print_error(f"停止服务 '{name}' 失败")
|
121
|
+
success = False
|
122
|
+
else:
|
123
|
+
console.print(f"{Emojis.STOPPED} 服务 '{name}' 已停止", style="stopped")
|
124
|
+
|
125
|
+
if success:
|
126
|
+
console.print(f"\n{Emojis.STOPPED} 所有服务已成功停止!", style="warning")
|
127
|
+
else:
|
128
|
+
print_warning("有些服务停止失败,请检查日志获取详情。")
|
129
|
+
|
130
|
+
return success
|
131
|
+
else:
|
132
|
+
result = manager.stop(service_name)
|
133
|
+
if result:
|
134
|
+
console.print(f"{Emojis.STOPPED} 服务 '{service_name}' 已成功停止", style="warning")
|
135
|
+
else:
|
136
|
+
print_error(f"停止服务 '{service_name}' 失败")
|
137
|
+
return result
|
138
|
+
|
139
|
+
def handle_restart(manager: ServiceManager, service_name: str) -> bool:
|
140
|
+
"""处理重启命令"""
|
141
|
+
if service_name == 'all':
|
142
|
+
service_names = manager.get_service_names()
|
143
|
+
if not service_names:
|
144
|
+
print_warning("配置中没有定义任何服务。")
|
145
|
+
return False
|
146
|
+
|
147
|
+
console.print(f"{Emojis.RESTART} 正在重启所有服务...", style="restart")
|
148
|
+
success = True
|
149
|
+
for name in service_names:
|
150
|
+
if not manager.restart(name):
|
151
|
+
print_error(f"重启服务 '{name}' 失败")
|
152
|
+
success = False
|
153
|
+
else:
|
154
|
+
console.print(f"{Emojis.RUNNING} 服务 '{name}' 已成功重启", style="restart")
|
155
|
+
|
156
|
+
if success:
|
157
|
+
print_success("所有服务已成功重启!")
|
158
|
+
else:
|
159
|
+
print_warning("有些服务重启失败,请检查日志获取详情。")
|
160
|
+
|
161
|
+
return success
|
162
|
+
else:
|
163
|
+
result = manager.restart(service_name)
|
164
|
+
if result:
|
165
|
+
console.print(f"{Emojis.RUNNING} 服务 '{service_name}' 已成功重启", style="restart")
|
166
|
+
else:
|
167
|
+
print_error(f"重启服务 '{service_name}' 失败")
|
168
|
+
return result
|
169
|
+
|
170
|
+
def handle_log(manager: ServiceManager, log_manager: LogManager, args) -> bool:
|
171
|
+
"""处理日志查看命令"""
|
172
|
+
service_name = args.service
|
173
|
+
follow = not args.no_follow
|
174
|
+
lines = args.lines
|
175
|
+
|
176
|
+
if service_name == 'all':
|
177
|
+
services = manager.get_service_names()
|
178
|
+
if not services:
|
179
|
+
print_warning("配置中没有定义任何服务。")
|
180
|
+
return False
|
181
|
+
else:
|
182
|
+
if service_name not in manager.get_service_names():
|
183
|
+
print_error(f"服务 '{service_name}' 在配置中未找到。")
|
184
|
+
return False
|
185
|
+
services = [service_name]
|
186
|
+
|
187
|
+
# 使用LogManager查看日志
|
188
|
+
log_manager.tail_logs(services, follow=follow, lines=lines)
|
189
|
+
return True
|
190
|
+
|
191
|
+
def handle_list(manager: ServiceManager) -> bool:
|
192
|
+
"""处理列出服务命令"""
|
193
|
+
service_names = manager.get_service_names()
|
194
|
+
|
195
|
+
if not service_names:
|
196
|
+
print_warning("配置中没有定义任何服务。")
|
197
|
+
return True
|
198
|
+
|
199
|
+
print_header("服务列表")
|
200
|
+
|
201
|
+
# 构建服务列表数据
|
202
|
+
services = []
|
203
|
+
for name in service_names:
|
204
|
+
is_running = manager.is_running(name)
|
205
|
+
pid = manager.get_service_pid(name)
|
206
|
+
|
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)
|
215
|
+
|
216
|
+
console.print(f"[dim]配置文件: {manager.config_path}[/]")
|
217
|
+
console.print(f"[dim]运行中服务: {len(manager.get_running_services())}/{len(service_names)}[/]")
|
218
|
+
console.print()
|
219
|
+
|
220
|
+
return True
|
221
|
+
|
222
|
+
def main():
|
223
|
+
"""CLI 应用程序入口点"""
|
224
|
+
# 显示头部
|
225
|
+
print_header("SERVLY - Modern Process Manager")
|
226
|
+
|
227
|
+
parser = setup_arg_parser()
|
228
|
+
args = parser.parse_args()
|
229
|
+
|
230
|
+
if not args.command:
|
231
|
+
parser.print_help()
|
232
|
+
return 1
|
233
|
+
|
234
|
+
# 创建服务管理器
|
235
|
+
try:
|
236
|
+
service_manager = ServiceManager(config_path=args.config)
|
237
|
+
except Exception as e:
|
238
|
+
print_error(f"加载配置文件时出错: {escape(str(e))}")
|
239
|
+
return 1
|
240
|
+
|
241
|
+
# 创建日志管理器
|
242
|
+
log_manager = LogManager(service_manager.log_dir)
|
243
|
+
|
244
|
+
# 处理命令
|
245
|
+
try:
|
246
|
+
if args.command == 'start':
|
247
|
+
success = handle_start(service_manager, args.service)
|
248
|
+
elif args.command == 'stop':
|
249
|
+
success = handle_stop(service_manager, args.service)
|
250
|
+
elif args.command == 'restart':
|
251
|
+
success = handle_restart(service_manager, args.service)
|
252
|
+
elif args.command == 'log':
|
253
|
+
success = handle_log(service_manager, log_manager, args)
|
254
|
+
elif args.command == 'list':
|
255
|
+
success = handle_list(service_manager)
|
256
|
+
else:
|
257
|
+
parser.print_help()
|
258
|
+
return 1
|
259
|
+
except KeyboardInterrupt:
|
260
|
+
console.print(f"\n{Emojis.STOP} 操作被用户中断", style="dim")
|
261
|
+
return 1
|
262
|
+
except Exception as e:
|
263
|
+
print_error(f"执行命令时出错: {escape(str(e))}")
|
264
|
+
logger.exception("命令执行异常")
|
265
|
+
return 1
|
266
|
+
|
267
|
+
return 0 if success else 1
|
268
|
+
|
269
|
+
if __name__ == "__main__":
|
270
|
+
sys.exit(main())
|
@@ -0,0 +1,226 @@
|
|
1
|
+
"""
|
2
|
+
Log management functionality for Servly.
|
3
|
+
使用Rich库进行日志格式化和展示,实现PM2风格的日志效果。
|
4
|
+
"""
|
5
|
+
import os
|
6
|
+
import sys
|
7
|
+
import time
|
8
|
+
import re
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import List, Dict, Optional, Tuple
|
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
|
+
|
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
|
+
})
|
34
|
+
|
35
|
+
# 创建Rich控制台对象
|
36
|
+
console = Console(theme=custom_theme)
|
37
|
+
|
38
|
+
# 服务相关的 emoji
|
39
|
+
class Emojis:
|
40
|
+
"""服务状态相关的emoji图标"""
|
41
|
+
SERVICE = "🔧"
|
42
|
+
START = "🟢"
|
43
|
+
STOP = "🔴"
|
44
|
+
RESTART = "🔄"
|
45
|
+
INFO = "ℹ️ "
|
46
|
+
WARNING = "⚠️ "
|
47
|
+
ERROR = "❌"
|
48
|
+
LOG = "📝"
|
49
|
+
STDOUT = "📤"
|
50
|
+
STDERR = "📥"
|
51
|
+
TIME = "🕒"
|
52
|
+
RUNNING = "✅"
|
53
|
+
STOPPED = "⛔"
|
54
|
+
LOADING = "⏳"
|
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()
|
62
|
+
|
63
|
+
def print_info(message: str):
|
64
|
+
"""打印信息消息"""
|
65
|
+
console.print(f"{Emojis.INFO} {message}", style="info")
|
66
|
+
|
67
|
+
def print_warning(message: str):
|
68
|
+
"""打印警告消息"""
|
69
|
+
console.print(f"{Emojis.WARNING} {message}", style="warning")
|
70
|
+
|
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")
|
85
|
+
|
86
|
+
for service in services:
|
87
|
+
name = service["name"]
|
88
|
+
status = service["status"]
|
89
|
+
pid = service["pid"] or "-"
|
90
|
+
|
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
|
+
)
|
100
|
+
|
101
|
+
console.print(table)
|
102
|
+
console.print()
|
103
|
+
|
104
|
+
|
105
|
+
class LogManager:
|
106
|
+
"""管理和显示服务日志"""
|
107
|
+
|
108
|
+
def __init__(self, log_dir: Path):
|
109
|
+
self.log_dir = log_dir
|
110
|
+
self.default_tail_lines = 15 # 默认展示最后15行日志
|
111
|
+
|
112
|
+
def get_log_files(self, service_name: str) -> Dict[str, Path]:
|
113
|
+
"""获取服务的stdout和stderr日志文件路径"""
|
114
|
+
return {
|
115
|
+
'stdout': self.log_dir / f"{service_name}-out.log",
|
116
|
+
'stderr': self.log_dir / f"{service_name}-error.log"
|
117
|
+
}
|
118
|
+
|
119
|
+
def _parse_log_line(self, line: str) -> Tuple[str, str]:
|
120
|
+
"""解析日志行,提取时间戳和内容"""
|
121
|
+
timestamp = ""
|
122
|
+
content = line.rstrip()
|
123
|
+
|
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()
|
130
|
+
else:
|
131
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
132
|
+
|
133
|
+
return timestamp, content
|
134
|
+
|
135
|
+
def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = None):
|
136
|
+
"""
|
137
|
+
显示服务日志
|
138
|
+
|
139
|
+
Args:
|
140
|
+
service_names: 要查看的服务名称列表
|
141
|
+
follow: 是否实时跟踪日志(类似tail -f)
|
142
|
+
lines: 初始显示的行数,默认为self.default_tail_lines
|
143
|
+
"""
|
144
|
+
if lines is None:
|
145
|
+
lines = self.default_tail_lines
|
146
|
+
|
147
|
+
if not service_names:
|
148
|
+
print_warning("未指定要查看日志的服务。")
|
149
|
+
return
|
150
|
+
|
151
|
+
# 检查日志文件是否存在
|
152
|
+
log_files = []
|
153
|
+
for service in service_names:
|
154
|
+
service_logs = self.get_log_files(service)
|
155
|
+
for log_type, log_path in service_logs.items():
|
156
|
+
if log_path.exists():
|
157
|
+
log_files.append((service, log_type, log_path))
|
158
|
+
else:
|
159
|
+
style = "stderr_service" if log_type == "stderr" else "stdout_service"
|
160
|
+
console.print(f"{Emojis.WARNING} 未找到服务 [{style}]{service}[/] 的 {log_type} 日志。", style="warning")
|
161
|
+
|
162
|
+
if not log_files:
|
163
|
+
print_warning("未找到指定服务的日志文件。")
|
164
|
+
return
|
165
|
+
|
166
|
+
if follow:
|
167
|
+
# 首先显示最后几行,然后再开始跟踪
|
168
|
+
self._display_recent_logs(log_files, lines)
|
169
|
+
self._follow_logs(log_files)
|
170
|
+
else:
|
171
|
+
self._display_recent_logs(log_files, lines)
|
172
|
+
|
173
|
+
def _display_recent_logs(self, log_files: List[Tuple[str, str, Path]], lines: int):
|
174
|
+
"""显示最近的日志行"""
|
175
|
+
for service, log_type, log_path in log_files:
|
176
|
+
# PM2风格的标题
|
177
|
+
console.print(f"\n[dim]{log_path} last {lines} lines:[/]")
|
178
|
+
|
179
|
+
try:
|
180
|
+
# 读取最后N行
|
181
|
+
with open(log_path, 'r') as f:
|
182
|
+
content = f.readlines()
|
183
|
+
last_lines = content[-lines:] if len(content) >= lines else content
|
184
|
+
|
185
|
+
# 打印每一行,PM2格式
|
186
|
+
for line in last_lines:
|
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}")
|
190
|
+
except Exception as e:
|
191
|
+
print_error(f"读取日志文件出错: {str(e)}")
|
192
|
+
|
193
|
+
def _follow_logs(self, log_files: List[Tuple[str, str, Path]]):
|
194
|
+
"""实时跟踪日志(类似tail -f)"""
|
195
|
+
file_handlers = {}
|
196
|
+
|
197
|
+
try:
|
198
|
+
# 打开所有日志文件
|
199
|
+
for service, log_type, log_path in log_files:
|
200
|
+
f = open(log_path, 'r')
|
201
|
+
# 移动到文件末尾
|
202
|
+
f.seek(0, os.SEEK_END)
|
203
|
+
file_handlers[(service, log_type)] = f
|
204
|
+
|
205
|
+
console.print(f"\n[dim]正在跟踪日志... (按Ctrl+C停止)[/]")
|
206
|
+
|
207
|
+
while True:
|
208
|
+
has_new_data = False
|
209
|
+
|
210
|
+
for (service, log_type), f in file_handlers.items():
|
211
|
+
line = f.readline()
|
212
|
+
if line:
|
213
|
+
has_new_data = True
|
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}")
|
217
|
+
|
218
|
+
if not has_new_data:
|
219
|
+
time.sleep(0.1)
|
220
|
+
|
221
|
+
except KeyboardInterrupt:
|
222
|
+
console.print(f"\n[dim]已停止日志跟踪[/]")
|
223
|
+
finally:
|
224
|
+
# 关闭所有文件
|
225
|
+
for f in file_handlers.values():
|
226
|
+
f.close()
|
@@ -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
|
-
|
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.
|
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
|
|
@@ -81,6 +81,88 @@ class TestAdvancedFeatures:
|
|
81
81
|
_, kwargs = mock_popen.call_args
|
82
82
|
assert 'cwd' in kwargs
|
83
83
|
assert kwargs['cwd'] == str(test_dir)
|
84
|
+
|
85
|
+
@patch('subprocess.Popen')
|
86
|
+
def test_multiline_command(self, mock_popen, temp_dir):
|
87
|
+
"""TC4.3: 测试长命令行使用换行符"""
|
88
|
+
# 创建包含多行命令的配置
|
89
|
+
config = {
|
90
|
+
'multiline-cmd': {
|
91
|
+
'cmd': """python -c "
|
92
|
+
import sys
|
93
|
+
import time
|
94
|
+
print('Testing multiline command')
|
95
|
+
time.sleep(0.1)
|
96
|
+
sys.stdout.flush()
|
97
|
+
print('Command executed successfully')
|
98
|
+
"
|
99
|
+
"""
|
100
|
+
},
|
101
|
+
'multiline-cmd-with-env': {
|
102
|
+
'cmd': """
|
103
|
+
PYTHONPATH=/path/to/lib \
|
104
|
+
DEBUG=true \
|
105
|
+
LOG_LEVEL=debug \
|
106
|
+
python -m complex_script \
|
107
|
+
--arg1=value1 \
|
108
|
+
--arg2=value2 \
|
109
|
+
--enabled \
|
110
|
+
--config=/etc/config.json
|
111
|
+
"""
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
config_path = Path(temp_dir) / 'multiline_cmd_test.yml'
|
116
|
+
with open(config_path, 'w') as f:
|
117
|
+
yaml.dump(config, f)
|
118
|
+
|
119
|
+
mock_process = MagicMock()
|
120
|
+
mock_process.pid = 12345
|
121
|
+
mock_popen.return_value = mock_process
|
122
|
+
|
123
|
+
# 创建服务管理器
|
124
|
+
manager = ServiceManager(config_path=config_path)
|
125
|
+
|
126
|
+
# 启动第一个多行脚本服务
|
127
|
+
result1 = manager.start('multiline-cmd')
|
128
|
+
assert result1 is True
|
129
|
+
|
130
|
+
# 验证命令是否被正确传递,保留换行符
|
131
|
+
call1_args, call1_kwargs = mock_popen.call_args_list[0]
|
132
|
+
assert call1_kwargs.get('shell') is True, "命令应该以 shell=True 模式执行"
|
133
|
+
cmd1 = call1_args[0]
|
134
|
+
|
135
|
+
# 验证多行命令中的关键部分
|
136
|
+
assert "import sys" in cmd1
|
137
|
+
assert "import time" in cmd1
|
138
|
+
assert "Testing multiline command" in cmd1
|
139
|
+
|
140
|
+
# 重置模拟并设置新的返回值
|
141
|
+
mock_popen.reset_mock()
|
142
|
+
mock_process2 = MagicMock()
|
143
|
+
mock_process2.pid = 12346
|
144
|
+
mock_popen.return_value = mock_process2
|
145
|
+
|
146
|
+
# 启动第二个带环境变量和行连接符的服务
|
147
|
+
result2 = manager.start('multiline-cmd-with-env')
|
148
|
+
assert result2 is True
|
149
|
+
|
150
|
+
# 验证带续行符的命令是否被正确处理
|
151
|
+
call2_args, call2_kwargs = mock_popen.call_args_list[0]
|
152
|
+
assert call2_kwargs.get('shell') is True, "命令应该以 shell=True 模式执行"
|
153
|
+
cmd2 = call2_args[0]
|
154
|
+
|
155
|
+
# 验证包含续行符和缩进的命令中的关键部分
|
156
|
+
assert "python -m complex_script" in cmd2
|
157
|
+
assert "--arg1=value1" in cmd2
|
158
|
+
assert "--arg2=value2" in cmd2
|
159
|
+
assert "--enabled" in cmd2
|
160
|
+
assert "--config=/etc/config.json" in cmd2
|
161
|
+
|
162
|
+
# 验证环境变量是否在命令字符串中
|
163
|
+
assert "PYTHONPATH=/path/to/lib" in cmd2
|
164
|
+
assert "DEBUG=true" in cmd2
|
165
|
+
assert "LOG_LEVEL=debug" in cmd2
|
84
166
|
|
85
167
|
class TestErrorHandling:
|
86
168
|
"""测试错误处理"""
|
servly-0.2.0/servly/cli.py
DELETED
@@ -1,295 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Command-line interface for Servly process manager.
|
4
|
-
"""
|
5
|
-
import os
|
6
|
-
import sys
|
7
|
-
import argparse
|
8
|
-
import logging
|
9
|
-
from pathlib import Path
|
10
|
-
from typing import List, Optional
|
11
|
-
|
12
|
-
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 # 导入对齐工具函数
|
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}"
|
32
|
-
|
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)
|
39
|
-
|
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")
|
45
|
-
|
46
|
-
def setup_arg_parser() -> argparse.ArgumentParser:
|
47
|
-
"""设置命令行参数解析器"""
|
48
|
-
parser = argparse.ArgumentParser(
|
49
|
-
description=f"{Colors.CYAN}{Colors.BOLD}{Emojis.SERVICE} Servly - Modern process manager{Colors.RESET}",
|
50
|
-
formatter_class=argparse.RawDescriptionHelpFormatter
|
51
|
-
)
|
52
|
-
|
53
|
-
# 全局参数
|
54
|
-
parser.add_argument('-f', '--config',
|
55
|
-
help='配置文件路径 (默认: servly.yml)',
|
56
|
-
default='servly.yml')
|
57
|
-
|
58
|
-
subparsers = parser.add_subparsers(dest='command', help='要执行的命令')
|
59
|
-
|
60
|
-
# Start 命令
|
61
|
-
start_parser = subparsers.add_parser('start', help=f'{Emojis.START} 启动服务')
|
62
|
-
start_parser.add_argument('service', nargs='?', default='all',
|
63
|
-
help='服务名称或 "all" 启动所有服务')
|
64
|
-
|
65
|
-
# Stop 命令
|
66
|
-
stop_parser = subparsers.add_parser('stop', help=f'{Emojis.STOP} 停止服务')
|
67
|
-
stop_parser.add_argument('service', nargs='?', default='all',
|
68
|
-
help='服务名称或 "all" 停止所有服务')
|
69
|
-
|
70
|
-
# Restart 命令
|
71
|
-
restart_parser = subparsers.add_parser('restart', help=f'{Emojis.RESTART} 重启服务')
|
72
|
-
restart_parser.add_argument('service', nargs='?', default='all',
|
73
|
-
help='服务名称或 "all" 重启所有服务')
|
74
|
-
|
75
|
-
# Log 命令
|
76
|
-
log_parser = subparsers.add_parser('log', help=f'{Emojis.LOG} 查看服务日志')
|
77
|
-
log_parser.add_argument('service', nargs='?', default='all',
|
78
|
-
help='服务名称或 "all" 查看所有日志')
|
79
|
-
log_parser.add_argument('--no-follow', '-n', action='store_true',
|
80
|
-
help='不实时跟踪日志')
|
81
|
-
log_parser.add_argument('--lines', '-l', type=int, default=10,
|
82
|
-
help='初始显示的行数')
|
83
|
-
|
84
|
-
# List 命令 - 保留原命令名,不改为 ps
|
85
|
-
list_parser = subparsers.add_parser('list', help='列出服务')
|
86
|
-
|
87
|
-
return parser
|
88
|
-
|
89
|
-
def handle_start(manager: ServiceManager, service_name: str) -> bool:
|
90
|
-
"""处理启动命令"""
|
91
|
-
if service_name == 'all':
|
92
|
-
service_names = manager.get_service_names()
|
93
|
-
if not service_names:
|
94
|
-
print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
|
95
|
-
return False
|
96
|
-
|
97
|
-
print(f"{Emojis.START} {Colors.BRIGHT_GREEN}正在启动所有服务...{Colors.RESET}")
|
98
|
-
success = True
|
99
|
-
for name in service_names:
|
100
|
-
if not manager.start(name):
|
101
|
-
print(f"{Emojis.ERROR} {Colors.RED}启动服务 '{name}' 失败{Colors.RESET}")
|
102
|
-
success = False
|
103
|
-
else:
|
104
|
-
print(f"{Emojis.RUNNING} {Colors.GREEN}服务 '{name}' 已成功启动{Colors.RESET}")
|
105
|
-
|
106
|
-
if success:
|
107
|
-
print(f"\n{Emojis.RUNNING} {Colors.BRIGHT_GREEN}所有服务已成功启动!{Colors.RESET}")
|
108
|
-
else:
|
109
|
-
print(f"\n{Emojis.WARNING} {Colors.YELLOW}有些服务启动失败,请检查日志获取详情。{Colors.RESET}")
|
110
|
-
|
111
|
-
return success
|
112
|
-
else:
|
113
|
-
result = manager.start(service_name)
|
114
|
-
if result:
|
115
|
-
print(f"{Emojis.RUNNING} {Colors.GREEN}服务 '{service_name}' 已成功启动{Colors.RESET}")
|
116
|
-
else:
|
117
|
-
print(f"{Emojis.ERROR} {Colors.RED}启动服务 '{service_name}' 失败{Colors.RESET}")
|
118
|
-
return result
|
119
|
-
|
120
|
-
def handle_stop(manager: ServiceManager, service_name: str) -> bool:
|
121
|
-
"""处理停止命令"""
|
122
|
-
if service_name == 'all':
|
123
|
-
service_names = manager.get_running_services()
|
124
|
-
if not service_names:
|
125
|
-
print(f"{Emojis.INFO} {Colors.BRIGHT_BLACK}当前没有正在运行的服务。{Colors.RESET}")
|
126
|
-
return True
|
127
|
-
|
128
|
-
print(f"{Emojis.STOP} {Colors.YELLOW}正在停止所有服务...{Colors.RESET}")
|
129
|
-
success = True
|
130
|
-
for name in service_names:
|
131
|
-
if not manager.stop(name):
|
132
|
-
print(f"{Emojis.ERROR} {Colors.RED}停止服务 '{name}' 失败{Colors.RESET}")
|
133
|
-
success = False
|
134
|
-
else:
|
135
|
-
print(f"{Emojis.STOPPED} {Colors.YELLOW}服务 '{name}' 已停止{Colors.RESET}")
|
136
|
-
|
137
|
-
if success:
|
138
|
-
print(f"\n{Emojis.STOPPED} {Colors.YELLOW}所有服务已成功停止!{Colors.RESET}")
|
139
|
-
else:
|
140
|
-
print(f"\n{Emojis.WARNING} {Colors.YELLOW}有些服务停止失败,请检查日志获取详情。{Colors.RESET}")
|
141
|
-
|
142
|
-
return success
|
143
|
-
else:
|
144
|
-
result = manager.stop(service_name)
|
145
|
-
if result:
|
146
|
-
print(f"{Emojis.STOPPED} {Colors.YELLOW}服务 '{service_name}' 已成功停止{Colors.RESET}")
|
147
|
-
else:
|
148
|
-
print(f"{Emojis.ERROR} {Colors.RED}停止服务 '{service_name}' 失败{Colors.RESET}")
|
149
|
-
return result
|
150
|
-
|
151
|
-
def handle_restart(manager: ServiceManager, service_name: str) -> bool:
|
152
|
-
"""处理重启命令"""
|
153
|
-
if service_name == 'all':
|
154
|
-
service_names = manager.get_service_names()
|
155
|
-
if not service_names:
|
156
|
-
print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
|
157
|
-
return False
|
158
|
-
|
159
|
-
print(f"{Emojis.RESTART} {Colors.MAGENTA}正在重启所有服务...{Colors.RESET}")
|
160
|
-
success = True
|
161
|
-
for name in service_names:
|
162
|
-
if not manager.restart(name):
|
163
|
-
print(f"{Emojis.ERROR} {Colors.RED}重启服务 '{name}' 失败{Colors.RESET}")
|
164
|
-
success = False
|
165
|
-
else:
|
166
|
-
print(f"{Emojis.RUNNING} {Colors.MAGENTA}服务 '{name}' 已成功重启{Colors.RESET}")
|
167
|
-
|
168
|
-
if success:
|
169
|
-
print(f"\n{Emojis.RUNNING} {Colors.BRIGHT_GREEN}所有服务已成功重启!{Colors.RESET}")
|
170
|
-
else:
|
171
|
-
print(f"\n{Emojis.WARNING} {Colors.YELLOW}有些服务重启失败,请检查日志获取详情。{Colors.RESET}")
|
172
|
-
|
173
|
-
return success
|
174
|
-
else:
|
175
|
-
result = manager.restart(service_name)
|
176
|
-
if result:
|
177
|
-
print(f"{Emojis.RUNNING} {Colors.MAGENTA}服务 '{service_name}' 已成功重启{Colors.RESET}")
|
178
|
-
else:
|
179
|
-
print(f"{Emojis.ERROR} {Colors.RED}重启服务 '{service_name}' 失败{Colors.RESET}")
|
180
|
-
return result
|
181
|
-
|
182
|
-
def handle_log(manager: ServiceManager, log_manager: LogManager, args) -> bool:
|
183
|
-
"""处理日志查看命令"""
|
184
|
-
service_name = args.service
|
185
|
-
follow = not args.no_follow
|
186
|
-
lines = args.lines
|
187
|
-
|
188
|
-
if service_name == 'all':
|
189
|
-
services = manager.get_service_names()
|
190
|
-
if not services:
|
191
|
-
print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
|
192
|
-
return False
|
193
|
-
else:
|
194
|
-
if service_name not in manager.get_service_names():
|
195
|
-
print(f"{Emojis.ERROR} {Colors.RED}服务 '{service_name}' 在配置中未找到。{Colors.RESET}")
|
196
|
-
return False
|
197
|
-
services = [service_name]
|
198
|
-
|
199
|
-
# 已经在 LogManager 中添加了彩色输出
|
200
|
-
log_manager.tail_logs(services, follow=follow, lines=lines)
|
201
|
-
return True
|
202
|
-
|
203
|
-
def handle_list(manager: ServiceManager) -> bool:
|
204
|
-
"""处理列出服务命令"""
|
205
|
-
service_names = manager.get_service_names()
|
206
|
-
|
207
|
-
if not service_names:
|
208
|
-
print(f"{Emojis.WARNING} {Colors.YELLOW}配置中没有定义任何服务。{Colors.RESET}")
|
209
|
-
return True
|
210
|
-
|
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}")
|
218
|
-
|
219
|
-
# 服务列表
|
220
|
-
for name in service_names:
|
221
|
-
is_running = manager.is_running(name)
|
222
|
-
pid = manager.get_service_pid(name) or '-'
|
223
|
-
|
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}")
|
241
|
-
|
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()
|
245
|
-
|
246
|
-
return True
|
247
|
-
|
248
|
-
def main():
|
249
|
-
"""CLI 应用程序入口点"""
|
250
|
-
# 显示头部
|
251
|
-
print_header()
|
252
|
-
|
253
|
-
parser = setup_arg_parser()
|
254
|
-
args = parser.parse_args()
|
255
|
-
|
256
|
-
if not args.command:
|
257
|
-
parser.print_help()
|
258
|
-
return 1
|
259
|
-
|
260
|
-
# 创建服务管理器
|
261
|
-
try:
|
262
|
-
service_manager = ServiceManager(config_path=args.config)
|
263
|
-
except Exception as e:
|
264
|
-
print(f"{Emojis.ERROR} {Colors.RED}加载配置文件时出错: {e}{Colors.RESET}")
|
265
|
-
return 1
|
266
|
-
|
267
|
-
# 创建日志管理器
|
268
|
-
log_manager = LogManager(service_manager.log_dir)
|
269
|
-
|
270
|
-
# 处理命令
|
271
|
-
try:
|
272
|
-
if args.command == 'start':
|
273
|
-
success = handle_start(service_manager, args.service)
|
274
|
-
elif args.command == 'stop':
|
275
|
-
success = handle_stop(service_manager, args.service)
|
276
|
-
elif args.command == 'restart':
|
277
|
-
success = handle_restart(service_manager, args.service)
|
278
|
-
elif args.command == 'log':
|
279
|
-
success = handle_log(service_manager, log_manager, args)
|
280
|
-
elif args.command == 'list':
|
281
|
-
success = handle_list(service_manager)
|
282
|
-
else:
|
283
|
-
parser.print_help()
|
284
|
-
return 1
|
285
|
-
except KeyboardInterrupt:
|
286
|
-
print(f"\n{Emojis.STOP} {Colors.BRIGHT_BLACK}操作被用户中断{Colors.RESET}")
|
287
|
-
return 1
|
288
|
-
except Exception as e:
|
289
|
-
print(f"\n{Emojis.ERROR} {Colors.RED}执行命令时出错: {e}{Colors.RESET}")
|
290
|
-
return 1
|
291
|
-
|
292
|
-
return 0 if success else 1
|
293
|
-
|
294
|
-
if __name__ == "__main__":
|
295
|
-
sys.exit(main())
|
servly-0.2.0/servly/logs.py
DELETED
@@ -1,266 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Log management functionality for Servly.
|
3
|
-
"""
|
4
|
-
import os
|
5
|
-
import sys
|
6
|
-
import time
|
7
|
-
import select
|
8
|
-
import fcntl
|
9
|
-
import termios
|
10
|
-
import subprocess
|
11
|
-
import re
|
12
|
-
from pathlib import Path
|
13
|
-
from typing import List, Dict, Optional, Tuple
|
14
|
-
import random
|
15
|
-
|
16
|
-
|
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"
|
50
|
-
|
51
|
-
|
52
|
-
# 服务相关的 emoji
|
53
|
-
class Emojis:
|
54
|
-
SERVICE = "🔧"
|
55
|
-
START = "🟢"
|
56
|
-
STOP = "🔴"
|
57
|
-
RESTART = "🔄"
|
58
|
-
INFO = "ℹ️ "
|
59
|
-
WARNING = "⚠️ "
|
60
|
-
ERROR = "❌"
|
61
|
-
LOG = "📝"
|
62
|
-
STDOUT = "📤"
|
63
|
-
STDERR = "📥"
|
64
|
-
TIME = "🕒"
|
65
|
-
RUNNING = "✅"
|
66
|
-
STOPPED = "⛔"
|
67
|
-
LOADING = "⏳"
|
68
|
-
|
69
|
-
|
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)
|
75
|
-
|
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
|
87
|
-
|
88
|
-
def align_colored_text(text: str, width: int, align='left') -> str:
|
89
|
-
"""
|
90
|
-
将彩色文本对齐到指定宽度
|
91
|
-
|
92
|
-
Args:
|
93
|
-
text: 可能包含 ANSI 颜色代码的文本
|
94
|
-
width: 期望的显示宽度
|
95
|
-
align: 对齐方式,'left', 'right' 或 'center'
|
96
|
-
|
97
|
-
Returns:
|
98
|
-
对齐后的文本,保留颜色代码
|
99
|
-
"""
|
100
|
-
visible_length = get_visible_length(text)
|
101
|
-
padding = max(0, width - visible_length)
|
102
|
-
|
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
|
111
|
-
|
112
|
-
|
113
|
-
class LogManager:
|
114
|
-
"""Handles viewing and managing logs for servly services."""
|
115
|
-
|
116
|
-
def __init__(self, log_dir: Path):
|
117
|
-
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
|
-
]
|
125
|
-
|
126
|
-
def get_log_files(self, service_name: str) -> Dict[str, Path]:
|
127
|
-
"""Get the stdout and stderr log files for a service."""
|
128
|
-
return {
|
129
|
-
'stdout': self.log_dir / f"{service_name}-out.log",
|
130
|
-
'stderr': self.log_dir / f"{service_name}-error.log"
|
131
|
-
}
|
132
|
-
|
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 ""
|
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
|
170
|
-
else:
|
171
|
-
prefix = f"{Emojis.INFO} "
|
172
|
-
|
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}"
|
180
|
-
|
181
|
-
def tail_logs(self, service_names: List[str], follow: bool = True, lines: int = 10):
|
182
|
-
"""
|
183
|
-
Display logs for specified services in real-time.
|
184
|
-
|
185
|
-
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
|
189
|
-
"""
|
190
|
-
if not service_names:
|
191
|
-
print(f"{Emojis.WARNING} {Colors.YELLOW}No services specified for log viewing.{Colors.RESET}")
|
192
|
-
return
|
193
|
-
|
194
|
-
# Check if the logs exist
|
195
|
-
log_files = []
|
196
|
-
for service in service_names:
|
197
|
-
service_logs = self.get_log_files(service)
|
198
|
-
for log_type, log_path in service_logs.items():
|
199
|
-
if log_path.exists():
|
200
|
-
log_files.append((service, log_type, log_path))
|
201
|
-
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}.")
|
204
|
-
|
205
|
-
if not log_files:
|
206
|
-
print(f"{Emojis.WARNING} {Colors.YELLOW}No log files found for specified services.{Colors.RESET}")
|
207
|
-
return
|
208
|
-
|
209
|
-
if follow:
|
210
|
-
self._follow_logs(log_files)
|
211
|
-
else:
|
212
|
-
self._display_recent_logs(log_files, lines)
|
213
|
-
|
214
|
-
def _display_recent_logs(self, log_files: List[Tuple[str, str, Path]], lines: int):
|
215
|
-
"""Display the most recent lines from log files."""
|
216
|
-
for service, log_type, log_path in log_files:
|
217
|
-
print(self._format_log_header(service, log_type))
|
218
|
-
try:
|
219
|
-
# 读取最后N行
|
220
|
-
with open(log_path, 'r') as f:
|
221
|
-
content = f.readlines()
|
222
|
-
last_lines = content[-lines:] if len(content) >= lines else content
|
223
|
-
|
224
|
-
# 打印每一行,增加格式
|
225
|
-
for line in last_lines:
|
226
|
-
print(self._format_log_line(service, log_type, line, show_timestamp=False))
|
227
|
-
except Exception as e:
|
228
|
-
print(f"{Emojis.ERROR} {Colors.RED}Error reading logs: {str(e)}{Colors.RESET}")
|
229
|
-
|
230
|
-
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
|
233
|
-
file_handlers = {}
|
234
|
-
|
235
|
-
try:
|
236
|
-
# Open all log files
|
237
|
-
for service, log_type, log_path in log_files:
|
238
|
-
f = open(log_path, 'r')
|
239
|
-
# Move to the end of the file
|
240
|
-
f.seek(0, os.SEEK_END)
|
241
|
-
file_handlers[(service, log_type)] = f
|
242
|
-
|
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}")
|
248
|
-
|
249
|
-
while True:
|
250
|
-
has_new_data = False
|
251
|
-
|
252
|
-
for (service, log_type), f in file_handlers.items():
|
253
|
-
line = f.readline()
|
254
|
-
if line:
|
255
|
-
has_new_data = True
|
256
|
-
print(self._format_log_line(service, log_type, line))
|
257
|
-
|
258
|
-
if not has_new_data:
|
259
|
-
time.sleep(0.1)
|
260
|
-
|
261
|
-
except KeyboardInterrupt:
|
262
|
-
print(f"\n{Emojis.STOP} {Colors.BRIGHT_BLACK}Stopped following logs.{Colors.RESET}")
|
263
|
-
finally:
|
264
|
-
# Close all file handlers
|
265
|
-
for f in file_handlers.values():
|
266
|
-
f.close()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|