mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 05:04:22 +00:00
- Update go.mod module path from mhsanaei/3x-ui/v3 to saeederamy/3x-ui/v3 - Update all 73 Go files' import paths accordingly - Fix README.fa_IR.md install command to point to fork's main branch The fork was referencing the original repo's module path in go.mod and all Go source imports, making it dependent on MHSanaei's namespace at build time. https://claude.ai/code/session_01M6d5atbWjuLTj6UwRHoK5m
202 lines
5.4 KiB
Go
202 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/saeederamy/3x-ui/v3/config"
|
|
"github.com/saeederamy/3x-ui/v3/logger"
|
|
)
|
|
|
|
// PanelService provides business logic for panel management operations.
|
|
// It handles panel restart, updates, and system-level panel controls.
|
|
type PanelService struct{}
|
|
|
|
// PanelUpdateInfo contains the current and latest available panel versions.
|
|
type PanelUpdateInfo struct {
|
|
CurrentVersion string `json:"currentVersion"`
|
|
LatestVersion string `json:"latestVersion"`
|
|
UpdateAvailable bool `json:"updateAvailable"`
|
|
}
|
|
|
|
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
|
p, err := os.FindProcess(syscall.Getpid())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func() {
|
|
time.Sleep(delay)
|
|
err := p.Signal(syscall.SIGHUP)
|
|
if err != nil {
|
|
logger.Error("failed to send SIGHUP signal:", err)
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// GetUpdateInfo checks GitHub for the latest 3x-ui release.
|
|
func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) {
|
|
latest, err := fetchLatestPanelVersion()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
current := config.GetVersion()
|
|
return &PanelUpdateInfo{
|
|
CurrentVersion: current,
|
|
LatestVersion: latest,
|
|
UpdateAvailable: isNewerVersion(latest, current),
|
|
}, nil
|
|
}
|
|
|
|
// StartUpdate starts the official updater outside of the current web request.
|
|
func (s *PanelService) StartUpdate() error {
|
|
if runtime.GOOS != "linux" {
|
|
return fmt.Errorf("panel web update is supported only on Linux installations")
|
|
}
|
|
|
|
bash, err := exec.LookPath("bash")
|
|
if err != nil {
|
|
return fmt.Errorf("bash is required to run the panel updater: %w", err)
|
|
}
|
|
curl, err := exec.LookPath("curl")
|
|
if err != nil {
|
|
return fmt.Errorf("curl is required to download the panel updater: %w", err)
|
|
}
|
|
|
|
mainFolder, serviceFolder := resolveUpdateFolders()
|
|
updateScript := fmt.Sprintf("set -o pipefail; %s -fLs https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh | %s", shellQuote(curl), shellQuote(bash))
|
|
|
|
if systemdRun, err := exec.LookPath("systemd-run"); err == nil {
|
|
unitName := fmt.Sprintf("x-ui-web-update-%d", time.Now().Unix())
|
|
cmd := exec.Command(systemdRun,
|
|
"--unit", unitName,
|
|
"--setenv", "XUI_MAIN_FOLDER="+mainFolder,
|
|
"--setenv", "XUI_SERVICE="+serviceFolder,
|
|
bash, "-lc", updateScript,
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
output := strings.TrimSpace(string(out))
|
|
if !strings.Contains(output, "System has not been booted with systemd") &&
|
|
!strings.Contains(output, "Failed to connect to bus") {
|
|
return fmt.Errorf("failed to start panel update job: %w: %s", err, output)
|
|
}
|
|
logger.Warning("systemd-run is unavailable, falling back to detached update process:", output)
|
|
} else {
|
|
logger.Infof("started panel update job via systemd-run unit %s", unitName)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command(bash, "-lc", updateScript)
|
|
cmd.Env = append(os.Environ(),
|
|
"XUI_MAIN_FOLDER="+mainFolder,
|
|
"XUI_SERVICE="+serviceFolder,
|
|
)
|
|
setDetachedProcess(cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start panel update job: %w", err)
|
|
}
|
|
if err := cmd.Process.Release(); err != nil {
|
|
logger.Warning("failed to release panel update process:", err)
|
|
}
|
|
logger.Infof("started panel update job with pid %d", cmd.Process.Pid)
|
|
return nil
|
|
}
|
|
|
|
func fetchLatestPanelVersion() (string, error) {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Get("https://api.github.com/repos/MHSanaei/3x-ui/releases/latest")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
|
|
}
|
|
|
|
var release Release
|
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
return "", err
|
|
}
|
|
if release.TagName == "" {
|
|
return "", fmt.Errorf("latest panel release tag is empty")
|
|
}
|
|
return release.TagName, nil
|
|
}
|
|
|
|
func resolveUpdateFolders() (string, string) {
|
|
mainFolder := os.Getenv("XUI_MAIN_FOLDER")
|
|
if mainFolder == "" {
|
|
if exePath, err := os.Executable(); err == nil {
|
|
mainFolder = filepath.Dir(exePath)
|
|
}
|
|
}
|
|
if mainFolder == "" {
|
|
mainFolder = "/usr/local/x-ui"
|
|
}
|
|
|
|
serviceFolder := os.Getenv("XUI_SERVICE")
|
|
if serviceFolder == "" {
|
|
serviceFolder = "/etc/systemd/system"
|
|
}
|
|
return mainFolder, serviceFolder
|
|
}
|
|
|
|
func isNewerVersion(latest string, current string) bool {
|
|
cmp, ok := compareVersionStrings(latest, current)
|
|
if !ok {
|
|
return normalizeVersionTag(latest) != normalizeVersionTag(current)
|
|
}
|
|
return cmp > 0
|
|
}
|
|
|
|
func compareVersionStrings(a string, b string) (int, bool) {
|
|
aParts, okA := parseVersionParts(a)
|
|
bParts, okB := parseVersionParts(b)
|
|
if !okA || !okB {
|
|
return 0, false
|
|
}
|
|
for i := 0; i < len(aParts); i++ {
|
|
if aParts[i] > bParts[i] {
|
|
return 1, true
|
|
}
|
|
if aParts[i] < bParts[i] {
|
|
return -1, true
|
|
}
|
|
}
|
|
return 0, true
|
|
}
|
|
|
|
func parseVersionParts(version string) ([3]int, bool) {
|
|
var result [3]int
|
|
parts := strings.Split(normalizeVersionTag(version), ".")
|
|
if len(parts) != 3 {
|
|
return result, false
|
|
}
|
|
for i, part := range parts {
|
|
n, err := strconv.Atoi(part)
|
|
if err != nil {
|
|
return result, false
|
|
}
|
|
result[i] = n
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
func normalizeVersionTag(version string) string {
|
|
return strings.TrimPrefix(strings.TrimSpace(version), "v")
|
|
}
|
|
|
|
func shellQuote(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
|
}
|