Files
HY2XS_flamy/apps/util/linux.go
T
founder cb20d8d28f fix(admin): связать отзыв учётных данных с идентичностью сессий и свести адрес control plane к одному
Отзыв секрета не сходился: `auth_id` при смене секрета оставался прежним,
поэтому сессия, установленная по отозванным учётным данным, была неотличима от
законной, и цикл учёта не имел признака, по которому её следовало завершить. У
состояния есть путь без единой неудачи — Hysteria регистрирует соединение в
Traffic Stats API только после возврата backend-auth, поэтому успешный /kick
может пройти мимо. Новое поколение credentials получает новый auth_id, kick идёт
по старому, пережившая сессия становится orphan.

Адрес Traffic Stats API имел два контракта: оркестратор принимал любой IPv4,
админка всегда шла на loopback. Валидная по всем гейтам конфигурация выключала
лимит устройств, учёт трафика и принудительное отключение разом. Адрес
зафиксирован, а расхождение файла с ним админка называет.

Состояние службы стало трёхзначным: util.Exec выбрасывал вывод systemctl при
ненулевом коде, поэтому «остановлена» и «спросить не удалось» приходили одним
значением, а доступность Traffic Stats API выводилась из него же. Журнал
Hysteria разбирается в фактическом формате upstream (time — дробное число),
страница конфигурации показывает файл вместо дефолтов UI и не возит секреты в
браузер, санитайзер выгрузки следует по YAML-якорям.

Разбор: docs/acceptance/2026-09-02-v1.0.0-rc4-preflight-findings.md
2026-09-02 23:24:01 +05:00

165 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}