将证书维护纳入核心服务生命周期,避免重启时配置锁竞争
This commit is contained in:
parent
fddfa5a670
commit
c32daa77cd
@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/config"
|
"git.pcux.cn/xuans/Smail/config"
|
||||||
"git.pcux.cn/xuans/Smail/internal/buildinfo"
|
"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/res_init"
|
||||||
"git.pcux.cn/xuans/Smail/services/runtimelog"
|
"git.pcux.cn/xuans/Smail/services/runtimelog"
|
||||||
"git.pcux.cn/xuans/Smail/utils/runtimepath"
|
"git.pcux.cn/xuans/Smail/utils/runtimepath"
|
||||||
@ -37,7 +36,6 @@ func Run() (err error) {
|
|||||||
return fmt.Errorf("启动运行日志失败: %w", err)
|
return fmt.Errorf("启动运行日志失败: %w", err)
|
||||||
}
|
}
|
||||||
defer finishRuntimeLogs(runtimeLogs, &err)
|
defer finishRuntimeLogs(runtimeLogs, &err)
|
||||||
config.MaintainManagedSSLHistory()
|
|
||||||
|
|
||||||
build := buildinfo.Current()
|
build := buildinfo.Current()
|
||||||
if build.Version == "dev" {
|
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)
|
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 {
|
if err := res_init.Init(build.Version); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -13,20 +13,35 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Start() {
|
// Runtime 将证书维护绑定到核心服务,避免恢复配置时并发争用状态锁。
|
||||||
|
type Runtime struct {
|
||||||
|
cancel context.CancelFunc
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
// 第一次启动,等待到初始化完成
|
// Start 只能在配置恢复及核心服务初始化完成后调用。
|
||||||
if cfg := config.CurrentConfig(); cfg == nil || !cfg.IsSetupReady() {
|
func Start() *Runtime {
|
||||||
for {
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
time.Sleep(1 * time.Minute)
|
runtime := &Runtime{cancel: cancel, done: make(chan struct{})}
|
||||||
if cfg := config.CurrentConfig(); cfg != nil && cfg.IsSetupReady() {
|
go func() {
|
||||||
break
|
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。
|
// 只检查外部维护的本地邮件证书是否更新,不触发 ACME。
|
||||||
@ -37,6 +52,13 @@ func sslCheck() {
|
|||||||
|
|
||||||
// 每轮读取最新策略,切换反代模式后不再等待 HTTP 挑战或执行自动续期。
|
// 每轮读取最新策略,切换反代模式后不再等待 HTTP 挑战或执行自动续期。
|
||||||
func maintainSSL() {
|
func maintainSSL() {
|
||||||
|
maintainSSLContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func maintainSSLContext(ctx context.Context) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
config.MaintainManagedSSLHistory()
|
config.MaintainManagedSSLHistory()
|
||||||
cfg := config.CurrentCertificateConfig()
|
cfg := config.CurrentCertificateConfig()
|
||||||
if cfg == nil || !cfg.IsSetupReady() {
|
if cfg == nil || !cfg.IsSetupReady() {
|
||||||
@ -46,30 +68,43 @@ func maintainSSL() {
|
|||||||
sslCheck()
|
sslCheck()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP && !waitHTTPReady() {
|
if cfg.EffectiveSSLType() == config.SSLTypeAutoHTTP && !waitHTTPReadyContext(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ssl.Update(true)
|
ssl.Update(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每天检查证书,外部管理模式仅保留本地证书变更检查。
|
// 每天检查证书,外部管理模式仅保留本地证书变更检查。
|
||||||
func sslUpdateLoop() {
|
func sslUpdateLoop(ctx context.Context) {
|
||||||
lastRenewal := time.Time{}
|
lastRenewal := time.Time{}
|
||||||
for {
|
for ctx.Err() == nil {
|
||||||
if time.Since(lastRenewal) >= 24*time.Hour {
|
if time.Since(lastRenewal) >= 24*time.Hour {
|
||||||
maintainSSL()
|
maintainSSLContext(ctx)
|
||||||
lastRenewal = time.Now()
|
lastRenewal = time.Now()
|
||||||
} else {
|
} else {
|
||||||
config.MaintainManagedSSLHistory()
|
config.MaintainManagedSSLHistory()
|
||||||
sslCheck()
|
sslCheck()
|
||||||
}
|
}
|
||||||
// 每分钟清理已到期的上一套证书;自动签发仍最多每天检查一次。
|
// 每分钟清理已到期的上一套证书;自动签发仍最多每天检查一次。
|
||||||
time.Sleep(time.Minute)
|
timer := time.NewTimer(time.Minute)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
timer.Stop()
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:HTTP 就绪探测(最多等待 ~90 秒)
|
// 新增:HTTP 就绪探测(最多等待 ~90 秒)
|
||||||
func waitHTTPReady() bool {
|
func waitHTTPReady() bool {
|
||||||
|
return waitHTTPReadyContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitHTTPReadyContext(ctx context.Context) bool {
|
||||||
port := config.CurrentHTTPConfig().Port
|
port := config.CurrentHTTPConfig().Port
|
||||||
if port < 1 || port > 65535 {
|
if port < 1 || port > 65535 {
|
||||||
log.Warnf("invalid HTTP port %d, skipping SSL update this round", port)
|
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}
|
client := &http.Client{Timeout: 2 * time.Second}
|
||||||
|
|
||||||
for i := 0; i < 90; i++ {
|
for i := 0; i < 90; i++ {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !config.CurrentCertificateConfig().AutomaticSSLEnabled() {
|
if !config.CurrentCertificateConfig().AutomaticSSLEnabled() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
requestContext, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
requestContext, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
request := (&http.Request{
|
request := (&http.Request{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
URL: target,
|
URL: target,
|
||||||
@ -105,7 +143,13 @@ func waitHTTPReady() bool {
|
|||||||
log.Infof("HTTP ready: %s", target.String())
|
log.Infof("HTTP ready: %s", target.String())
|
||||||
return true
|
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")
|
log.Warnf("HTTP not ready after 90s, skipping SSL update this round")
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"git.pcux.cn/xuans/Smail/db/migration"
|
"git.pcux.cn/xuans/Smail/db/migration"
|
||||||
"git.pcux.cn/xuans/Smail/dto/parsemail"
|
"git.pcux.cn/xuans/Smail/dto/parsemail"
|
||||||
"git.pcux.cn/xuans/Smail/hooks"
|
"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/http_server"
|
||||||
"git.pcux.cn/xuans/Smail/listen/imap_server"
|
"git.pcux.cn/xuans/Smail/listen/imap_server"
|
||||||
"git.pcux.cn/xuans/Smail/listen/pop3_server"
|
"git.pcux.cn/xuans/Smail/listen/pop3_server"
|
||||||
@ -52,6 +53,7 @@ var credentialService *credentials.Service
|
|||||||
var webAuthnRuntimeService *webauthnservice.Service
|
var webAuthnRuntimeService *webauthnservice.Service
|
||||||
var logRuntime *logstore.Runtime
|
var logRuntime *logstore.Runtime
|
||||||
var outboxRuntime *outbox.Runtime
|
var outboxRuntime *outbox.Runtime
|
||||||
|
var certificateRuntime *cron_server.Runtime
|
||||||
|
|
||||||
func startConfigManager() error {
|
func startConfigManager() error {
|
||||||
root := filepath.Join(config.ROOT_PATH, "config")
|
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("邮箱监听任务已提交,具体监听结果由各服务报告")
|
log.WithFields(log.Fields{"action": "启动邮箱监听", "object": "SMTP / IMAP / POP3", "result": "started"}).Info("邮箱监听任务已提交,具体监听结果由各服务报告")
|
||||||
|
certificateRuntime = cron_server.Start()
|
||||||
restart.SetCurrent(restart.NewSignalTrigger(signal.RestartChan))
|
restart.SetCurrent(restart.NewSignalTrigger(signal.RestartChan))
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -293,6 +296,14 @@ func startService(serviceErrors chan<- error, name string, service func() error)
|
|||||||
|
|
||||||
func stopServices() error {
|
func stopServices() error {
|
||||||
restart.SetCurrent(nil)
|
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()
|
stopAdminResetControl()
|
||||||
smtp_server.Stop()
|
smtp_server.Stop()
|
||||||
http_server.HttpsStop()
|
http_server.HttpsStop()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user