package service import ( "errors" "strings" "hy2xs-admin/util" ) // Жизненный цикл Hysteria принадлежит systemd и install-оркестратору. // Панель умеет только наблюдать состояние: функций Start/Stop/Restart и // смены версии здесь нет намеренно, а не «временно отключены». // InitHysteria2 проверяет единственное, что панели действительно нужно от // Hysteria на старте: наличие бинарника, версию которого показывает дашборд и // у которого спрашивает состояние HysteriaServiceState. // // Чтения HYSTERIA2_ENABLE здесь больше нет. Ключ был последним следом модели, // в которой панель сама запускала и останавливала Hysteria: clean install // создавал его со значением "0", записать "1" было некому, а вся ветка // сводилась к строке в журнале. Настройка, которая ничем не управляет, но // выглядит выключателем, хуже отсутствующей. func InitHysteria2() error { if !util.Exists(util.GetHysteria2BinPath()) { return errors.New("systemd-managed hysteria binary not found") } return nil } // Состояние службы имеет ТРИ значения, а не два. // // Что было. Ответ сводился к `bool`: // // func Hysteria2IsRunning() bool { // _, err := util.Exec("systemctl is-active --quiet hysteria-server") // return err == nil // } // // `false` при этом означал сразу две несовместимые вещи — «служба неактивна» и // «спросить не получилось», — потому что util.Exec выбрасывает вывод команды, // как только код возврата не нулевой, а `is-active` отвечает именно кодом 3 // вместе со словом состояния в stdout. Прочитать это слово было нечем. // // Цена такой неопределённости оказалась не в одной неверной плашке. Пока // «неизвестно» выглядело как «остановлена», на этом значении стояли решения: // цикл учёта и проверка лимита устройств пропускались при сломанном systemctl // и живой Hysteria. Оба гейта убраны (см. cron.go и Hysteria2Auth), и это // правильно, но сама неопределённость никуда не делась — она осталась в // диагностике, где как раз и вредна: // // Hysteria stopped // Traffic Stats API reachable // 0 online users // // Три утверждения об одной системе, из которых первое и второе не могут быть // верны одновременно. Оператор, увидев такое в аварии, ищет не там. // // Теперь состояние читается словом, а не кодом возврата, и неизвестность // называется неизвестностью. type HysteriaServiceState string const ( // HysteriaServiceActive — systemd сообщил "active". HysteriaServiceActive HysteriaServiceState = "active" // HysteriaServiceInactive — systemd назвал любое другое известное // состояние: inactive, failed, activating, deactivating. // // Различать их дальше продукту незачем: панель отвечает на вопрос «идёт ли // сейчас обслуживание», а не пересказывает конечный автомат systemd. HysteriaServiceInactive HysteriaServiceState = "inactive" // HysteriaServiceUnknown — спросить не удалось либо ответ не разобран. // // Это НЕ «служба остановлена». Единственное честное следствие — что о // состоянии службы сейчас ничего не известно. HysteriaServiceUnknown HysteriaServiceState = "unknown" ) // Hysteria2ServiceState — ответ systemd о состоянии службы, и НЕ БОЛЬШЕ ТОГО. // // Значение годится для отображения и ни для чего больше. Решения о доступе // принимаются по фактическому ответу Traffic Stats API — см. hysteria2Online и // disconnectAuthIDs, — и это правило стоило продукту трёх дыр: гейт // `if !Hysteria2IsRunning() { return }` в cron молча отключал и сбор трафика, и // принудительное отключение при сломанном systemctl. func Hysteria2ServiceState() HysteriaServiceState { return hysteriaServiceState() } // hysteriaServiceState — та же проверка в виде значения. // // Переменная нужна тестам: без неё каждая проверка, зависящая от состояния // службы, доказуема только на живом systemd, то есть не доказуема в сборке // вовсе. Продуктовый путь от этого не меняется — значение по умолчанию и есть // реализация. var hysteriaServiceState = func() HysteriaServiceState { // `--quiet` здесь недопустим: он подавляет ровно тот вывод, ради которого // команда вызывается. output, _, err := util.ExecProbe("systemctl is-active hysteria-server") if err != nil { return HysteriaServiceUnknown } return parseSystemdActiveState(output) } // parseSystemdActiveState разбирает ответ `systemctl is-active`. // // Слово состояния приезжает первой строкой. Неизвестное значение считается // неизвестным состоянием, а не «неактивна»: список состояний systemd // пополнялся и раньше, и молчаливое приведение нового слова к «остановлена» // вернуло бы ровно ту ложь, от которой избавляет этот тип. func parseSystemdActiveState(output string) HysteriaServiceState { state := strings.TrimSpace(output) if idx := strings.IndexAny(state, "\r\n"); idx >= 0 { state = strings.TrimSpace(state[:idx]) } switch state { case "active": return HysteriaServiceActive case "inactive", "failed", "activating", "deactivating", "reloading", "maintenance": return HysteriaServiceInactive default: return HysteriaServiceUnknown } }