diff --git a/backend/internal/handler/admin/system_handler.go b/backend/internal/handler/admin/system_handler.go index ae6613a3..cd145e52 100644 --- a/backend/internal/handler/admin/system_handler.go +++ b/backend/internal/handler/admin/system_handler.go @@ -2,8 +2,10 @@ package admin import ( "net/http" + "time" "sub2api/internal/pkg/response" + "sub2api/internal/pkg/sysutil" "sub2api/internal/service" "github.com/gin-gonic/gin" @@ -72,10 +74,14 @@ func (h *SystemHandler) Rollback(c *gin.Context) { // RestartService restarts the systemd service // POST /api/v1/admin/system/restart func (h *SystemHandler) RestartService(c *gin.Context) { - if err := h.updateSvc.RestartService(); err != nil { - response.Error(c, http.StatusInternalServerError, err.Error()) - return - } + // Schedule service restart in background after sending response + // This ensures the client receives the success response before the service restarts + go func() { + // Wait a moment to ensure the response is sent + time.Sleep(500 * time.Millisecond) + sysutil.RestartServiceAsync() + }() + response.Success(c, gin.H{ "message": "Service restart initiated", }) diff --git a/backend/internal/pkg/sysutil/restart.go b/backend/internal/pkg/sysutil/restart.go new file mode 100644 index 00000000..aadf4aa7 --- /dev/null +++ b/backend/internal/pkg/sysutil/restart.go @@ -0,0 +1,51 @@ +package sysutil + +import ( + "fmt" + "log" + "os/exec" + "runtime" +) + +const serviceName = "sub2api" + +// RestartService triggers a service restart via systemd. +// +// IMPORTANT: This function initiates the restart and returns immediately. +// The actual restart happens asynchronously - the current process will be killed +// by systemd and a new process will be started. +// +// We use Start() instead of Run() because: +// - systemctl restart will kill the current process first +// - Run() waits for completion, but the process dies before completion +// - Start() spawns the command independently, allowing systemd to handle the full cycle +// +// Prerequisites: +// - Linux OS with systemd +// - NOPASSWD sudo access configured (install.sh creates /etc/sudoers.d/sub2api) +func RestartService() error { + if runtime.GOOS != "linux" { + return fmt.Errorf("systemd restart only available on Linux") + } + + log.Println("Initiating service restart...") + + // The sub2api user has NOPASSWD sudo access for systemctl commands + // (configured by install.sh in /etc/sudoers.d/sub2api). + cmd := exec.Command("sudo", "systemctl", "restart", serviceName) + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to initiate service restart: %w", err) + } + + log.Println("Service restart initiated successfully") + return nil +} + +// RestartServiceAsync is a fire-and-forget version of RestartService. +// It logs errors instead of returning them, suitable for goroutine usage. +func RestartServiceAsync() { + if err := RestartService(); err != nil { + log.Printf("Service restart failed: %v", err) + log.Println("Please restart the service manually: sudo systemctl restart sub2api") + } +} diff --git a/backend/internal/service/update_service.go b/backend/internal/service/update_service.go index 54c0fd16..b25bd87e 100644 --- a/backend/internal/service/update_service.go +++ b/backend/internal/service/update_service.go @@ -13,7 +13,6 @@ import ( "net/http" "net/url" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -244,23 +243,6 @@ func (s *UpdateService) Rollback() error { return nil } -// RestartService triggers a service restart via systemd -func (s *UpdateService) RestartService() error { - if runtime.GOOS != "linux" { - return fmt.Errorf("systemd restart only available on Linux") - } - - // Try direct systemctl first (works if running as root or with proper permissions) - cmd := exec.Command("systemctl", "restart", "sub2api") - if err := cmd.Run(); err != nil { - // Try with sudo (requires NOPASSWD sudoers entry) - sudoCmd := exec.Command("sudo", "systemctl", "restart", "sub2api") - if sudoErr := sudoCmd.Run(); sudoErr != nil { - return fmt.Errorf("systemctl restart failed: %w (sudo also failed: %v)", err, sudoErr) - } - } - return nil -} func (s *UpdateService) fetchLatestRelease(ctx context.Context) (*UpdateInfo, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", githubRepo) diff --git a/backend/internal/setup/handler.go b/backend/internal/setup/handler.go index a8e73a03..ebd02ed5 100644 --- a/backend/internal/setup/handler.go +++ b/backend/internal/setup/handler.go @@ -2,17 +2,15 @@ package setup import ( "fmt" - "log" "net/http" "net/mail" - "os/exec" "regexp" - "runtime" "strings" "sync" "time" "sub2api/internal/pkg/response" + "sub2api/internal/pkg/sysutil" "github.com/gin-gonic/gin" ) @@ -346,7 +344,7 @@ func install(c *gin.Context) { go func() { // Wait a moment to ensure the response is sent time.Sleep(500 * time.Millisecond) - triggerServiceRestart() + sysutil.RestartServiceAsync() }() response.Success(c, gin.H{ @@ -355,27 +353,3 @@ func install(c *gin.Context) { }) } -// triggerServiceRestart attempts to restart the service via systemd -// This is called after setup completes to switch from setup mode to normal mode -func triggerServiceRestart() { - if runtime.GOOS != "linux" { - log.Println("Service restart: not on Linux, manual restart required") - return - } - - log.Println("Setup completed, triggering service restart...") - - // Try direct systemctl first (works if running as root or with proper permissions) - cmd := exec.Command("systemctl", "restart", "sub2api") - if err := cmd.Run(); err != nil { - // Try with sudo (requires NOPASSWD sudoers entry) - sudoCmd := exec.Command("sudo", "systemctl", "restart", "sub2api") - if sudoErr := sudoCmd.Run(); sudoErr != nil { - log.Printf("Service restart failed: %v (sudo also failed: %v)", err, sudoErr) - log.Println("Please restart the service manually: sudo systemctl restart sub2api") - return - } - } - - log.Println("Service restart initiated successfully") -}