package util import ( "errors" "fmt" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/mem" "github.com/sirupsen/logrus" "net" "os" "os/exec" "strconv" "strings" "time" ) func resolveDiskPath() string { if configured := strings.TrimSpace(os.Getenv("HY2XS_DISK_PATH")); configured != "" { return configured } return "/" } func GetEnvDiskPath() string { return strings.TrimSpace(os.Getenv("HY2XS_DISK_PATH")) } func Exec(cmd string) (string, error) { command := exec.Command("bash", "-c", cmd) command.Env = os.Environ() output, err := command.CombinedOutput() if err != nil { logrus.Errorf("execute command failed cmd: %s err: %v", cmd, err) return "", fmt.Errorf("execute command failed cmd: %s", cmd) } return string(output), nil } // ExecProbe запускает команду, для которой НЕНУЛЕВОЙ КОД ВОЗВРАТА — это ответ, // а не отказ. // // Зачем это отдельно от Exec. Exec выбрасывает вывод, как только код возврата // не нулевой, и схлопывает два разных события в одну ошибку: // // systemctl is-active -> "inactive", код 3 — служба неактивна; // systemctl запустить не удалось — состояние неизвестно. // // Для команд-действий (`systemctl restart`) такое поведение верно: там любой // ненулевой код означает неудачу. Но `is-active` — это ВОПРОС, и ответ на него // приезжает именно в stdout вместе с кодом 3. Пока в продукте не было способа // его прочитать, дашборд не мог отличить «Hysteria остановлена» от «спросить не // получилось» и показывал оба состояния как «остановлена». // // Возвращаемая ошибка означает ровно одно: процесс не удалось запустить или // дождаться. Ненулевой код возврата ошибкой здесь не является и приезжает // вызывающему числом. func ExecProbe(cmd string) (string, int, error) { command := exec.Command("bash", "-c", cmd) command.Env = os.Environ() output, err := command.CombinedOutput() if err == nil { return string(output), 0, nil } var exitErr *exec.ExitError if errors.As(err, &exitErr) { return string(output), exitErr.ExitCode(), nil } logrus.Errorf("probe command failed cmd: %s err: %v", cmd, err) return "", -1, fmt.Errorf("probe command failed cmd: %s", cmd) } func Systemctl(action string, unit string) error { _, err := Exec(fmt.Sprintf("systemctl %s %s", action, unit)) return err } func IsPortAvailable(port uint, network string) bool { if network == "tcp" { listener, err := net.ListenTCP(network, &net.TCPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: int(port), }) defer func() { if listener != nil { listener.Close() } }() if err != nil { logrus.Errorf("port %d is taken err: %s", port, err) return false } } if network == "udp" { listener, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: int(port), }) defer func() { if listener != nil { listener.Close() } }() if err != nil { logrus.Errorf("port %d is taken err: %s", port, err) return false } } return true } func GetCpuPercent() (float64, error) { var err error percent, err := cpu.Percent(time.Second, false) value, err := strconv.ParseFloat(fmt.Sprintf("%.1f", percent[0]), 64) return value, err } func GetMemPercent() (float64, error) { var err error memInfo, err := mem.VirtualMemory() value, err := strconv.ParseFloat(fmt.Sprintf("%.1f", memInfo.UsedPercent), 64) return value, err } func GetMemInfo() (*mem.VirtualMemoryStat, error) { return mem.VirtualMemory() } func GetDiskPercent() (float64, error) { diskInfo, err := disk.Usage(resolveDiskPath()) if err != nil { return 0, err } value, err := strconv.ParseFloat(fmt.Sprintf("%.1f", diskInfo.UsedPercent), 64) return value, err } func GetDiskInfo() (*disk.UsageStat, error) { usage, err := disk.Usage(resolveDiskPath()) if err != nil { return nil, err } if usage == nil { return nil, errors.New("disk usage not found") } return usage, nil } func VerifyPort(port string) error { if port != "" { value, err := strconv.ParseInt(port, 10, 64) if err != nil { return errors.New("invalid port value") } if value <= 0 || value > 65535 { return errors.New("the port range is between 0-65535") } } return nil }