feat: auto-restart server when listen addr or admin path changes
Build and Push to GHCR / build-and-push (push) Has been cancelled
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:
+55
-19
@@ -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...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user