mirror of
https://github.com/tnb-labs/panel.git
synced 2026-09-24 23:20:50 +08:00
依赖注入:
- 移除 google/wire(含 wire.go/wire_gen.go 与全部 ProviderSet),
改用 samber/do v2 单注入器 + 双入口惰性构建
- 贡献模型替代命令式注册:路由 routes:、命令 commands:、
任务 jobs: 前缀经 internal/registry 收集与校验
- 构造函数统一 func NewXxx(i do.Injector) (T, error)
三层架构:
- 补全用例层:每个 biz.XxxRepo 配 XxxUsecase,
service/command/job 表现层只依赖用例,不再直接引用仓储
路由与文档:
- route/http.go 按域拆为声明式 Endpoint 贡献,
Endpoint 承载登录白名单与端点限流语义
- 调试模式下提供 OpenAPI 3.1 文档:/openapi.json 与 /docs(Scalar),
从 validate 标签生成
目录与依赖:
- internal/http/{middleware,request,rule} 拍平至 internal/*
- CLI 命令拆至 internal/command
- 日志轮转 timberjack 换为 libtnb/logrotate
- 升级 validator/cron/sessions/gormstore/sqlite/securecookie
- 数据库迁移保持 gormigrate 不变
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
378 lines
9.7 KiB
Go
378 lines
9.7 KiB
Go
package supervisor
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/go-chi/chi/v5"
|
||
"github.com/leonelquinteros/gotext"
|
||
"github.com/libtnb/chix"
|
||
"github.com/samber/do/v2"
|
||
"github.com/spf13/cast"
|
||
|
||
"github.com/acepanel/panel/v3/internal/service"
|
||
"github.com/acepanel/panel/v3/pkg/io"
|
||
"github.com/acepanel/panel/v3/pkg/os"
|
||
"github.com/acepanel/panel/v3/pkg/shell"
|
||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||
"github.com/acepanel/panel/v3/pkg/types"
|
||
)
|
||
|
||
type App struct {
|
||
t *gotext.Locale
|
||
name string
|
||
}
|
||
|
||
func NewApp(i do.Injector) (*App, error) {
|
||
t := do.MustInvoke[*gotext.Locale](i)
|
||
var name string
|
||
if os.IsRHEL() {
|
||
name = "supervisord"
|
||
} else {
|
||
name = "supervisor"
|
||
}
|
||
|
||
return &App{
|
||
t: t,
|
||
name: name,
|
||
}, nil
|
||
}
|
||
|
||
func (s *App) Route(r chi.Router) {
|
||
r.Get("/service", s.Service)
|
||
r.Get("/config", s.GetConfig)
|
||
r.Post("/config", s.UpdateConfig)
|
||
r.Get("/processes", s.Processes)
|
||
r.Post("/processes/{process}/start", s.StartProcess)
|
||
r.Post("/processes/{process}/stop", s.StopProcess)
|
||
r.Post("/processes/{process}/restart", s.RestartProcess)
|
||
r.Get("/processes/{process}/log", s.ProcessLog)
|
||
r.Get("/processes/{process}", s.ProcessConfig)
|
||
r.Post("/processes/{process}", s.UpdateProcessConfig)
|
||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||
r.Post("/processes", s.CreateProcess)
|
||
}
|
||
|
||
// Service 获取服务名称
|
||
func (s *App) Service(w http.ResponseWriter, r *http.Request) {
|
||
service.Success(w, s.name)
|
||
}
|
||
|
||
func (s *App) Status() string {
|
||
ok, _ := systemctl.Status(s.name)
|
||
return types.AggregateAppStatus(ok)
|
||
}
|
||
|
||
// GetConfig 获取配置
|
||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||
var config string
|
||
var err error
|
||
if os.IsRHEL() {
|
||
config, err = io.Read(`/etc/supervisord.conf`)
|
||
} else {
|
||
config, err = io.Read(`/etc/supervisor/supervisord.conf`)
|
||
}
|
||
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
service.Success(w, config)
|
||
}
|
||
|
||
// UpdateConfig 保存配置
|
||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[UpdateConfig](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
if os.IsRHEL() {
|
||
err = io.Write(`/etc/supervisord.conf`, req.Config, 0644)
|
||
} else {
|
||
err = io.Write(`/etc/supervisor/supervisord.conf`, req.Config, 0644)
|
||
}
|
||
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
if err = systemctl.Restart(s.name); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart %s: %v", s.name, err))
|
||
return
|
||
}
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// Processes 进程列表
|
||
func (s *App) Processes(w http.ResponseWriter, r *http.Request) {
|
||
out, err := shell.Execf(`supervisorctl status`)
|
||
if err != nil && out == "" {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
var processes []Process
|
||
for line := range strings.SplitSeq(out, "\n") {
|
||
fields := strings.Fields(line)
|
||
if len(fields) < 2 {
|
||
continue
|
||
}
|
||
|
||
p := Process{
|
||
Name: fields[0],
|
||
Status: fields[1],
|
||
Pid: "-",
|
||
Uptime: "-",
|
||
}
|
||
// RUNNING 行格式:name RUNNING pid 1234, uptime 1:23:45 (超过 1 天为 "1 day, 1:23:45")
|
||
if p.Status == "RUNNING" {
|
||
if _, rest, ok := strings.Cut(line, "pid "); ok {
|
||
if pid, _, ok := strings.Cut(rest, ","); ok {
|
||
p.Pid = pid
|
||
}
|
||
}
|
||
if _, uptime, ok := strings.Cut(line, "uptime "); ok {
|
||
p.Uptime = uptime
|
||
}
|
||
}
|
||
processes = append(processes, p)
|
||
}
|
||
|
||
paged, total := service.Paginate(r, processes)
|
||
|
||
service.Success(w, chix.M{
|
||
"total": total,
|
||
"items": paged,
|
||
})
|
||
}
|
||
|
||
// StartProcess 启动进程
|
||
func (s *App) StartProcess(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ProcessName](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
if out, err := shell.Execf(`supervisorctl start '%s'`, req.Process); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||
return
|
||
}
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// StopProcess 停止进程
|
||
func (s *App) StopProcess(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ProcessName](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||
return
|
||
}
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// RestartProcess 重启进程
|
||
func (s *App) RestartProcess(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ProcessName](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
if out, err := shell.Execf(`supervisorctl restart '%s'`, req.Process); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||
return
|
||
}
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// ProcessLog 进程日志
|
||
func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ProcessName](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
name := programName(req.Process)
|
||
var logPath string
|
||
if os.IsRHEL() {
|
||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||
} else {
|
||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||
}
|
||
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||
return
|
||
}
|
||
|
||
service.Success(w, logPath)
|
||
}
|
||
|
||
// ProcessConfig 获取进程配置
|
||
func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ProcessName](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
name := programName(req.Process)
|
||
var config string
|
||
if os.IsRHEL() {
|
||
config, err = io.Read(`/etc/supervisord.d/` + name + `.conf`)
|
||
} else {
|
||
config, err = io.Read(`/etc/supervisor/conf.d/` + name + `.conf`)
|
||
}
|
||
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
service.Success(w, config)
|
||
}
|
||
|
||
// UpdateProcessConfig 保存进程配置
|
||
func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[UpdateProcessConfig](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
name := programName(req.Process)
|
||
if os.IsRHEL() {
|
||
err = io.Write(`/etc/supervisord.d/`+name+`.conf`, req.Config, 0644)
|
||
} else {
|
||
err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, req.Config, 0644)
|
||
}
|
||
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
_, _ = shell.Execf(`supervisorctl reread`)
|
||
_, _ = shell.Execf(`supervisorctl update`)
|
||
_, _ = shell.Execf(`supervisorctl restart '%s:'`, name)
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// CreateProcess 添加进程
|
||
func (s *App) CreateProcess(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[CreateProcess](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
processName := `%(program_name)s`
|
||
if req.Num > 1 {
|
||
processName = `%(program_name)s_%(process_num)02d`
|
||
}
|
||
|
||
config := `[program:` + req.Name + `]
|
||
command=` + req.Command + `
|
||
process_name=` + processName + `
|
||
directory=` + req.Path + `
|
||
autostart=true
|
||
autorestart=true
|
||
user=` + req.User + `
|
||
numprocs=` + cast.ToString(req.Num) + `
|
||
redirect_stderr=true
|
||
stdout_logfile=/var/log/supervisor/` + req.Name + `.log
|
||
stdout_logfile_maxbytes=2MB
|
||
`
|
||
|
||
var confPath string
|
||
if os.IsRHEL() {
|
||
confPath = `/etc/supervisord.d/` + req.Name + `.conf`
|
||
} else {
|
||
confPath = `/etc/supervisor/conf.d/` + req.Name + `.conf`
|
||
}
|
||
|
||
if io.Exists(confPath) {
|
||
service.Error(w, http.StatusConflict, s.t.Get("process %s already exists", req.Name))
|
||
return
|
||
}
|
||
|
||
if err = io.Write(confPath, config, 0644); err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
_, _ = shell.Execf(`supervisorctl reread`)
|
||
_, _ = shell.Execf(`supervisorctl update`)
|
||
_, _ = shell.Execf(`supervisorctl start '%s:'`, req.Name)
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// DeleteProcess 删除进程
|
||
func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ProcessName](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
name := programName(req.Process)
|
||
if out, err := shell.Execf(`supervisorctl stop '%s:'`, name); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||
return
|
||
}
|
||
|
||
var logPath string
|
||
if os.IsRHEL() {
|
||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||
return
|
||
}
|
||
if err = io.Remove(`/etc/supervisord.d/` + name + `.conf`); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
} else {
|
||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||
return
|
||
}
|
||
if err = io.Remove(`/etc/supervisor/conf.d/` + name + `.conf`); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
}
|
||
|
||
if err = io.Remove(logPath); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
_, _ = shell.Execf(`supervisorctl reread`)
|
||
_, _ = shell.Execf(`supervisorctl update`)
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
func programName(process string) string {
|
||
name, _, _ := strings.Cut(process, ":")
|
||
return name
|
||
}
|