platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
@@ -1,14 +1,5 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// фоновые вотчеры / события
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -17,226 +8,3 @@ import (
|
||||
// EN: publish normalized events into the in-memory event bus for SSE clients.
|
||||
// RU: Фоновые poll-вотчеры, отслеживающие изменения файлов/сервисов и
|
||||
// RU: публикующие нормализованные события в in-memory event bus для SSE-клиентов.
|
||||
|
||||
func startWatchers(ctx context.Context) {
|
||||
statusEvery := time.Duration(envInt("SVPN_POLL_STATUS_MS", defaultPollStatusMs)) * time.Millisecond
|
||||
loginEvery := time.Duration(envInt("SVPN_POLL_LOGIN_MS", defaultPollLoginMs)) * time.Millisecond
|
||||
autoEvery := time.Duration(envInt("SVPN_POLL_AUTOLOOP_MS", defaultPollAutoloopMs)) * time.Millisecond
|
||||
systemdEvery := time.Duration(envInt("SVPN_POLL_SYSTEMD_MS", defaultPollSystemdMs)) * time.Millisecond
|
||||
traceEvery := time.Duration(envInt("SVPN_POLL_TRACE_MS", defaultPollTraceMs)) * time.Millisecond
|
||||
appMarksEvery := time.Duration(envInt("SVPN_POLL_APPMARKS_MS", defaultPollAppMarksMs)) * time.Millisecond
|
||||
|
||||
go watchStatusFile(ctx, statusEvery)
|
||||
go watchLoginFile(ctx, loginEvery)
|
||||
go watchAutoloop(ctx, autoEvery)
|
||||
go watchFileChange(ctx, traceLogPath, "trace_changed", "full", traceEvery)
|
||||
go watchFileChange(ctx, smartdnsLogPath, "trace_changed", "smartdns", traceEvery)
|
||||
go watchTrafficAppMarksTTL(ctx, appMarksEvery)
|
||||
|
||||
go watchSystemdUnitDynamic(ctx, routesServiceUnitName, "routes_service", systemdEvery)
|
||||
go watchSystemdUnitDynamic(ctx, routesTimerUnitName, "routes_timer", systemdEvery)
|
||||
go watchSystemdUnit(ctx, adgvpnUnit, "vpn_unit", systemdEvery)
|
||||
go watchSystemdUnit(ctx, "smartdns-local.service", "smartdns_unit", systemdEvery)
|
||||
}
|
||||
|
||||
func watchTrafficAppMarksTTL(ctx context.Context, every time.Duration) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
_ = pruneExpiredAppMarks()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// status file watcher
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func watchStatusFile(ctx context.Context, every time.Duration) {
|
||||
var last [32]byte
|
||||
have := false
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(statusFilePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h := sha256.Sum256(data)
|
||||
if have && h == last {
|
||||
continue
|
||||
}
|
||||
last = h
|
||||
have = true
|
||||
|
||||
var st Status
|
||||
if err := json.Unmarshal(data, &st); err != nil {
|
||||
events.push("status_error", map[string]any{"error": err.Error()})
|
||||
continue
|
||||
}
|
||||
events.push("status_changed", st)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// login file watcher
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func watchLoginFile(ctx context.Context, every time.Duration) {
|
||||
var last [32]byte
|
||||
have := false
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(loginStatePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
h := sha256.Sum256(data)
|
||||
if have && h == last {
|
||||
continue
|
||||
}
|
||||
last = h
|
||||
have = true
|
||||
|
||||
var st VPNLoginState
|
||||
if err := json.Unmarshal(data, &st); err != nil {
|
||||
events.push("login_state_error", map[string]any{"error": err.Error()})
|
||||
continue
|
||||
}
|
||||
events.push("login_state_changed", st)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// autoloop watcher
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func watchAutoloop(ctx context.Context, every time.Duration) {
|
||||
lastWord := ""
|
||||
lastRaw := ""
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
|
||||
lines := tailFile(autoloopLogPath, 200)
|
||||
word, raw := parseAutoloopStatus(lines)
|
||||
if word == "" && raw == "" {
|
||||
continue
|
||||
}
|
||||
if word == lastWord && raw == lastRaw {
|
||||
continue
|
||||
}
|
||||
lastWord, lastRaw = word, raw
|
||||
events.push("autoloop_status_changed", map[string]string{
|
||||
"status_word": word,
|
||||
"raw_text": raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// systemd unit watcher
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func watchSystemdUnit(ctx context.Context, unit string, kind string, every time.Duration) {
|
||||
last := ""
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
|
||||
stdout, _, _, err := runCommand("systemctl", "is-active", unit)
|
||||
state := strings.TrimSpace(stdout)
|
||||
if err != nil || state == "" {
|
||||
state = "unknown"
|
||||
}
|
||||
if state == last {
|
||||
continue
|
||||
}
|
||||
last = state
|
||||
events.push("unit_state_changed", map[string]string{
|
||||
"unit": unit,
|
||||
"kind": kind,
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func watchSystemdUnitDynamic(ctx context.Context, resolveUnit func() string, kind string, every time.Duration) {
|
||||
lastUnit := ""
|
||||
lastState := ""
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
|
||||
unit := strings.TrimSpace(resolveUnit())
|
||||
state := "unknown"
|
||||
if unit != "" {
|
||||
stdout, _, _, err := runCommand("systemctl", "is-active", unit)
|
||||
s := strings.TrimSpace(stdout)
|
||||
if err == nil && s != "" {
|
||||
state = s
|
||||
}
|
||||
}
|
||||
if unit == lastUnit && state == lastState {
|
||||
continue
|
||||
}
|
||||
lastUnit, lastState = unit, state
|
||||
events.push("unit_state_changed", map[string]string{
|
||||
"unit": unit,
|
||||
"kind": kind,
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// generic file watcher
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func watchFileChange(ctx context.Context, path string, kind string, mode string, every time.Duration) {
|
||||
var lastMod time.Time
|
||||
var lastSize int64 = -1
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(every):
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime() == lastMod && info.Size() == lastSize {
|
||||
continue
|
||||
}
|
||||
lastMod = info.ModTime()
|
||||
lastSize = info.Size()
|
||||
events.push(kind, map[string]any{
|
||||
"path": path,
|
||||
"mode": mode,
|
||||
"size": info.Size(),
|
||||
"mtime": info.ModTime().UTC().Format(time.RFC3339Nano),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user