servly 0.1.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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ Servly - A simple process management tool.
3
+ """
4
+
5
+ __version__ = "0.1.0"
servly/cli.py ADDED
@@ -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())
servly/logs.py ADDED
@@ -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()
servly/service.py ADDED
@@ -0,0 +1,215 @@
1
+ """
2
+ Service management functionality for Servly.
3
+ """
4
+ import os
5
+ import subprocess
6
+ import signal
7
+ import yaml
8
+ import logging
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Dict, Union, List, Optional, Any
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class ServiceManager:
16
+ """Manages processes based on servly.yml configuration."""
17
+
18
+ def __init__(self, config_path: str = "servly.yml", servly_dir: str = ".servly"):
19
+ self.config_path = config_path
20
+ # 修改为使用配置文件所在目录
21
+ config_dir = os.path.dirname(os.path.abspath(config_path))
22
+ self.servly_dir = Path(config_dir) / servly_dir
23
+ self.pid_dir = self.servly_dir / "pids"
24
+ self.log_dir = self.servly_dir / "logs"
25
+ self._ensure_dirs()
26
+ self.services = self._load_config()
27
+
28
+ def _ensure_dirs(self):
29
+ """Create required directories if they don't exist."""
30
+ self.pid_dir.mkdir(parents=True, exist_ok=True)
31
+ self.log_dir.mkdir(parents=True, exist_ok=True)
32
+
33
+ def _load_config(self) -> Dict[str, Any]:
34
+ """Load service configurations from servly.yml."""
35
+ if not os.path.exists(self.config_path):
36
+ logger.error(f"Configuration file not found: {self.config_path}")
37
+ return {}
38
+
39
+ try:
40
+ with open(self.config_path, 'r') as file:
41
+ config = yaml.safe_load(file) or {}
42
+
43
+ # Validate config and convert simple format to detailed format
44
+ validated_config = {}
45
+ for name, conf in config.items():
46
+ if name.lower() == "servly":
47
+ logger.warning(f"'servly' is a reserved name and cannot be used as a service name.")
48
+ continue
49
+
50
+ if isinstance(conf, str):
51
+ validated_config[name] = {"cmd": conf}
52
+ elif isinstance(conf, dict) and "cmd" in conf:
53
+ validated_config[name] = conf
54
+ elif isinstance(conf, dict) and "script" in conf:
55
+ validated_config[name] = conf
56
+ # Convert script to cmd for consistency
57
+ validated_config[name]["cmd"] = conf["script"]
58
+ else:
59
+ logger.warning(f"Invalid configuration for service '{name}', skipping.")
60
+
61
+ return validated_config
62
+ except Exception as e:
63
+ logger.error(f"Error loading configuration: {str(e)}")
64
+ return {}
65
+
66
+ def get_pid_file(self, service_name: str) -> Path:
67
+ """Get the path to a service's PID file."""
68
+ return self.pid_dir / f"{service_name}.pid"
69
+
70
+ def get_service_pid(self, service_name: str) -> Optional[int]:
71
+ """Get the PID for a running service, or None if not running."""
72
+ pid_file = self.get_pid_file(service_name)
73
+ if not pid_file.exists():
74
+ return None
75
+
76
+ try:
77
+ with open(pid_file, 'r') as f:
78
+ pid = int(f.read().strip())
79
+
80
+ # Check if process is still running
81
+ if self._is_process_running(pid):
82
+ return pid
83
+ else:
84
+ # Clean up stale PID file
85
+ os.remove(pid_file)
86
+ return None
87
+ except (ValueError, FileNotFoundError):
88
+ return None
89
+
90
+ def _is_process_running(self, pid: int) -> bool:
91
+ """Check if process with given PID is running."""
92
+ try:
93
+ os.kill(pid, 0)
94
+ return True
95
+ except ProcessLookupError:
96
+ return False
97
+ except PermissionError:
98
+ # Process exists but we don't have permission to send signals to it
99
+ return True
100
+
101
+ def is_running(self, service_name: str) -> bool:
102
+ """Check if a service is running."""
103
+ return self.get_service_pid(service_name) is not None
104
+
105
+ def get_service_names(self) -> List[str]:
106
+ """Get list of configured service names."""
107
+ return list(self.services.keys())
108
+
109
+ def get_running_services(self) -> List[str]:
110
+ """Get list of currently running services."""
111
+ return [name for name in self.get_service_names() if self.is_running(name)]
112
+
113
+ def start(self, service_name: str) -> bool:
114
+ """Start a specified service."""
115
+ if service_name not in self.services:
116
+ logger.error(f"Service '{service_name}' not found in configuration.")
117
+ return False
118
+
119
+ if self.is_running(service_name):
120
+ logger.info(f"Service '{service_name}' is already running.")
121
+ return True
122
+
123
+ config = self.services[service_name]
124
+ cmd = config.get("cmd")
125
+ if not cmd:
126
+ logger.error(f"No command specified for service '{service_name}'.")
127
+ return False
128
+
129
+ # Prepare environment variables
130
+ env = os.environ.copy()
131
+ if "env" in config and isinstance(config["env"], dict):
132
+ env.update(config["env"])
133
+
134
+ # Prepare working directory
135
+ cwd = config.get("cwd", os.getcwd())
136
+
137
+ # Prepare log files
138
+ stdout_log = self.log_dir / f"{service_name}-out.log"
139
+ stderr_log = self.log_dir / f"{service_name}-error.log"
140
+
141
+ try:
142
+ with open(stdout_log, 'a') as out, open(stderr_log, 'a') as err:
143
+ # Add timestamp to logs
144
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
145
+ out.write(f"\n--- Starting service '{service_name}' at {timestamp} ---\n")
146
+ err.write(f"\n--- Starting service '{service_name}' at {timestamp} ---\n")
147
+
148
+ # Start the process
149
+ process = subprocess.Popen(
150
+ cmd,
151
+ shell=True,
152
+ stdout=out,
153
+ stderr=err,
154
+ cwd=cwd,
155
+ env=env,
156
+ start_new_session=True # Detach from current process group
157
+ )
158
+
159
+ # Save the PID to file
160
+ with open(self.get_pid_file(service_name), 'w') as f:
161
+ f.write(str(process.pid))
162
+
163
+ logger.info(f"Started service '{service_name}' with PID {process.pid}")
164
+ return True
165
+
166
+ except Exception as e:
167
+ logger.error(f"Failed to start service '{service_name}': {str(e)}")
168
+ return False
169
+
170
+ def stop(self, service_name: str, timeout: int = 5) -> bool:
171
+ """Stop a specified service."""
172
+ pid = self.get_service_pid(service_name)
173
+ if not pid:
174
+ logger.info(f"Service '{service_name}' is not running.")
175
+ return True
176
+
177
+ try:
178
+ # First try SIGTERM for graceful shutdown
179
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
180
+
181
+ # Wait for process to terminate
182
+ for _ in range(timeout):
183
+ if not self._is_process_running(pid):
184
+ break
185
+ time.sleep(1)
186
+
187
+ # If still running, force kill with SIGKILL
188
+ if self._is_process_running(pid):
189
+ os.killpg(os.getpgid(pid), signal.SIGKILL)
190
+ time.sleep(0.5)
191
+
192
+ # Clean up PID file
193
+ pid_file = self.get_pid_file(service_name)
194
+ if os.path.exists(pid_file):
195
+ os.remove(pid_file)
196
+
197
+ logger.info(f"Stopped service '{service_name}'")
198
+ return True
199
+
200
+ except ProcessLookupError:
201
+ # Process already terminated
202
+ pid_file = self.get_pid_file(service_name)
203
+ if os.path.exists(pid_file):
204
+ os.remove(pid_file)
205
+ logger.info(f"Service '{service_name}' was not running")
206
+ return True
207
+
208
+ except Exception as e:
209
+ logger.error(f"Failed to stop service '{service_name}': {str(e)}")
210
+ return False
211
+
212
+ def restart(self, service_name: str) -> bool:
213
+ """Restart a service."""
214
+ self.stop(service_name)
215
+ return self.start(service_name)
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: servly
3
+ Version: 0.3.0
4
+ Summary: simple process manager
5
+ Author-email: simpxx <simpxx@gmail.com>
6
+ License: MIT
7
+ Keywords: command-line,tool
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: pyyaml>=6.0.2
14
+ Requires-Dist: rich>=14.0.0
15
+
16
+ # SERVLY
17
+
18
+ A simple process management tool for Linux, similar to PM2, designed to simplify application deployment and management.
19
+
20
+ ## Features
21
+
22
+ - Start, stop, and restart services defined in a `servly.yml` configuration file
23
+ - View real-time logs with service name highlighting
24
+ - Automatic process supervision and PID management
25
+ - Environment variable support
26
+ - Simple YAML configuration
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ # Using pip
32
+ pip install servly
33
+
34
+ # From source
35
+ git clone https://github.com/yourusername/servly.git
36
+ cd servly
37
+ pip install -e .
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Quick Start
43
+
44
+ 1. Create a `servly.yml` file in your project:
45
+
46
+ ```yaml
47
+ # Basic format: service-name: command
48
+ web-server: node server.js
49
+
50
+ # Detailed format
51
+ api:
52
+ cmd: python api.py
53
+ cwd: ./api
54
+ env:
55
+ NODE_ENV: production
56
+ ```
57
+
58
+ 2. Start your services:
59
+
60
+ ```bash
61
+ servly start
62
+ ```
63
+
64
+ ### Core Commands
65
+
66
+ - **Start Services**:
67
+ ```bash
68
+ servly start [all | service-name]
69
+ ```
70
+ Starts all services or a specific service by name.
71
+
72
+ - **Stop Services**:
73
+ ```bash
74
+ servly stop [all | service-name]
75
+ ```
76
+ Stops all running services or a specific service by name.
77
+
78
+ - **Restart Services**:
79
+ ```bash
80
+ servly restart [all | service-name]
81
+ ```
82
+ Restarts all services or a specific service by name.
83
+
84
+ - **View Service Logs**:
85
+ ```bash
86
+ servly log [all | service-name]
87
+ ```
88
+ Shows logs in real-time (similar to `tail -f`).
89
+
90
+ - **List Services**:
91
+ ```bash
92
+ servly list
93
+ ```
94
+ Shows status of all configured services.
95
+
96
+ ## Configuration
97
+
98
+ ### servly.yml
99
+
100
+ The `servly.yml` file supports two formats:
101
+
102
+ 1. **Simple format**:
103
+ ```yaml
104
+ service-name: command to run
105
+ ```
106
+
107
+ 2. **Detailed format**:
108
+ ```yaml
109
+ service-name:
110
+ cmd: command to run
111
+ cwd: working directory (optional)
112
+ env:
113
+ ENV_VAR1: value1
114
+ ENV_VAR2: value2
115
+ ```
116
+
117
+ **Note**: The name "servly" is reserved and cannot be used as a service name.
118
+
119
+ ### Directory Structure
120
+
121
+ Servly creates a `.servly` directory to store runtime information:
122
+
123
+ ```
124
+ .servly/
125
+ ├── logs/
126
+ │ ├── [service-name]-out.log # Standard output logs
127
+ │ └── [service-name]-error.log # Error logs
128
+ ├── pids/
129
+ │ └── [service-name].pid # Process ID files
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT
@@ -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,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ servly = servly.cli:main
@@ -0,0 +1 @@
1
+ servly
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: servly
3
- Version: 0.1.0
4
- Summary: simple process manager
5
- Author-email: simpxx <simpxx@gmail.com>
6
- License: MIT
7
- Keywords: command-line,tool
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.12
12
- Description-Content-Type: text/markdown
13
-
14
- # XM
@@ -1,7 +0,0 @@
1
- xm/__init__.py,sha256=L6zbQIZKsAP-Knhm6fBcQFPoVdIDuejxze60qX23jiw,21
2
- xm/cli.py,sha256=Wb9RNHjD2vJ3sysY52U9gORMDA6bVSnZ4sd6MmoA9rc,108
3
- servly-0.1.0.dist-info/METADATA,sha256=RwhsvIBoTDT6sg_UyUjU_H5kCe_6MHF-iA_xYn4HFsE,379
4
- servly-0.1.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
5
- servly-0.1.0.dist-info/entry_points.txt,sha256=tNnO4et40QVUJwNa0i2e5lOvMpcba6chlfwK4d4-SpA,35
6
- servly-0.1.0.dist-info/top_level.txt,sha256=4v0vf9bT58AtJn_CpjgroekLoaoDDo5QvmFqNby7gPc,3
7
- servly-0.1.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- xm = xm.cli:main
@@ -1 +0,0 @@
1
- xm
xm/__init__.py DELETED
@@ -1 +0,0 @@
1
- __version__ = '0.1.0'
xm/cli.py DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- def main():
4
- print("Hello World from xm!")
5
-
6
- if __name__ == "__main__":
7
- main()
File without changes