完善续期调度生命周期和Web开关配置
This commit is contained in:
parent
1f987eb8be
commit
f6ce85b174
@ -52,6 +52,7 @@ SMTP 通知是 Scert 的出站连接,不需要为 Scert 开放 SMTP 入站端
|
||||
SCERT_SERVER_HOST=127.0.0.1
|
||||
SCERT_SERVER_PORT=9004
|
||||
SCERT_SERVER_HTTPS_MODE=proxy
|
||||
SCERT_WEB_ENABLED=true
|
||||
SCERT_WEB_HOSTNAME=web.example.com
|
||||
SCERT_WEB_PUBLIC_IP=192.168.2.3
|
||||
SCERT_DATABASE_DRIVER=sqlite
|
||||
|
||||
@ -169,6 +169,9 @@ func (a *App) Close() error {
|
||||
return nil
|
||||
}
|
||||
var first error
|
||||
if a.Renewal != nil {
|
||||
a.Renewal.Stop()
|
||||
}
|
||||
if a.HTTP != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
if err := a.HTTP.Shutdown(ctx); err != nil && first == nil {
|
||||
|
||||
@ -36,6 +36,8 @@ type RenewalScheduler struct {
|
||||
policyMu sync.RWMutex
|
||||
runMu sync.Mutex
|
||||
running bool
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
wake chan struct{}
|
||||
}
|
||||
|
||||
@ -138,20 +140,31 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
s.cancel = cancel
|
||||
s.done = make(chan struct{})
|
||||
done := s.done
|
||||
s.mu.Unlock()
|
||||
go func() {
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
s.running = false
|
||||
s.cancel = nil
|
||||
s.done = nil
|
||||
s.mu.Unlock()
|
||||
close(done)
|
||||
}()
|
||||
report, err := s.RunOnce(ctx)
|
||||
report, err := s.RunOnce(runCtx)
|
||||
for {
|
||||
interval := s.Policy().CheckInterval
|
||||
if err == nil && report.NearExpiry > 0 {
|
||||
@ -159,7 +172,7 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
||||
}
|
||||
timer := time.NewTimer(interval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-runCtx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
@ -167,14 +180,35 @@ func (s *RenewalScheduler) Start(ctx context.Context) {
|
||||
case <-timer.C:
|
||||
case <-s.wake:
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
if runCtx.Err() != nil {
|
||||
return
|
||||
}
|
||||
report, err = s.RunOnce(ctx)
|
||||
report, err = s.RunOnce(runCtx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop 停止后台续期调度并等待当前检查安全退出。
|
||||
func (s *RenewalScheduler) Stop() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
if !s.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
cancel := s.cancel
|
||||
done := s.done
|
||||
s.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
if done != nil {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RenewalScheduler) issueWithRetry(ctx context.Context, domainID string, urgent bool, policy RenewalPolicy) error {
|
||||
attempts := 1
|
||||
if urgent {
|
||||
|
||||
@ -165,3 +165,34 @@ func TestRenewalSchedulerUpdatePolicyWakesRunningScheduler(t *testing.T) {
|
||||
t.Fatal("\u66f4\u65b0\u7eed\u671f\u7b56\u7565\u540e\u8c03\u5ea6\u5668\u672a\u88ab\u7acb\u5373\u5524\u9192")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewalSchedulerStopStopsRunningScheduler(t *testing.T) {
|
||||
lister := &wakeRenewalLister{calls: make(chan struct{}, 4)}
|
||||
scheduler := NewRenewalSchedulerWithPolicy(lister, &renewalService{}, RenewalPolicy{
|
||||
CheckInterval: time.Hour,
|
||||
RetryInterval: time.Nanosecond,
|
||||
})
|
||||
scheduler.Start(context.Background())
|
||||
select {
|
||||
case <-lister.calls:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("续期调度器启动后未执行首次检查")
|
||||
}
|
||||
|
||||
scheduler.Stop()
|
||||
scheduler.UpdatePolicy(RenewalPolicy{CheckInterval: time.Hour})
|
||||
select {
|
||||
case <-lister.calls:
|
||||
t.Fatal("续期调度器停止后不应继续执行检查")
|
||||
case <-time.After(30 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewalSchedulerStopIsIdempotent(t *testing.T) {
|
||||
scheduler := NewRenewalSchedulerWithPolicy(&wakeRenewalLister{calls: make(chan struct{}, 1)}, &renewalService{}, RenewalPolicy{CheckInterval: time.Hour})
|
||||
scheduler.Stop()
|
||||
scheduler.Stop()
|
||||
scheduler.Start(context.Background())
|
||||
scheduler.Stop()
|
||||
scheduler.Stop()
|
||||
}
|
||||
|
||||
@ -263,6 +263,23 @@ func (c *Config) ApplyEnvironment() error {
|
||||
*override.target = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
boolOverrides := []struct {
|
||||
name string
|
||||
target *bool
|
||||
}{
|
||||
{"SCERT_WEB_ENABLED", &c.Web.Enabled},
|
||||
}
|
||||
for _, override := range boolOverrides {
|
||||
value, ok := os.LookupEnv(override.name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
parsed, err := parseEnvironmentBool(override.name, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*override.target = parsed
|
||||
}
|
||||
intOverrides := []struct {
|
||||
name string
|
||||
target *int
|
||||
@ -291,6 +308,17 @@ func (c *Config) ApplyEnvironment() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEnvironmentBool(name, value string) (bool, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "true":
|
||||
return true, nil
|
||||
case "false":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("环境变量 %s 必须是 true 或 false", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Normalize() {
|
||||
if c == nil {
|
||||
return
|
||||
|
||||
@ -265,6 +265,7 @@ func TestLoadWithEnvOverridesRuntimeSettings(t *testing.T) {
|
||||
t.Setenv("SCERT_SERVER_HOST", "127.0.0.1")
|
||||
t.Setenv("SCERT_SERVER_PORT", "9004")
|
||||
t.Setenv("SCERT_SERVER_HTTPS_MODE", "proxy")
|
||||
t.Setenv("SCERT_WEB_ENABLED", "false")
|
||||
t.Setenv("SCERT_WEB_PUBLIC_IP", "192.168.2.3")
|
||||
t.Setenv("SCERT_ACME_PROPAGATION_TIMEOUT_SECS", "1200")
|
||||
loaded, err := LoadWithEnv(path)
|
||||
@ -274,7 +275,7 @@ func TestLoadWithEnvOverridesRuntimeSettings(t *testing.T) {
|
||||
if loaded.Server.Host != "127.0.0.1" || loaded.Server.Port != 9004 || loaded.Server.HTTPSMode != "proxy" {
|
||||
t.Fatalf("服务运行配置覆盖不正确:%+v", loaded.Server)
|
||||
}
|
||||
if loaded.Web.PublicIP != "192.168.2.3" || loaded.ACME.PropagationTimeoutSecs != 1200 {
|
||||
if loaded.Web.Enabled || loaded.Web.PublicIP != "192.168.2.3" || loaded.ACME.PropagationTimeoutSecs != 1200 {
|
||||
t.Fatalf("运行时辅助配置覆盖不正确:web=%+v acme=%+v", loaded.Web, loaded.ACME)
|
||||
}
|
||||
fileConfig, err := Load(path)
|
||||
@ -286,6 +287,19 @@ func TestLoadWithEnvOverridesRuntimeSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithEnvRejectsInvalidBoolean(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := Default(dir)
|
||||
path := filepath.Join(dir, "config", "scert.json")
|
||||
if err := cfg.Save(path); err != nil {
|
||||
t.Fatalf("保存配置失败:%v", err)
|
||||
}
|
||||
t.Setenv("SCERT_WEB_ENABLED", "是")
|
||||
if _, err := LoadWithEnv(path); err == nil {
|
||||
t.Fatal("无效 Web 开关环境变量未被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithEnvRejectsInvalidInteger(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := Default(dir)
|
||||
|
||||
@ -65,6 +65,19 @@ systemctl stop Scert-admin
|
||||
|
||||
`Scert-admin` 不监听任何 TCP/UDP 端口。启动后,它只维护 Scert 主服务 `/reset` 路径的短时激活租约,并将一次性校验码写入 `/run/scert-admin/reset-code` 受保护临时文件,不写入标准输出或 systemd 日志;停止后入口立即失效,异常退出后入口会在租约过期时失效。管理员可使用 `sudo cat /run/scert-admin/reset-code` 读取校验码,校验码在成功消费、服务停止或租约失效后自动销毁。云安全组只需由管理员根据初始化提示放行主服务端口。
|
||||
|
||||
### 环境变量覆盖
|
||||
|
||||
`Scert` 启动时支持使用 `SCERT_*` 环境变量覆盖当前进程配置,适合写入 systemd 的 `EnvironmentFile`。例如:
|
||||
|
||||
```text
|
||||
SCERT_SERVER_HOST=127.0.0.1
|
||||
SCERT_SERVER_PORT=9004
|
||||
SCERT_SERVER_HTTPS_MODE=proxy
|
||||
SCERT_WEB_ENABLED=true
|
||||
```
|
||||
|
||||
`SCERT_WEB_ENABLED=false` 只关闭 Scert 提供的 Web 静态页面,不会关闭核心 API 或桌面端本地管理能力。环境变量不会回写 `scert.json`;修改后需要重启 `Scert` 服务。
|
||||
|
||||
## 参数说明
|
||||
|
||||
- `--source-dir`:包含两个 Linux 可执行文件和 `web/index.html` 的目录,默认是安装器所在目录。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user