fix: 修复一些已知问题

This commit is contained in:
耗子
2026-10-09 15:06:34 +08:00
parent 10aff4b928
commit e27e4c1770
11 changed files with 97 additions and 44 deletions
+4 -2
View File
@@ -71,12 +71,13 @@ func initAce() (*app.Ace, func() error, error) {
wireValue18_0 := caddy.NewApp(wireValue1_0)
wireValue4_0, wireErr4 := bootstrap.NewDB(wireValue0_0)
wireValue4_0, wireResourceCleanup4, wireErr4 := bootstrap.NewDB(wireValue0_0)
if wireErr4 != nil {
wireCommitted = true
cleanupErr := wireCleanup.close()
return wireZero0, nil, errors.Join(wireErr4, cleanupErr)
}
wireCleanup.arm(wireResourceCleanup4)
wireValue106_0 := data.NewDatabaseServerRepo(wireValue4_0)
@@ -177,12 +178,13 @@ func initAce() (*app.Ace, func() error, error) {
wireValue123_0 := data.NewUserTokenRepo(wireValue0_0, wireValue4_0, wireValue1_0)
wireValue9_0, wireErr9 := middleware.NewMiddlewares(wireValue0_0, wireValue1_0, wireValue6_0, wireValue90_0, wireValue123_0)
wireValue9_0, wireResourceCleanup9, wireErr9 := middleware.NewMiddlewares(wireValue0_0, wireValue1_0, wireValue6_0, wireValue90_0, wireValue123_0)
if wireErr9 != nil {
wireCommitted = true
cleanupErr := wireCleanup.close()
return wireZero0, nil, errors.Join(wireErr9, cleanupErr)
}
wireCleanup.arm(wireResourceCleanup9)
wireValue8_0 := bootstrap.NewValidator(wireValue0_0, wireValue4_0, wireValue1_0)
+2 -1
View File
@@ -30,12 +30,13 @@ func initCli() (*app.Cli, func() error, error) {
wireValue1_0 := bootstrap.NewT(wireValue0_0)
wireValue4_0, wireErr4 := bootstrap.NewDB(wireValue0_0)
wireValue4_0, wireResourceCleanup4, wireErr4 := bootstrap.NewDB(wireValue0_0)
if wireErr4 != nil {
wireCommitted = true
cleanupErr := wireCleanup.close()
return wireZero0, nil, errors.Join(wireErr4, cleanupErr)
}
wireCleanup.arm(wireResourceCleanup4)
wireValue2_0, wireResourceCleanup2, wireErr2 := bootstrap.NewLogger(wireValue0_0)
if wireErr2 != nil {
+7 -5
View File
@@ -1,6 +1,7 @@
package bootstrap
import (
"errors"
"log/slog"
"path/filepath"
"time"
@@ -16,7 +17,8 @@ import (
"github.com/acepanel/panel/v3/pkg/config"
)
func NewDB(conf *config.Config) (*gorm.DB, error) {
// NewDB 打开面板数据库,返回的 cleanup 在进程退出时关闭连接
func NewDB(conf *config.Config) (*gorm.DB, func() error, error) {
// db 日志写入轮转文件
w, err := logrotate.New(filepath.Join(app.Root, "panel/storage/logs/db.log"),
logrotate.WithMaxSize(10*logrotate.MB),
@@ -26,7 +28,7 @@ func NewDB(conf *config.Config) (*gorm.DB, error) {
logrotate.WithLocation(time.Local),
)
if err != nil {
return nil, err
return nil, nil, err
}
handler := slog.New(slog.NewJSONHandler(w, nil)).Handler()
@@ -42,17 +44,17 @@ func NewDB(conf *config.Config) (*gorm.DB, error) {
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
return nil, err
return nil, nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
return nil, nil, err
}
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
return db, nil
return db, func() error { return errors.Join(sqlDB.Close(), w.Close()) }, nil
}
func NewMigrate(db *gorm.DB) *gormigrate.Gormigrate {
+10 -8
View File
@@ -1670,10 +1670,12 @@ func (r *backupRepo) FixPanel(ctx context.Context) error {
return errors.New(r.t.Get("Files are normal and do not need to be repaired, please run acepanel update to update the panel"))
}
// 有异常,先停止面板
// 有异常,先同步停掉面板再动文件,避免运行中的面板往新目录写东西
// 停掉面板后的修复被取消会让面板起不来,后续全程不可取消
ctx = context.WithoutCancel(ctx)
tools.StopPanel(ctx)
if err := systemctl.Stop(ctx, "acepanel"); err != nil {
return errors.New(r.t.Get("Failed to stop panel: %v", err))
}
// 删除损坏的辅助数据库(会自动重建)
for _, name := range brokenAuxDBs {
@@ -1686,12 +1688,14 @@ func (r *backupRepo) FixPanel(ctx context.Context) error {
}
}
// 仅辅助数据库异常,重启即可恢复
// 仅辅助数据库异常,重新启动即可恢复
if !panelBroken {
if err := systemctl.Start(ctx, "acepanel"); err != nil {
return errors.New(r.t.Get("Failed to start panel: %v", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Fix completed"))
}
tools.RestartPanel(ctx)
return nil
}
@@ -1727,6 +1731,7 @@ func (r *backupRepo) FixPanel(ctx context.Context) error {
}
// restorePanel 用指定的面板备份覆盖当前面板,完成后重启面板
// 面板内恢复备份与 CLI 修复共用,重启必须走 RestartPanel 的延迟排程,面板内调用才不会自杀在半路
func (r *backupRepo) restorePanel(ctx context.Context, backup string) error {
// 覆盖的是运行中的面板二进制与 systemd 单元,中途取消会留下起不来的面板
ctx = context.WithoutCancel(ctx)
@@ -1898,10 +1903,7 @@ func (r *backupRepo) UpdatePanel(ctx context.Context, version, url, checksum str
r.log.Info("panel updated", slog.String("version", version))
progress(r.t.Get("Update completed"))
// 由调用方重启面板
if sqlDB, err := r.db.DB(); err == nil {
_ = sqlDB.Close()
}
// 由调用方重启面板,数据库连接在进程退出时统一关闭
return nil
}
+3 -3
View File
@@ -28,7 +28,7 @@ type Middlewares struct {
userToken biz.UserTokenRepo
}
func NewMiddlewares(conf *config.Config, t *gotext.Locale, session *sessions.Manager, appRepo biz.AppRepo, userTokenRepo biz.UserTokenRepo) (*Middlewares, error) {
func NewMiddlewares(conf *config.Config, t *gotext.Locale, session *sessions.Manager, appRepo biz.AppRepo, userTokenRepo biz.UserTokenRepo) (*Middlewares, func() error, error) {
// http 访问日志写入轮转文件
w, err := logrotate.New(filepath.Join(app.Root, "panel/storage/logs/http.log"),
logrotate.WithMaxSize(10*logrotate.MB),
@@ -38,7 +38,7 @@ func NewMiddlewares(conf *config.Config, t *gotext.Locale, session *sessions.Man
logrotate.WithLocation(time.Local),
)
if err != nil {
return nil, err
return nil, nil, err
}
return &Middlewares{
@@ -48,7 +48,7 @@ func NewMiddlewares(conf *config.Config, t *gotext.Locale, session *sessions.Man
session: session,
appRepo: appRepo,
userToken: userTokenRepo,
}, nil
}, w.Close, nil
}
// Globals 全局中间件集合,应用到每个请求;whitelist 为登录白名单路径。
+4 -1
View File
@@ -44,7 +44,10 @@ func (s *FirewallService) GetStatus(w http.ResponseWriter, r *http.Request) {
return
}
Success(w, running)
Success(w, chix.M{
"running": running,
"name": s.firewall.Name(),
})
}
func (s *FirewallService) UpdateStatus(w http.ResponseWriter, r *http.Request) {
+27 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/spf13/cast"
"github.com/acepanel/panel/v3/internal/request"
"github.com/acepanel/panel/v3/pkg/firewall"
"github.com/acepanel/panel/v3/pkg/io"
"github.com/acepanel/panel/v3/pkg/os"
"github.com/acepanel/panel/v3/pkg/shell"
@@ -17,8 +18,9 @@ import (
)
type ToolboxSSHService struct {
t *gotext.Locale
service string
t *gotext.Locale
service string
firewall firewall.Firewall
}
func NewToolboxSSHService(t *gotext.Locale) *ToolboxSSHService {
@@ -28,8 +30,9 @@ func NewToolboxSSHService(t *gotext.Locale) *ToolboxSSHService {
service = "ssh"
}
return &ToolboxSSHService{
t: t,
service: service,
t: t,
service: service,
firewall: firewall.NewFirewall(),
}
}
@@ -87,6 +90,26 @@ func (s *ToolboxSSHService) UpdatePort(w http.ResponseWriter, r *http.Request) {
return
}
// 先放行再改端口
running, err := s.firewall.Status(r.Context())
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if running {
if err = s.firewall.Port(r.Context(), firewall.FireInfo{
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Protocol: firewall.ProtocolTCP,
Strategy: firewall.StrategyAccept,
Direction: firewall.DirectionIn,
}, firewall.OperationAdd); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update firewall rule: %v", err))
return
}
}
if err = s.updateSSHConfig("Port", cast.ToString(req.Port)); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update SSH port: %v", err))
return
+23 -6
View File
@@ -13,8 +13,15 @@ import (
)
// Firewall 防火墙统一接口
// 变更方法由 NewFirewall 返回的实现统一加锁并断开取消链,各后端实现内无需再处理
type Firewall interface {
backend
// Name 当前使用的防火墙软件名,未检测到支持的防火墙时为空
Name() string
}
// backend 各防火墙软件需实现的操作
// 变更方法由 NewFirewall 返回的实现统一加锁并断开取消链,各后端实现内无需再处理
type backend interface {
// Status 获取防火墙运行状态
Status(ctx context.Context) (bool, error)
// Enable 启用防火墙
@@ -45,18 +52,24 @@ func NewFirewall() Firewall {
return lockedFirewall{}
}
// detected 探测到的防火墙后端及其软件名
type detected struct {
backend
name string
}
// detectFirewall 防火墙类型是开机后不再变化的环境事实,探测一次后全局复用
// 探测用不可取消 ctx:跟随调用方 ctx 时,取消会让两次探测都失败而退回 firewalld 默认值,并把这个错误结论缓存到进程结束
var detectFirewall = sync.OnceValue(func() Firewall {
var detectFirewall = sync.OnceValue(func() detected {
ctx := context.Background()
if _, err := shell.Execf(ctx, "firewall-cmd --version"); err == nil {
return newFirewalld()
return detected{newFirewalld(), "firewalld"}
}
if _, err := shell.Execf(ctx, "ufw version"); err == nil {
return newUFW()
return detected{newUFW(), "ufw"}
}
// 默认 firewalld
return newFirewalld()
// 都没装时按 firewalld 处理,名字留空供上层提示未安装
return detected{newFirewalld(), ""}
})
// mu 串行化所有防火墙变更操作
@@ -68,6 +81,10 @@ var mu sync.Mutex
// firewalld 下规则进了永久配置却没进 runtime,ufw 下面板读的规则文件已改而内核里的规则还在跑
type lockedFirewall struct{}
func (lockedFirewall) Name() string {
return detectFirewall().name
}
func (lockedFirewall) Status(ctx context.Context) (bool, error) {
return detectFirewall().Status(ctx)
}
+4 -9
View File
@@ -186,22 +186,17 @@ func CollectTopProcesses() types.TopProcesses {
return result
}
// 以下三个自杀式操作靠 sleep 1 熬过 HTTP 响应,ExecfAsync 入口已断开取消链,
// 调用方直接传请求 ctx 即可,无需再包 WithoutCancel
// StopPanel 停止面板
func StopPanel(ctx context.Context) {
_ = shell.ExecfAsync(ctx, "sleep 1 && systemctl stop acepanel")
}
// 以下两个自杀式操作交给 systemd 的瞬态定时器延迟 1 秒执行,既熬过 HTTP 响应,
// 又脱离面板自身的 cgroup,不会在停服务时被连带杀掉而误报命令失败
// RestartPanel 重启面板
func RestartPanel(ctx context.Context) {
_ = shell.ExecfAsync(ctx, "sleep 1 && systemctl restart acepanel")
_ = shell.ExecfAsync(ctx, "systemd-run --quiet --collect --on-active=1 systemctl restart acepanel")
}
// RestartServer 重启服务器
func RestartServer(ctx context.Context) {
_ = shell.ExecfAsync(ctx, "sleep 1 && reboot")
_ = shell.ExecfAsync(ctx, "systemd-run --quiet --collect --on-active=1 systemctl reboot")
}
// IsChina 是否中国大陆
+12 -4
View File
@@ -20,12 +20,14 @@ const currentTab = ref('rule')
// n-tabs 对 v-if 动态增删子 tab 不会重算指示条位置,用 key 强制重挂
const ruleTabs = ['rule', 'ip-rule', 'forward']
const firewallRunning = ref(false)
const firewallName = ref('')
const ready = ref(false)
const { send: refreshStatus } = useRequest(firewall.status)
.onSuccess(({ data }) => {
firewallRunning.value = data
if (!data && ruleTabs.includes(currentTab.value)) currentTab.value = 'scan'
firewallRunning.value = data.running
firewallName.value = data.name
if (!data.running && ruleTabs.includes(currentTab.value)) currentTab.value = 'scan'
})
.onComplete(() => {
ready.value = true
@@ -52,14 +54,20 @@ onUnmounted(() => {
<n-tab name="setting" :tab="$gettext('Settings')" />
</n-tabs>
</template>
<template v-if="ready">
<n-flex v-if="ready" vertical :size="20">
<n-alert v-if="firewallName" type="info" :bordered="false">
{{ $gettext('Current firewall: %{ name }', { name: firewallName }) }}
</n-alert>
<n-alert v-else type="warning" :bordered="false">
{{ $gettext('No supported firewall detected, please install firewalld or ufw') }}
</n-alert>
<rule-view v-if="currentTab === 'rule'" />
<ip-rule-view v-if="currentTab === 'ip-rule'" />
<forward-view v-if="currentTab === 'forward'" />
<scan-view v-if="currentTab === 'scan'" />
<tamper-view v-if="currentTab === 'tamper'" />
<setting-view v-if="currentTab === 'setting'" />
</template>
</n-flex>
</PageContainer>
</template>
+1 -1
View File
@@ -11,7 +11,7 @@ const model = ref({
})
useRequest(firewall.status).onSuccess(({ data }) => {
model.value.firewallStatus = data
model.value.firewallStatus = data.running
})
useRequest(safe.pingStatus).onSuccess(({ data }) => {
model.value.pingStatus = data