feat: auto-restart server when listen addr or admin path changes
Build and Push to GHCR / build-and-push (push) Has been cancelled

When admin config is updated with a new listen_addr or admin_path,
the server now automatically performs a graceful restart instead of
requiring the user to manually restart the service.

- cmd/main.go: refactor into a loop that supports graceful shutdown
  and restart via http.Server.Shutdown
- internal/admin/handlers.go: add SetOnRestart callback, triggered
  after config save when restart is required
- web/static/index.html: remove manual restart alert; auto-redirect
  to new admin path or show toast if port changed

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <[email protected]>
This commit is contained in:
Agent
2026-05-24 16:06:10 +08:00
co-authored by HAPI
parent 086f326841
commit e8f760e510
3 changed files with 78 additions and 24 deletions
+55 -19
View File
@@ -1,11 +1,13 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"mirror-proxy/internal/admin" "mirror-proxy/internal/admin"
"mirror-proxy/internal/auth" "mirror-proxy/internal/auth"
@@ -14,16 +16,7 @@ import (
"mirror-proxy/internal/proxy" "mirror-proxy/internal/proxy"
) )
func main() { func buildHandler(cfg *config.Config) (*admin.Handler, http.Handler) {
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "config.json"
}
cfg, err := config.Load(configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
rl := auth.NewRateLimiter() rl := auth.NewRateLimiter()
adminHandler := admin.NewHandler(cfg) adminHandler := admin.NewHandler(cfg)
@@ -112,16 +105,59 @@ func main() {
authProxy.ServeHTTP(w, r) authProxy.ServeHTTP(w, r)
}) })
os.MkdirAll("web/static", 0755) return adminHandler, middleware.CORS(mux)
}
handler := middleware.CORS(mux) func main() {
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "config.json"
}
fmt.Printf("Mirror Proxy Server starting on %s\n", cfg.ListenAddr) restartCh := make(chan struct{}, 1)
fmt.Printf("Admin panel: http://localhost%s%s/\n", cfg.ListenAddr, adminPath)
fmt.Printf("Admin user: %s\n", cfg.AdminUser)
fmt.Printf("Docker Hub: http://localhost%s/{linkID}/{token}/v2/...\n", cfg.ListenAddr)
fmt.Printf("GHCR: http://localhost%s/{linkID}/{token}/v2/...\n", cfg.ListenAddr)
fmt.Printf("GitHub: http://localhost%s/{linkID}/{token}/github/...\n", cfg.ListenAddr)
log.Fatal(http.ListenAndServe(cfg.ListenAddr, handler)) for {
cfg, err := config.Load(configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
adminHandler, handler := buildHandler(cfg)
adminHandler.SetOnRestart(func() {
select {
case restartCh <- struct{}{}:
default:
}
})
os.MkdirAll("web/static", 0755)
srv := &http.Server{
Addr: cfg.ListenAddr,
Handler: handler,
}
fmt.Printf("Mirror Proxy Server starting on %s\n", cfg.ListenAddr)
fmt.Printf("Admin panel: http://localhost%s%s/\n", cfg.ListenAddr, cfg.AdminPath)
fmt.Printf("Admin user: %s\n", cfg.AdminUser)
fmt.Printf("Docker Hub: http://localhost%s/{linkID}/{token}/v2/...\n", cfg.ListenAddr)
fmt.Printf("GHCR: http://localhost%s/{linkID}/{token}/v2/...\n", cfg.ListenAddr)
fmt.Printf("GitHub: http://localhost%s/{linkID}/{token}/github/...\n", cfg.ListenAddr)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("Server error: %v", err)
}
}()
<-restartCh
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server shutdown error: %v", err)
}
cancel()
log.Println("Server restarting with new configuration...")
}
} }
+10 -1
View File
@@ -13,13 +13,18 @@ import (
) )
type Handler struct { type Handler struct {
cfg *config.Config cfg *config.Config
onRestart func()
} }
func NewHandler(cfg *config.Config) *Handler { func NewHandler(cfg *config.Config) *Handler {
return &Handler{cfg: cfg} return &Handler{cfg: cfg}
} }
func (h *Handler) SetOnRestart(fn func()) {
h.onRestart = fn
}
// GetLinks 获取所有链接 // GetLinks 获取所有链接
func (h *Handler) GetLinks(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetLinks(w http.ResponseWriter, r *http.Request) {
links := h.cfg.ListLinks() links := h.cfg.ListLinks()
@@ -226,6 +231,10 @@ func (h *Handler) UpdateConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
if restartRequired && h.onRestart != nil {
go h.onRestart()
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"listen_addr": h.cfg.ListenAddr, "listen_addr": h.cfg.ListenAddr,
+13 -4
View File
@@ -650,12 +650,21 @@
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
currentConfig = data; currentConfig = data;
showToast('保存成功');
closeSettingsModal();
if (data.restart_required) { if (data.restart_required) {
showToast('配置已保存,服务正在应用新配置');
closeSettingsModal();
setTimeout(() => { setTimeout(() => {
alert('配置已保存,由于修改了监听地址或管理后台路径,需要重启服务才能生效。'); const newPort = data.listen_addr.includes(':') ? data.listen_addr.split(':').pop() : '';
}, 300); const currentPort = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
if (newPort && newPort !== currentPort) {
showToast('服务已在新端口启动,请使用新地址访问', 'warning');
} else {
window.location.href = data.admin_path + '/';
}
}, 1500);
} else {
showToast('保存成功');
closeSettingsModal();
} }
} else { } else {
showToast('保存失败', 'error'); showToast('保存失败', 'error');