将证书维护纳入核心服务生命周期,避免重启时配置锁竞争
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-27 00:44:21 +08:00
parent e5931dbf71
commit 99919778a4
3 changed files with 74 additions and 24 deletions

View File

@ -6,7 +6,6 @@ import (
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/internal/buildinfo"
"git.pcux.cn/xuans/Smail/listen/cron_server"
"git.pcux.cn/xuans/Smail/res_init"
"git.pcux.cn/xuans/Smail/services/runtimelog"
"git.pcux.cn/xuans/Smail/utils/runtimepath"
@ -37,7 +36,6 @@ func Run() (err error) {
return fmt.Errorf("启动运行日志失败: %w", err)
}
defer finishRuntimeLogs(runtimeLogs, &err)
config.MaintainManagedSSLHistory()
build := buildinfo.Current()
if build.Version == "dev" {
@ -46,9 +44,6 @@ func Run() (err error) {
log.WithFields(log.Fields{"action": "启动主服务", "object": "Smail", "result": "started"}).Infof("服务启动,版本:%s", build.Version)
// 启动定时任务。
go cron_server.Start()
// 启动核心服务。
if err := res_init.Init(build.Version); err != nil {
return err

View File

@ -13,20 +13,35 @@ import (
"strconv"
)
func Start() {
// Runtime 将证书维护绑定到核心服务,避免恢复配置时并发争用状态锁。
type Runtime struct {
cancel context.CancelFunc
done chan struct{}
}
// 第一次启动,等待到初始化完成
if cfg := config.CurrentConfig(); cfg == nil || !cfg.IsSetupReady() {
for {
time.Sleep(1 * time.Minute)
if cfg := config.CurrentConfig(); cfg != nil && cfg.IsSetupReady() {
break
}
}
// Start 只能在配置恢复及核心服务初始化完成后调用。
func Start() *Runtime {
ctx, cancel := context.WithCancel(context.Background())
runtime := &Runtime{cancel: cancel, done: make(chan struct{})}
go func() {
defer close(runtime.done)
sslUpdateLoop(ctx)
}()
return runtime
}
// Stop 取消后续维护并等待当前操作结束;超时不能继续关闭配置及数据库。
func (runtime *Runtime) Stop(ctx context.Context) error {
if runtime == nil {
return nil
}
runtime.cancel()
select {
case <-runtime.done:
return nil
case <-ctx.Done():
return ctx.Err()
}
go sslUpdateLoop()
}
// 只检查外部维护的本地邮件证书是否更新,不触发 ACME。
@ -37,6 +52,13 @@ func sslCheck() {
// 每轮读取最新策略,切换反代模式后不再等待 HTTP 挑战或执行自动续期。
func maintainSSL() {
maintainSSLContext(context.Background())
}
func maintainSSLContext(ctx context.Context) {
if ctx.Err() != nil {
return
}
config.MaintainManagedSSLHistory()
cfg := config.CurrentCertificateConfig()
if cfg == nil || !cfg.IsSetupReady() {
@ -46,30 +68,43 @@ func maintainSSL() {
sslCheck()
return
}
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP && !waitHTTPReady() {
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP && !waitHTTPReadyContext(ctx) {
return
}
if ctx.Err() != nil {
return
}
ssl.Update(true)
}
// 每天检查证书,外部管理模式仅保留本地证书变更检查。
func sslUpdateLoop() {
func sslUpdateLoop(ctx context.Context) {
lastRenewal := time.Time{}
for {
for ctx.Err() == nil {
if time.Since(lastRenewal) >= 24*time.Hour {
maintainSSL()
maintainSSLContext(ctx)
lastRenewal = time.Now()
} else {
config.MaintainManagedSSLHistory()
sslCheck()
}
// 每分钟清理已到期的上一套证书;自动签发仍最多每天检查一次。
time.Sleep(time.Minute)
timer := time.NewTimer(time.Minute)
select {
case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
}
}
}
// 新增:HTTP 就绪探测(最多等待 ~90 秒)
func waitHTTPReady() bool {
return waitHTTPReadyContext(context.Background())
}
func waitHTTPReadyContext(ctx context.Context) bool {
port := config.CurrentHTTPConfig().Port
if port < 1 || port > 65535 {
log.Warnf("invalid HTTP port %d, skipping SSL update this round", port)
@ -84,10 +119,13 @@ func waitHTTPReady() bool {
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 90; i++ {
if ctx.Err() != nil {
return false
}
if !config.CurrentCertificateConfig().AutomaticSSLEnabled() {
return false
}
requestContext, cancel := context.WithTimeout(context.Background(), 2*time.Second)
requestContext, cancel := context.WithTimeout(ctx, 2*time.Second)
request := (&http.Request{
Method: http.MethodGet,
URL: target,
@ -105,7 +143,13 @@ func waitHTTPReady() bool {
log.Infof("HTTP ready: %s", target.String())
return true
}
time.Sleep(1 * time.Second)
timer := time.NewTimer(time.Second)
select {
case <-ctx.Done():
timer.Stop()
return false
case <-timer.C:
}
}
log.Warnf("HTTP not ready after 90s, skipping SSL update this round")
return false

View File

@ -16,6 +16,7 @@ import (
"git.pcux.cn/xuans/Smail/db/migration"
"git.pcux.cn/xuans/Smail/dto/parsemail"
"git.pcux.cn/xuans/Smail/hooks"
"git.pcux.cn/xuans/Smail/listen/cron_server"
"git.pcux.cn/xuans/Smail/listen/http_server"
"git.pcux.cn/xuans/Smail/listen/imap_server"
"git.pcux.cn/xuans/Smail/listen/pop3_server"
@ -52,6 +53,7 @@ var credentialService *credentials.Service
var webAuthnRuntimeService *webauthnservice.Service
var logRuntime *logstore.Runtime
var outboxRuntime *outbox.Runtime
var certificateRuntime *cron_server.Runtime
func startConfigManager() error {
root := filepath.Join(config.ROOT_PATH, "config")
@ -261,6 +263,7 @@ func Init(serverVersion string) error {
}
log.WithFields(log.Fields{"action": "启动邮箱监听", "object": "SMTP / IMAP / POP3", "result": "started"}).Info("邮箱监听任务已提交,具体监听结果由各服务报告")
certificateRuntime = cron_server.Start()
restart.SetCurrent(restart.NewSignalTrigger(signal.RestartChan))
select {
@ -293,6 +296,14 @@ func startService(serviceErrors chan<- error, name string, service func() error)
func stopServices() error {
restart.SetCurrent(nil)
// 先结束证书维护,再关闭其依赖;超时直接上报,禁止进入下一轮配置恢复。
certificateCtx, certificateCancel := context.WithTimeout(context.Background(), 25*time.Second)
certificateErr := certificateRuntime.Stop(certificateCtx)
certificateCancel()
if certificateErr != nil {
return fmt.Errorf("certificate maintenance did not stop; dependencies retained: %w", certificateErr)
}
certificateRuntime = nil
stopAdminResetControl()
smtp_server.Stop()
http_server.HttpsStop()