v 0.0.7 b1 (оптимизация под разнве системы)

This commit is contained in:
DND
2026-06-10 13:20:40 +07:00
parent b8608532ae
commit 28cda73f32
18 changed files with 647 additions and 311 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -275,10 +275,10 @@ func (db *DB) getChannel(id string) (*ChannelConfigDB, error) {
&config.SampleRate, &config.Channels, &config.Format,
&config.AutoRestart, &config.SMBFolder, &config.CreatedAt, &config.UpdatedAt)
if err == sql.ErrNoRows {
switch {
case err == sql.ErrNoRows:
return nil, nil
}
if err != nil {
case err != nil:
return nil, err
}
+12
View File
@@ -275,3 +275,15 @@
{"id":432,"timestamp":"2026-06-10T12:29:50.762449527+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-10 12-29-50.wav"}
{"id":433,"timestamp":"2026-06-10T12:31:09.804898834+07:00","level":"INFO","category":"RECORDING","message":"Запись: ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-10 12-29-50.wav | Длительность: 5м 0с | Размер: 2 MB"}
{"id":434,"timestamp":"2026-06-10T12:31:09.807941618+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-10 12-29-50.wav | Папка: 1-60рпропол-998-2026"}
{"id":435,"timestamp":"2026-06-10T12:37:10.352531773+07:00","level":"INFO","category":"OPENVPN","message":"Остановка OpenVPN"}
{"id":436,"timestamp":"2026-06-10T13:12:47.020233582+07:00","level":"INFO","category":"OPENVPN","message":"Восстановление подключения после перезагрузки"}
{"id":437,"timestamp":"2026-06-10T13:12:47.031657155+07:00","level":"INFO","category":"VPN","message":"VPN Manager инициализирован | VPN по умолчанию: wireguard"}
{"id":438,"timestamp":"2026-06-10T13:12:52.046931708+07:00","level":"INFO","category":"OPENVPN","message":"Запуск OpenVPN | Конфиг: /home/dnd/pfSense-UDP4-1194-DND-config.ovpn"}
{"id":439,"timestamp":"2026-06-10T13:12:52.04969342+07:00","level":"INFO","category":"OPENVPN","message":"OpenVPN запущен, ожидание подключения..."}
{"id":440,"timestamp":"2026-06-10T13:12:52.130010364+07:00","level":"INFO","category":"WIREGUARD","message":"Запуск WireGuard | Интерфейс: wg0"}
{"id":441,"timestamp":"2026-06-10T13:12:52.507874042+07:00","level":"INFO","category":"WIREGUARD","message":"WireGuard запущен успешно"}
{"id":442,"timestamp":"2026-06-10T13:13:02.05878545+07:00","level":"WARNING","category":"OPENVPN","message":"Подключение не установлено, проверьте логи"}
{"id":443,"timestamp":"2026-06-10T13:14:01.404863738+07:00","level":"INFO","category":"RECORDING","message":"Запись: НАЧАЛО | Канал: channel1 | Файл: 2026-06-10 13-14-01.wav"}
{"id":444,"timestamp":"2026-06-10T13:14:24.332423949+07:00","level":"INFO","category":"RECORDING","message":"Запись: ЗАВЕРШЕНА | Канал: channel1 | Файл: 2026-06-10 13-14-01.wav | Длительность: 5м 0с | Размер: 652 KB"}
{"id":445,"timestamp":"2026-06-10T13:14:24.334883655+07:00","level":"INFO","category":"SMB","message":"Добавлен в очередь повторных попыток | Файл: 2026-06-10 13-14-01.wav | Папка: 1-60рпропол-998-2026"}
{"id":446,"timestamp":"2026-06-10T13:17:29.85417718+07:00","level":"INFO","category":"OPENVPN","message":"Остановка OpenVPN"}
+21 -65
View File
@@ -10,22 +10,25 @@ import (
"syscall"
"time"
"golang.org/x/crypto/bcrypt"
"audio-recorder/database"
"audio-recorder/devices"
"audio-recorder/logger"
"audio-recorder/network"
"audio-recorder/optimization"
"audio-recorder/recorder"
"audio-recorder/server"
"audio-recorder/smb"
"audio-recorder/vpn"
"golang.org/x/crypto/bcrypt"
)
//go:embed static
var staticFiles embed.FS
func main() {
// Оптимизация для архитектуры
optimization.OptimizeGoroutines()
// Инициализация логгера
logger.Init()
@@ -38,14 +41,9 @@ func main() {
log.Printf("Warning: Failed to init default settings: %v", err)
}
// Загружаем пользователей из БД (после создания таблиц)
// Загружаем пользователей из БД
loadUsersFromDB()
// Автоматическое монтирование дисков
diskManager := network.GetDiskManager()
if err := diskManager.AutoMountDisks(); err != nil {
log.Printf("Warning: Auto-mount failed: %v", err)
}
// Получаем папку для записей из БД
recordingsDir, err := db.GetSetting("recordings_dir")
if err != nil || recordingsDir == "" {
@@ -93,7 +91,7 @@ func main() {
// Создаем рекордер
rec := recorder.NewRecorder(recordingsDir)
// Проверяем, есть ли каналы в БД, если нет - создаем каналы по умолчанию
// Проверяем, есть ли каналы в БД
channels, err := db.GetAllChannels()
if err != nil {
log.Printf("Warning: Failed to get channels from DB: %v", err)
@@ -102,36 +100,19 @@ func main() {
if len(channels) == 0 {
log.Println("No channels in database, creating default channels...")
// Создаем папки для каналов
os.MkdirAll(filepath.Join(recordingsDir, "channel1"), 0755)
os.MkdirAll(filepath.Join(recordingsDir, "channel2"), 0755)
// Канал 1
channel1 := recorder.NewChannel(
"channel1",
"Канал 1",
"default",
60,
8000,
1,
"economy",
false,
recordingsDir,
"channel1", "Канал 1", "default",
60, 8000, 1, "economy", false, recordingsDir,
)
channel1.SMBFolder = "channel1"
rec.AddChannel(channel1)
// Канал 2
channel2 := recorder.NewChannel(
"channel2",
"Канал 2",
"default",
60,
8000,
1,
"economy",
false,
recordingsDir,
"channel2", "Канал 2", "default",
60, 8000, 1, "economy", false, recordingsDir,
)
channel2.SMBFolder = "channel2"
rec.AddChannel(channel2)
@@ -139,17 +120,12 @@ func main() {
log.Println("Default channels created successfully")
}
// Устанавливаем экземпляр рекордера в обработчики
server.SetRecorder(rec)
// Устанавливаем экземпляры для обратных вызовов
recorder.SetRecorderInstance(rec)
recorder.SetDeviceManager(dm)
// Восстанавливаем состояние каналов после перезагрузки
restoreChannelsState(rec, dm, db)
// Запускаем проверяльщик состояний
stateChecker := recorder.GetStateChecker()
stateChecker.Start()
@@ -174,7 +150,6 @@ func main() {
}
}
// Применяем настройки SMB
smbConfig := smb.SMBConfig{
Enabled: smbEnabledStr == "true",
AutoStart: smbAutoStartStr == "true",
@@ -191,7 +166,6 @@ func main() {
log.Printf("Warning: Failed to update SMB config: %v", err)
}
// Устанавливаем колбэк для отправки файлов на SMB
recorder.SetRecordingCompletedCallback(func(filename string, filePath string, smbFolder string) {
if smbClient.IsConnected() {
log.Printf("SMB client connected, uploading...")
@@ -212,7 +186,6 @@ func main() {
}
})
// Запускаем SMB клиент если включен
if smbEnabledStr == "true" {
log.Printf("Запуск SMB клиента...")
if err := smbClient.Start(); err != nil {
@@ -220,48 +193,31 @@ func main() {
}
}
// Запускаем обработчик очереди повторных отправок
go smbClient.ProcessRetryQueue()
// ========== Инициализация VPN ==========
// Настройка OpenVPN клиента
// Инициализация VPN
openvpn := vpn.GetOpenVPNClient()
openvpn.RestoreState()
// Настройка WireGuard клиента
wireguard := vpn.GetWireGuardClient()
wireguard.LoadFromDB()
// Инициализация VPN Manager
vpnManager := vpn.GetVPNManager()
vpnManager.Init()
// Настраиваем обработку сигналов для graceful shutdown
// Настраиваем обработку сигналов
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
<-signalChan
log.Println("Получен сигнал завершения, сохраняем состояния каналов...")
// Останавливаем проверяльщик состояний
stateChecker.Stop()
// Останавливаем SMB клиент
smbClient.Stop()
// Останавливаем OpenVPN
openvpn.Stop()
// Останавливаем WireGuard
wireguard.Stop()
// Останавливаем все VPN
vpnManager.StopAll()
// Сохраняем состояния всех каналов в БД
rec.SaveAllStates()
log.Println("Завершаем работу...")
os.Exit(0)
}()
@@ -275,18 +231,16 @@ func main() {
log.Printf("📝 Логин: admin, Пароль: admin")
log.Printf("📁 Папка записей: %s", recordingsDir)
log.Printf("🔄 Проверка состояния каналов: каждые %d секунд", stateChecker.GetCheckInterval())
log.Printf("🏗️ Архитектура: %s", optimization.GetArch())
log.Printf("")
// Запускаем сервер с graceful shutdown
if err := srv.Run(); err != nil {
log.Fatal("Server error:", err)
}
}
// restoreChannelsState восстанавливает состояние каналов после перезагрузки
func restoreChannelsState(rec *recorder.Recorder, dm *devices.DeviceManager, db *database.DB) {
channels := rec.GetAllChannels()
log.Printf("=== Восстановление состояния каналов из БД ===")
for id, ch := range channels {
@@ -305,11 +259,15 @@ func restoreChannelsState(rec *recorder.Recorder, dm *devices.DeviceManager, db
return dm.TestDevice(device)
}
// Используем отдельную горутину с копией переменных
channelID := id
channel := ch
go func() {
time.Sleep(2 * time.Second)
// Для ARM добавляем задержку
if optimization.IsARM() {
time.Sleep(3 * time.Second)
} else {
time.Sleep(2 * time.Second)
}
if err := channel.StartRecording(channel.OutputDir, testFunc); err != nil {
log.Printf("Не удалось восстановить запись для канала %s: %v", channelID, err)
db.SetChannelState(channelID, false)
@@ -324,7 +282,6 @@ func restoreChannelsState(rec *recorder.Recorder, dm *devices.DeviceManager, db
log.Printf("=== Восстановление состояний завершено ===")
}
// loadUsersFromDB загружает пользователей из БД или создает admin
func loadUsersFromDB() {
db := database.GetDB()
users, err := db.GetAllUsers()
@@ -332,7 +289,6 @@ func loadUsersFromDB() {
log.Printf("Error loading users from DB: %v", err)
}
// Если нет пользователей в БД, создаем admin
if len(users) == 0 {
hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
if err != nil {
+121
View File
@@ -0,0 +1,121 @@
# Makefile
.PHONY: build-all build-amd64 build-arm64 build-armhf build-386 clean help
APP_NAME = audio-recorder
VERSION = $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME = $(shell date -u '+%Y-%m-%d_%H:%M:%S')
LDFLAGS = -ldflags="-s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)"
# Цвета для вывода
GREEN = \033[0;32m
RED = \033[0;31m
NC = \033[0m
all: build-all
# AMD64 (x86_64) - для обычных ПК и серверов
build-amd64:
@echo "$(GREEN)Building for AMD64 (x86_64)...$(NC)"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-amd64
@echo "$(GREEN)✅ Done: $(APP_NAME)-amd64$(NC)"
@ls -lh $(APP_NAME)-amd64
@file $(APP_NAME)-amd64
# ARM64 (aarch64) - для Raspberry Pi 4/5, Orange Pi Zero 2W, Apple Silicon
build-arm64:
@echo "$(GREEN)Building for ARM64 (aarch64)...$(NC)"
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-arm64
@echo "$(GREEN)✅ Done: $(APP_NAME)-arm64$(NC)"
@ls -lh $(APP_NAME)-arm64
@file $(APP_NAME)-arm64
# ARMHF (armv7l) - для старых Raspberry Pi 2/3, Orange Pi PC
build-armhf:
@echo "$(GREEN)Building for ARMHF (armv7l)...$(NC)"
GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-armhf
@echo "$(GREEN)✅ Done: $(APP_NAME)-armhf$(NC)"
@ls -lh $(APP_NAME)-armhf
@file $(APP_NAME)-armhf
# ARMv6 (armv6l) - для Raspberry Pi Zero, Raspberry Pi 1
build-armv6:
@echo "$(GREEN)Building for ARMv6 (armv6l)...$(NC)"
GOOS=linux GOARCH=arm GOARM=6 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-armv6
@echo "$(GREEN)✅ Done: $(APP_NAME)-armv6$(NC)"
@ls -lh $(APP_NAME)-armv6
# 32-bit x86 - для старых 32-битных систем
build-386:
@echo "$(GREEN)Building for 386 (32-bit x86)...$(NC)"
GOOS=linux GOARCH=386 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-386
@echo "$(GREEN)✅ Done: $(APP_NAME)-386$(NC)"
@ls -lh $(APP_NAME)-386
# Windows AMD64
build-windows:
@echo "$(GREEN)Building for Windows AMD64...$(NC)"
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-windows-amd64.exe
@echo "$(GREEN)✅ Done: $(APP_NAME)-windows-amd64.exe$(NC)"
@ls -lh $(APP_NAME)-windows-amd64.exe
# macOS AMD64 (Intel)
build-darwin-amd64:
@echo "$(GREEN)Building for macOS AMD64 (Intel)...$(NC)"
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-darwin-amd64
@echo "$(GREEN)✅ Done: $(APP_NAME)-darwin-amd64$(NC)"
@ls -lh $(APP_NAME)-darwin-amd64
# macOS ARM64 (Apple Silicon M1/M2)
build-darwin-arm64:
@echo "$(GREEN)Building for macOS ARM64 (Apple Silicon)...$(NC)"
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build $(LDFLAGS) -o $(APP_NAME)-darwin-arm64
@echo "$(GREEN)✅ Done: $(APP_NAME)-darwin-arm64$(NC)"
@ls -lh $(APP_NAME)-darwin-arm64
# Сборка всех Linux архитектур
build-linux: build-amd64 build-arm64 build-armhf build-armv6 build-386
@echo "$(GREEN)✅ All Linux builds complete!$(NC)"
# Сборка всех архитектур
build-all: build-linux build-windows build-darwin-amd64 build-darwin-arm64
@echo "$(GREEN)✅ All builds complete!$(NC)"
# Очистка
clean:
@echo "$(GREEN)Cleaning...$(NC)"
rm -f $(APP_NAME)-*
@echo "$(GREEN)✅ Clean complete$(NC)"
# Упаковка в архив для каждой архитектуры
package:
@echo "$(GREEN)Creating packages...$(NC)"
mkdir -p packages
for arch in amd64 arm64 armhf armv6 386; do \
if [ -f "$(APP_NAME)-$$arch" ]; then \
mkdir -p packages/$(APP_NAME)-$$arch; \
cp $(APP_NAME)-$$arch packages/$(APP_NAME)-$$arch/; \
cp -r static packages/$(APP_NAME)-$$arch/; \
cd packages && tar -czf $(APP_NAME)-$$arch.tar.gz $(APP_NAME)-$$arch && cd ..; \
rm -rf packages/$(APP_NAME)-$$arch; \
echo "$(GREEN)✅ Created $(APP_NAME)-$$arch.tar.gz$(NC)"; \
fi \
done
rm -rf packages
@echo "$(GREEN)✅ Packaging complete!$(NC)"
# Помощь
help:
@echo "Available targets:"
@echo " make build-amd64 - Build for Linux AMD64 (x86_64)"
@echo " make build-arm64 - Build for Linux ARM64 (aarch64)"
@echo " make build-armhf - Build for Linux ARMHF (armv7l)"
@echo " make build-armv6 - Build for Linux ARMv6 (Raspberry Pi Zero)"
@echo " make build-386 - Build for Linux 386 (32-bit x86)"
@echo " make build-windows - Build for Windows AMD64"
@echo " make build-darwin-amd64 - Build for macOS Intel"
@echo " make build-darwin-arm64 - Build for macOS Apple Silicon"
@echo " make build-linux - Build all Linux architectures"
@echo " make build-all - Build all architectures"
@echo " make clean - Remove all binaries"
@echo " make package - Create tar.gz packages"
@echo " make help - Show this help"
+58 -33
View File
@@ -1,4 +1,3 @@
// network/disks.go
package network
import (
@@ -9,8 +8,10 @@ import (
"os/exec"
"strings"
"sync"
"time"
"audio-recorder/database"
"audio-recorder/optimization"
)
type DiskManager struct {
@@ -20,7 +21,7 @@ type DiskManager struct {
type DiskInfo struct {
Device string `json:"device"`
Size string `json:"size"`
Type string `json:"type"` // disk, part
Type string `json:"type"`
Filesystem string `json:"filesystem"`
MountPoint string `json:"mount_point"`
Free string `json:"free"`
@@ -43,18 +44,27 @@ func GetDiskManager() *DiskManager {
return diskInstance
}
// GetDisks возвращает список всех дисков
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func (dm *DiskManager) GetDisks() ([]DiskInfo, error) {
dm.mu.Lock()
defer dm.mu.Unlock()
var disks []DiskInfo
// Получаем список дисков через lsblk в текстовом режиме
if !commandExists("lsblk") {
log.Printf("lsblk not found, skipping disk detection")
return disks, nil
}
textCmd := exec.Command("lsblk", "-o", "NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,LABEL", "-n")
textOutput, err := textCmd.Output()
if err != nil {
return nil, err
log.Printf("Error running lsblk: %v", err)
return disks, nil
}
scanner := bufio.NewScanner(strings.NewReader(string(textOutput)))
@@ -87,13 +97,11 @@ func (dm *DiskManager) GetDisks() ([]DiskInfo, error) {
disk.Label = fields[5]
}
// Получаем свободное место если смонтирован
if disk.MountPoint != "" {
if disk.MountPoint != "" && commandExists("df") {
free := dm.getFreeSpace(disk.MountPoint)
disk.Free = free
}
// Пропускаем системные диски
if strings.HasPrefix(name, "loop") || name == "sr0" {
continue
}
@@ -105,6 +113,10 @@ func (dm *DiskManager) GetDisks() ([]DiskInfo, error) {
}
func (dm *DiskManager) getFreeSpace(mountPoint string) string {
if !commandExists("df") {
return "unknown"
}
cmd := exec.Command("df", "-h", mountPoint)
output, err := cmd.Output()
if err != nil {
@@ -121,9 +133,7 @@ func (dm *DiskManager) getFreeSpace(mountPoint string) string {
return "unknown"
}
// SaveMountConfig сохраняет настройки монтирования в БД
func (dm *DiskManager) SaveMountConfig(device, mountPoint string, enabled bool) error {
// Сохраняем как JSON
key := "disk_mount_" + strings.ReplaceAll(device, "/", "_")
value := fmt.Sprintf(`{"device":"%s","mount_point":"%s","enabled":%v}`, device, mountPoint, enabled)
@@ -131,11 +141,9 @@ func (dm *DiskManager) SaveMountConfig(device, mountPoint string, enabled bool)
return db.SetSetting(key, value)
}
// GetAllMountConfigsFromDB получает все настройки монтирования из БД
func (dm *DiskManager) GetAllMountConfigsFromDB() ([]MountConfig, error) {
db := database.GetDB()
// Получаем все settings
settings, err := db.GetAllSettings()
if err != nil {
return nil, err
@@ -145,7 +153,6 @@ func (dm *DiskManager) GetAllMountConfigsFromDB() ([]MountConfig, error) {
for key, value := range settings {
if strings.HasPrefix(key, "disk_mount_") {
var config MountConfig
// Парсим JSON
if err := parseMountConfig(value, &config); err == nil {
configs = append(configs, config)
}
@@ -156,12 +163,9 @@ func (dm *DiskManager) GetAllMountConfigsFromDB() ([]MountConfig, error) {
}
func parseMountConfig(value string, config *MountConfig) error {
// Простой парсинг JSON (можно использовать json.Unmarshal)
// Для простоты используем ручной парсинг
var device, mountPoint string
var enabled bool
// Удаляем фигурные скобки
value = strings.Trim(value, "{}")
parts := strings.Split(value, ",")
for _, part := range parts {
@@ -188,17 +192,20 @@ func parseMountConfig(value string, config *MountConfig) error {
return nil
}
// DeleteMountConfig удаляет настройку монтирования
func (dm *DiskManager) DeleteMountConfig(device string) error {
db := database.GetDB()
key := "disk_mount_" + strings.ReplaceAll(device, "/", "_")
return db.SetSetting(key, "")
}
// AutoMountDisks автоматически монтирует диски по сохраненным настройкам
func (dm *DiskManager) AutoMountDisks() error {
log.Println("Auto-mounting disks from saved configs...")
if !commandExists("mountpoint") || !commandExists("mount") {
log.Printf("Mount commands not available, skipping auto-mount")
return nil
}
configs, err := dm.GetAllMountConfigsFromDB()
if err != nil {
log.Printf("Error getting mount configs: %v", err)
@@ -210,50 +217,57 @@ func (dm *DiskManager) AutoMountDisks() error {
continue
}
// Проверяем, не смонтирован ли уже
checkCmd := exec.Command("mountpoint", "-q", config.MountPoint)
if checkCmd.Run() == nil {
log.Printf("Already mounted: %s at %s", config.Device, config.MountPoint)
continue
}
// Проверяем, существует ли устройство
if _, err := os.Stat(config.Device); os.IsNotExist(err) {
log.Printf("Device %s not found, skipping", config.Device)
continue
}
// Монтируем
log.Printf("Auto-mounting %s to %s", config.Device, config.MountPoint)
if err := dm.MountDisk(config.Device, config.MountPoint); err != nil {
log.Printf("Failed to auto-mount %s: %v", config.Device, err)
} else {
log.Printf("Successfully auto-mounted %s to %s", config.Device, config.MountPoint)
}
// Для ARM добавляем задержку между монтированиями
if optimization.IsARM() {
time.Sleep(1 * time.Second)
}
}
return nil
}
// MountDisk монтирует диск и сохраняет настройку
func (dm *DiskManager) MountDisk(device, mountPoint string) error {
log.Printf("Mounting %s to %s", device, mountPoint)
// Создаем точку монтирования если не существует
if err := os.MkdirAll(mountPoint, 0755); err != nil {
return fmt.Errorf("failed to create mount point: %v", err)
}
// Проверяем, не смонтирован ли уже
checkCmd := exec.Command("mountpoint", "-q", mountPoint)
if checkCmd.Run() == nil {
return fmt.Errorf("already mounted at %s", mountPoint)
}
// Определяем тип файловой системы для правильных параметров монтирования
fstype := "auto"
// Монтируем
if commandExists("ntfs-3g") {
checkFsCmd := exec.Command("blkid", "-o", "value", "-s", "TYPE", device)
if fsOutput, err := checkFsCmd.Output(); err == nil {
fsType := strings.TrimSpace(string(fsOutput))
if fsType == "ntfs" {
fstype = "ntfs-3g"
}
}
}
cmd := exec.Command("sudo", "mount", "-t", fstype, device, mountPoint)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("mount failed: %v, output: %s", err, output)
@@ -263,7 +277,6 @@ func (dm *DiskManager) MountDisk(device, mountPoint string) error {
return nil
}
// MountDiskWithSave монтирует диск и сохраняет настройку
func (dm *DiskManager) MountDiskWithSave(device, mountPoint string, saveConfig bool) error {
if err := dm.MountDisk(device, mountPoint); err != nil {
return err
@@ -278,7 +291,6 @@ func (dm *DiskManager) MountDiskWithSave(device, mountPoint string, saveConfig b
return nil
}
// UnmountDisk отмонтирует диск и удаляет настройку
func (dm *DiskManager) UnmountDisk(mountPoint string, removeConfig bool, device string) error {
log.Printf("Unmounting %s", mountPoint)
@@ -298,11 +310,27 @@ func (dm *DiskManager) UnmountDisk(mountPoint string, removeConfig bool, device
return nil
}
// FormatDisk форматирует диск
func (dm *DiskManager) FormatDisk(device, filesystem, label string) error {
log.Printf("Formatting %s with %s, label: %s", device, filesystem, label)
// Проверяем, не смонтирован ли диск
var formatCmd string
switch filesystem {
case "ext4":
formatCmd = "mkfs.ext4"
case "ntfs":
formatCmd = "mkfs.ntfs"
case "fat32":
formatCmd = "mkfs.vfat"
case "exfat":
formatCmd = "mkfs.exfat"
default:
return fmt.Errorf("unsupported filesystem: %s", filesystem)
}
if !commandExists(formatCmd) {
return fmt.Errorf("format command %s not available", formatCmd)
}
checkCmd := exec.Command("findmnt", "-n", device)
if checkCmd.Run() == nil {
return fmt.Errorf("device is mounted, please unmount first")
@@ -318,8 +346,6 @@ func (dm *DiskManager) FormatDisk(device, filesystem, label string) error {
cmd = exec.Command("sudo", "mkfs.vfat", "-F", "32", "-n", label, device)
case "exfat":
cmd = exec.Command("sudo", "mkfs.exfat", "-n", label, device)
default:
return fmt.Errorf("unsupported filesystem: %s", filesystem)
}
if output, err := cmd.CombinedOutput(); err != nil {
@@ -330,7 +356,6 @@ func (dm *DiskManager) FormatDisk(device, filesystem, label string) error {
return nil
}
// GetMountableDevices возвращает список немонтированных устройств
func (dm *DiskManager) GetMountableDevices() ([]DiskInfo, error) {
disks, err := dm.GetDisks()
if err != nil {
+30 -52
View File
@@ -2,20 +2,22 @@ package network
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/exec"
"strings"
"sync"
"time"
"audio-recorder/optimization"
)
// NetworkManager - менеджер сетевых интерфейсов
type NetworkManager struct {
mu sync.RWMutex
}
// NetworkInterface - информация о сетевом интерфейсе
type NetworkInterface struct {
Name string `json:"name"`
Status string `json:"status"`
@@ -28,16 +30,14 @@ type NetworkInterface struct {
Type string `json:"type"`
}
// IPConfig - конфигурация IP
type IPConfig struct {
Mode string `json:"mode"` // dhcp/static
Mode string `json:"mode"`
IP string `json:"ip"`
Netmask string `json:"netmask"`
Gateway string `json:"gateway"`
DNS string `json:"dns"`
}
// WiFiNetwork - информация о WiFi сети
type WiFiNetwork struct {
SSID string `json:"ssid"`
Strength int `json:"strength"`
@@ -47,7 +47,6 @@ type WiFiNetwork struct {
var instance *NetworkManager
var once sync.Once
// GetManager возвращает экземпляр менеджера сетей
func GetManager() *NetworkManager {
once.Do(func() {
instance = &NetworkManager{}
@@ -55,7 +54,6 @@ func GetManager() *NetworkManager {
return instance
}
// GetInterfaces возвращает список всех сетевых интерфейсов
func (nm *NetworkManager) GetInterfaces() ([]NetworkInterface, error) {
nm.mu.Lock()
defer nm.mu.Unlock()
@@ -88,7 +86,6 @@ func (nm *NetworkManager) GetInterfaces() ([]NetworkInterface, error) {
Status: status,
}
// Определяем тип интерфейса
if strings.HasPrefix(name, "wl") || strings.HasPrefix(name, "wlan") {
iface.Type = "wifi"
} else if strings.HasPrefix(name, "en") || strings.HasPrefix(name, "eth") {
@@ -97,13 +94,11 @@ func (nm *NetworkManager) GetInterfaces() ([]NetworkInterface, error) {
iface.Type = "other"
}
// Получаем MAC адрес
macCmd := exec.Command("cat", "/sys/class/net/"+name+"/address")
if macOutput, err := macCmd.Output(); err == nil {
iface.MAC = strings.TrimSpace(string(macOutput))
}
// Получаем IP адрес
ipCmd := exec.Command("ip", "-4", "addr", "show", name)
ipOutput, _ := ipCmd.Output()
lines := strings.Split(string(ipOutput), "\n")
@@ -120,7 +115,6 @@ func (nm *NetworkManager) GetInterfaces() ([]NetworkInterface, error) {
}
}
// Получаем шлюз
routeCmd := exec.Command("ip", "route", "show", "default", "dev", name)
if routeOutput, err := routeCmd.Output(); err == nil {
routeParts := strings.Fields(string(routeOutput))
@@ -132,7 +126,6 @@ func (nm *NetworkManager) GetInterfaces() ([]NetworkInterface, error) {
}
}
// Получаем DNS
if dns, err := nm.getDNS(); err == nil {
iface.DNS = dns
}
@@ -143,7 +136,6 @@ func (nm *NetworkManager) GetInterfaces() ([]NetworkInterface, error) {
return interfaces, nil
}
// getDNS получает DNS серверы из resolv.conf
func (nm *NetworkManager) getDNS() (string, error) {
file, err := os.Open("/etc/resolv.conf")
if err != nil {
@@ -166,7 +158,6 @@ func (nm *NetworkManager) getDNS() (string, error) {
return strings.Join(dnsServers, ", "), nil
}
// GetIPConfig получает текущую конфигурацию IP для интерфейса
func (nm *NetworkManager) GetIPConfig(interfaceName string) (*IPConfig, error) {
config := &IPConfig{Mode: "dhcp"}
@@ -176,7 +167,6 @@ func (nm *NetworkManager) GetIPConfig(interfaceName string) (*IPConfig, error) {
return nil, err
}
// Проверяем, есть ли указание на dynamic (DHCP)
if strings.Contains(string(ipOutput), "dynamic") {
config.Mode = "dhcp"
} else {
@@ -193,7 +183,6 @@ func (nm *NetworkManager) GetIPConfig(interfaceName string) (*IPConfig, error) {
}
}
// Получаем шлюз
routeCmd := exec.Command("ip", "route", "show", "default", "dev", interfaceName)
if routeOutput, err := routeCmd.Output(); err == nil {
routeParts := strings.Fields(string(routeOutput))
@@ -205,7 +194,6 @@ func (nm *NetworkManager) GetIPConfig(interfaceName string) (*IPConfig, error) {
}
}
// Получаем DNS
if dns, err := nm.getDNS(); err == nil {
config.DNS = dns
}
@@ -213,15 +201,17 @@ func (nm *NetworkManager) GetIPConfig(interfaceName string) (*IPConfig, error) {
return config, nil
}
// SetDHCP устанавливает DHCP для интерфейса
func (nm *NetworkManager) SetDHCP(interfaceName string) error {
log.Printf("Setting DHCP for interface %s", interfaceName)
// Убиваем старые dhclient процессы
exec.Command("sudo", "pkill", "-f", "dhclient.*"+interfaceName).Run()
exec.Command("sudo", "dhclient", "-r", interfaceName).Run()
// Запускаем dhclient
// Для ARM добавляем задержку
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
cmd := exec.Command("sudo", "dhclient", "-v", interfaceName)
if output, err := cmd.CombinedOutput(); err != nil {
log.Printf("DHCP failed: %v, output: %s", err, output)
@@ -232,14 +222,11 @@ func (nm *NetworkManager) SetDHCP(interfaceName string) error {
return nil
}
// SetStaticIP устанавливает статический IP для интерфейса
func (nm *NetworkManager) SetStaticIP(interfaceName, ip, netmask, gateway, dns string) error {
log.Printf("Setting static IP for interface %s: %s/%s, gateway %s", interfaceName, ip, netmask, gateway)
// Очищаем текущие IP адреса
exec.Command("sudo", "ip", "addr", "flush", "dev", interfaceName).Run()
// Добавляем новый IP
cidr := netmask
if cidr == "" {
cidr = "24"
@@ -251,13 +238,9 @@ func (nm *NetworkManager) SetStaticIP(interfaceName, ip, netmask, gateway, dns s
return err
}
// Поднимаем интерфейс
exec.Command("sudo", "ip", "link", "set", interfaceName, "up").Run()
// Удаляем старый шлюз
exec.Command("sudo", "ip", "route", "del", "default", "dev", interfaceName).Run()
// Добавляем новый шлюз
if gateway != "" {
routeCmd := exec.Command("sudo", "ip", "route", "add", "default", "via", gateway, "dev", interfaceName)
if output, err := routeCmd.CombinedOutput(); err != nil {
@@ -267,7 +250,6 @@ func (nm *NetworkManager) SetStaticIP(interfaceName, ip, netmask, gateway, dns s
}
}
// Настраиваем DNS
if dns != "" {
if err := nm.setDNS(dns); err != nil {
log.Printf("Warning: failed to set DNS: %v", err)
@@ -278,7 +260,6 @@ func (nm *NetworkManager) SetStaticIP(interfaceName, ip, netmask, gateway, dns s
return nil
}
// setDNS устанавливает DNS серверы
func (nm *NetworkManager) setDNS(dnsServers string) error {
servers := strings.Split(dnsServers, ",")
@@ -299,34 +280,42 @@ func (nm *NetworkManager) setDNS(dnsServers string) error {
return cmd.Run()
}
// RenewDHCP обновляет DHCP аренду
func (nm *NetworkManager) RenewDHCP(interfaceName string) error {
log.Printf("Renewing DHCP for interface %s", interfaceName)
exec.Command("sudo", "dhclient", "-r", interfaceName).Run()
// Для ARM добавляем задержку
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
return nm.SetDHCP(interfaceName)
}
// SetInterfaceUp поднимает интерфейс
func (nm *NetworkManager) SetInterfaceUp(name string) error {
cmd := exec.Command("sudo", "ip", "link", "set", name, "up")
return cmd.Run()
}
// SetInterfaceDown опускает интерфейс
func (nm *NetworkManager) SetInterfaceDown(name string) error {
cmd := exec.Command("sudo", "ip", "link", "set", name, "down")
return cmd.Run()
}
// ScanWiFiNetworks сканирует доступные WiFi сети
func (nm *NetworkManager) ScanWiFiNetworks(interfaceName string) ([]WiFiNetwork, error) {
// Поднимаем интерфейс
nm.SetInterfaceUp(interfaceName)
// Сканируем
cmd := exec.Command("sudo", "iwlist", interfaceName, "scan")
// Для ARM используем более короткий таймаут
timeout := 30 * time.Second
if optimization.IsARM() {
timeout = 15 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "iwlist", interfaceName, "scan")
output, err := cmd.Output()
if err != nil {
log.Printf("Error scanning WiFi: %v", err)
@@ -358,7 +347,6 @@ func (nm *NetworkManager) ScanWiFiNetworks(interfaceName string) ([]WiFiNetwork,
if idx2 := strings.Index(qualityPart, "/"); idx2 != -1 {
var quality int
fmt.Sscanf(qualityPart, "%d", &quality)
// Нормализуем до 100% (максимальное значение обычно 70)
currentNetwork.Strength = quality * 100 / 70
if currentNetwork.Strength > 100 {
currentNetwork.Strength = 100
@@ -381,16 +369,13 @@ func (nm *NetworkManager) ScanWiFiNetworks(interfaceName string) ([]WiFiNetwork,
return networks, nil
}
// ConnectToWiFi подключается к WiFi сети
func (nm *NetworkManager) ConnectToWiFi(interfaceName, ssid, password, country string) error {
log.Printf("Connecting to WiFi: %s on %s", ssid, interfaceName)
// Устанавливаем страну
if country != "" {
exec.Command("sudo", "iw", "reg", "set", country).Run()
}
// Создаем конфиг для wpa_supplicant
configPath := "/tmp/wpa_supplicant.conf"
var configContent string
@@ -413,44 +398,38 @@ network={
return err
}
// Убиваем старые процессы wpa_supplicant
exec.Command("sudo", "killall", "wpa_supplicant").Run()
// Запускаем wpa_supplicant
// Для ARM добавляем задержку
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
cmd := exec.Command("sudo", "wpa_supplicant", "-B", "-i", interfaceName, "-c", configPath)
if err := cmd.Run(); err != nil {
return err
}
// Получаем IP через DHCP
exec.Command("sudo", "dhclient", "-r", interfaceName).Run()
exec.Command("sudo", "dhclient", interfaceName).Run()
// Поднимаем интерфейс
nm.SetInterfaceUp(interfaceName)
log.Printf("Connected to WiFi: %s", ssid)
return nil
}
// DisconnectWiFi отключает WiFi
func (nm *NetworkManager) DisconnectWiFi(interfaceName string) error {
log.Printf("Disconnecting WiFi from %s", interfaceName)
// Убиваем wpa_supplicant
exec.Command("sudo", "killall", "wpa_supplicant").Run()
// Очищаем IP
exec.Command("sudo", "ip", "addr", "flush", "dev", interfaceName).Run()
// Опускаем интерфейс
nm.SetInterfaceDown(interfaceName)
log.Printf("WiFi disconnected from %s", interfaceName)
return nil
}
// GetWiFiStatus возвращает статус WiFi подключения
func (nm *NetworkManager) GetWiFiStatus(interfaceName string) (bool, string) {
cmd := exec.Command("iwconfig", interfaceName)
output, err := cmd.Output()
@@ -462,7 +441,6 @@ func (nm *NetworkManager) GetWiFiStatus(interfaceName string) (bool, string) {
return false, ""
}
// Извлекаем ESSID
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "ESSID:") && !strings.Contains(line, "off/any") {
+136
View File
@@ -0,0 +1,136 @@
package optimization
import (
"log"
"runtime"
"sync"
"time"
)
var (
arch string
isARM bool
isARM64 bool
isARMHF bool
isAMD64 bool
optimizeOnce sync.Once
)
func init() {
detectArchitecture()
}
func detectArchitecture() {
arch = runtime.GOARCH
isAMD64 = arch == "amd64"
isARM64 = arch == "arm64"
isARMHF = arch == "arm" || arch == "armv7l" || arch == "armv6l"
isARM = isARM64 || isARMHF
log.Printf("Architecture detected: %s", arch)
log.Printf("Optimizations: ARM=%v, ARM64=%v, ARMHF=%v, AMD64=%v", isARM, isARM64, isARMHF, isAMD64)
}
// GetArch возвращает текущую архитектуру
func GetArch() string {
return arch
}
// IsARM возвращает true для ARM архитектур
func IsARM() bool {
return isARM
}
// IsARM64 возвращает true для ARM64
func IsARM64() bool {
return isARM64
}
// IsARMHF возвращает true для ARMHF (32-bit ARM)
func IsARMHF() bool {
return isARMHF
}
// IsAMD64 возвращает true для AMD64
func IsAMD64() bool {
return isAMD64
}
// GetBufferSize возвращает оптимальный размер буфера для архитектуры
func GetBufferSize(defaultSize, armSize, arm64Size, armhfSize int) int {
switch {
case IsARM64():
return arm64Size
case IsARMHF():
return armhfSize
case IsARM():
return armSize
default:
return defaultSize
}
}
// GetTimeout возвращает оптимизированный таймаут
func GetTimeout(defaultTimeout, armTimeout time.Duration) time.Duration {
if IsARM() {
return armTimeout
}
return defaultTimeout
}
// GetInterval возвращает оптимизированный интервал
func GetInterval(defaultInterval, armInterval time.Duration) time.Duration {
if IsARM() {
return armInterval
}
return defaultInterval
}
// ShouldParallel возвращает true если можно использовать параллельное выполнение
func ShouldParallel() bool {
return !IsARM() // Только на AMD64 используем параллелизацию
}
// GetCPULimit возвращает оптимальное количество CPU для архитектуры
func GetCPULimit() int {
switch {
case IsARM64():
return 2 // Orange Pi Zero 2W - ограничиваем для стабильности
case IsARMHF():
return 1 // Старые ARM устройства
default:
return runtime.NumCPU()
}
}
// GetLogLevel возвращает уровень логирования
func GetLogLevel() string {
if IsARM() {
return "WARNING" // Меньше логов на ARM
}
return "INFO"
}
// GetWorkerCount возвращает количество воркеров для архитектуры
func GetWorkerCount(defaultCount, armCount int) int {
if IsARM() {
return armCount
}
return defaultCount
}
// SleepForARM добавляет задержку для ARM устройств
func SleepForARM(duration time.Duration) {
if IsARM() {
time.Sleep(duration)
}
}
// OptimizeGoroutines устанавливает оптимальное количество горутин
func OptimizeGoroutines() {
if IsARM() {
// Ограничиваем количество горутин на ARM
runtime.GOMAXPROCS(GetCPULimit())
log.Printf("GOMAXPROCS set to %d for ARM optimization", GetCPULimit())
}
}
+41 -12
View File
@@ -15,6 +15,7 @@ import (
"audio-recorder/database"
"audio-recorder/logger"
"audio-recorder/optimization"
)
type Channel struct {
@@ -62,9 +63,34 @@ type ChannelConfig struct {
SMBFolder string `json:"smb_folder"`
}
// Константы для разных архитектур
const (
bufferSizeAMD64 = 4096
bufferSizeARM64 = 2048
bufferSizeARMHF = 1024
)
var recorderInstance *Recorder
var deviceManager interface{}
type RecordingCompletedCallback func(filename string, filePath string, smbFolder string)
var onRecordingCompleted RecordingCompletedCallback
func getOptimalBufferSize() int {
return optimization.GetBufferSize(bufferSizeAMD64, bufferSizeARM64, bufferSizeARM64, bufferSizeARMHF)
}
func getOptimalAudioBufferSize(sampleRate, channels int) int {
size := sampleRate * channels * 2 * 2
if optimization.IsARM() {
if size > 32768 {
size = 32768
}
}
return size
}
func SetRecorderInstance(rec *Recorder) {
recorderInstance = rec
}
@@ -81,10 +107,6 @@ func GetDeviceManager() interface{} {
return deviceManager
}
type RecordingCompletedCallback func(filename string, filePath string, smbFolder string)
var onRecordingCompleted RecordingCompletedCallback
func SetRecordingCompletedCallback(callback RecordingCompletedCallback) {
onRecordingCompleted = callback
}
@@ -93,6 +115,8 @@ func NewChannel(id, name, device string, duration, sampleRate, channels int, for
log.Printf("NewChannel: id=%s, name=%s, device=%s, duration=%d, sampleRate=%d, channels=%d, format=%s, autoRestart=%v",
id, name, device, duration, sampleRate, channels, format, autoRestart)
audioDataSize := getOptimalAudioBufferSize(sampleRate, channels)
return &Channel{
ID: id,
Name: name,
@@ -105,7 +129,7 @@ func NewChannel(id, name, device string, duration, sampleRate, channels int, for
SMBFolder: "",
CreatedAt: time.Now(),
OutputDir: outputDir,
audioData: make([]byte, 0, sampleRate*channels*2),
audioData: make([]byte, 0, audioDataSize),
stopChan: make(chan struct{}),
ScheduleEnabled: false,
}
@@ -203,8 +227,11 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
ch.cmd = cmd
// Оптимизированный буфер для архитектуры
bufferSize := getOptimalBufferSize()
go func() {
buf := make([]byte, 1024)
buf := make([]byte, 256)
for {
select {
case <-ch.ctx.Done():
@@ -223,9 +250,9 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
}
}()
ch.audioData = make([]byte, 0, ch.SampleRate*ch.Channels*2*2)
ch.audioData = make([]byte, 0, getOptimalAudioBufferSize(ch.SampleRate, ch.Channels))
go func() {
buffer := make([]byte, 4096)
buffer := make([]byte, bufferSize)
for {
select {
case <-ch.ctx.Done():
@@ -237,7 +264,7 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
if n > 0 {
ch.mu.Lock()
ch.audioData = append(ch.audioData, buffer[:n]...)
maxBufferSize := ch.SampleRate * ch.Channels * 2 * 2
maxBufferSize := getOptimalAudioBufferSize(ch.SampleRate, ch.Channels)
if len(ch.audioData) > maxBufferSize {
ch.audioData = ch.audioData[len(ch.audioData)-maxBufferSize:]
}
@@ -310,7 +337,6 @@ func (ch *Channel) StopRecording() {
ch.stopOnce.Do(func() {
log.Printf("=== STOPPING RECORDING for channel %s ===", ch.ID)
// Сохраняем информацию о файле до остановки
currentFile := ch.CurrentFile
currentPath := ""
smbFolder := ch.SMBFolder
@@ -391,7 +417,6 @@ func (ch *Channel) StopRecording() {
ch.saveStateToDB()
// Проверяем существует ли файл
fileExists := false
if currentPath != "" {
if _, err := os.Stat(currentPath); err == nil {
@@ -402,7 +427,6 @@ func (ch *Channel) StopRecording() {
log.Printf("DEBUG: fileExists=%v, currentPath=%s, smbFolder=%s, onRecordingCompleted=%v",
fileExists, currentPath, smbFolder, onRecordingCompleted != nil)
// Отправляем файл на SMB если есть файл и папка SMB указана
if fileExists && currentPath != "" && smbFolder != "" && onRecordingCompleted != nil {
log.Printf("✅ Sending file to SMB: %s, folder: %s, path: %s", currentFile, smbFolder, currentPath)
@@ -591,6 +615,11 @@ func (ch *Channel) StartSchedule() {
ch.ID, targetStart.Format("2006-01-02 15:04:05"), startDuration,
targetEnd.Format("2006-01-02 15:04:05"), endDuration)
// Для ARM добавляем небольшую задержку перед стартом
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
time.AfterFunc(startDuration, func() {
log.Printf("Schedule: Starting recording for channel %s", ch.ID)
db := database.GetDB()
+49 -8
View File
@@ -8,6 +8,7 @@ import (
"time"
"audio-recorder/database"
"audio-recorder/optimization"
)
type Recorder struct {
@@ -21,6 +22,11 @@ func NewRecorder(outputDir string) *Recorder {
channels: make(map[string]*Channel),
OutputDir: outputDir,
}
if optimization.IsARM() {
log.Println("ARM mode: using optimized channel loading")
}
r.loadChannelsFromDB()
return r
}
@@ -38,6 +44,12 @@ func (r *Recorder) loadChannelsFromDB() {
return
}
// Для ARM добавляем задержку между загрузкой каналов
delay := 0
if optimization.IsARM() {
delay = 100 // миллисекунд
}
for _, chConfig := range channels {
channel := NewChannel(
chConfig.ID, chConfig.Name, chConfig.Device,
@@ -46,7 +58,6 @@ func (r *Recorder) loadChannelsFromDB() {
)
channel.SMBFolder = chConfig.SMBFolder
// Загружаем расписание
schedule, _ := db.GetSchedule(chConfig.ID)
if schedule != nil {
channel.ScheduleEnabled = schedule.Enabled
@@ -57,9 +68,12 @@ func (r *Recorder) loadChannelsFromDB() {
channel.RepeatDaily = schedule.RepeatDaily
if schedule.Enabled {
// Добавляем задержку перед стартом расписания для полной инициализации
go func(ch *Channel) {
time.Sleep(2 * time.Second)
sleepTime := 2 * time.Second
if optimization.IsARM() {
sleepTime = 3 * time.Second
}
time.Sleep(sleepTime)
ch.StartSchedule()
}(channel)
}
@@ -68,9 +82,14 @@ func (r *Recorder) loadChannelsFromDB() {
r.channels[channel.ID] = channel
os.MkdirAll(filepath.Join(recordingsDir, channel.ID), 0755)
log.Printf("Loaded channel %s from database", chConfig.ID)
if delay > 0 {
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
}
// AddChannel добавляет новый канал
func (r *Recorder) AddChannel(channel *Channel) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -105,6 +124,7 @@ func (r *Recorder) AddChannel(channel *Channel) {
db.SaveChannel(config)
}
// GetChannel возвращает канал по ID
func (r *Recorder) GetChannel(id string) (*Channel, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -112,6 +132,7 @@ func (r *Recorder) GetChannel(id string) (*Channel, bool) {
return ch, exists
}
// GetAllChannels возвращает все каналы
func (r *Recorder) GetAllChannels() map[string]*Channel {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -122,6 +143,7 @@ func (r *Recorder) GetAllChannels() map[string]*Channel {
return result
}
// DeleteChannel удаляет канал
func (r *Recorder) DeleteChannel(id string) bool {
r.mu.Lock()
defer r.mu.Unlock()
@@ -140,6 +162,7 @@ func (r *Recorder) DeleteChannel(id string) bool {
return false
}
// UpdateChannel обновляет конфигурацию канала
func (r *Recorder) UpdateChannel(id string, config ChannelConfig) bool {
r.mu.Lock()
defer r.mu.Unlock()
@@ -166,6 +189,7 @@ func (r *Recorder) UpdateChannel(id string, config ChannelConfig) bool {
return false
}
// GetChannelsList возвращает список каналов для API
func (r *Recorder) GetChannelsList() []map[string]interface{} {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -204,12 +228,29 @@ func (r *Recorder) SaveAllStates() {
log.Printf("=== Сохранение состояний всех каналов ===")
for id, ch := range r.channels {
if err := db.SetChannelState(id, ch.IsRecording); err != nil {
log.Printf("Failed to save state for channel %s: %v", id, err)
} else {
log.Printf("✅ Saved state for channel %s: %v", id, ch.IsRecording)
// На ARM используем последовательное сохранение
if optimization.IsARM() {
for id, ch := range r.channels {
if err := db.SetChannelState(id, ch.IsRecording); err != nil {
log.Printf("Failed to save state for channel %s: %v", id, err)
} else {
log.Printf("✅ Saved state for channel %s: %v", id, ch.IsRecording)
}
time.Sleep(50 * time.Millisecond)
}
} else {
// На AMD64 можно параллельно
var wg sync.WaitGroup
for id, ch := range r.channels {
wg.Add(1)
go func(id string, ch *Channel) {
defer wg.Done()
if err := db.SetChannelState(id, ch.IsRecording); err != nil {
log.Printf("Failed to save state for channel %s: %v", id, err)
}
}(id, ch)
}
wg.Wait()
}
log.Printf("=== Сохранение состояний завершено ===")
+2 -5
View File
@@ -66,15 +66,12 @@ func (sc *StateChecker) run() {
saveTicker := time.NewTicker(30 * time.Second)
defer saveTicker.Stop()
for {
for range ticker.C {
select {
case <-sc.stopChan:
log.Println("State checker stopping...")
return
case <-ticker.C:
default:
sc.checkStates()
case <-saveTicker.C:
sc.forceSaveStates()
}
}
}
+48 -56
View File
@@ -17,15 +17,16 @@ import (
"sync"
"time"
"github.com/gorilla/websocket"
"audio-recorder/database"
"audio-recorder/devices"
"audio-recorder/logger"
"audio-recorder/network"
"audio-recorder/optimization"
"audio-recorder/recorder"
"audio-recorder/smb"
"audio-recorder/vpn"
"github.com/gorilla/websocket"
)
// WebSocket upgrader с настройками безопасности
@@ -1111,7 +1112,12 @@ func HandleLiveStream(w http.ResponseWriter, r *http.Request) {
}
}()
buffer := make([]byte, 8192)
// Оптимизированный буфер для ARM
bufferSize := 8192
if optimization.IsARM() {
bufferSize = 4096
}
buffer := make([]byte, bufferSize)
done := make(chan struct{})
go func() {
@@ -1828,7 +1834,6 @@ func HandleBrowseDirectory(w http.ResponseWriter, r *http.Request) {
path = "/"
}
// Разрешенные пути для просмотра
allowedPrefixes := []string{"/etc/openvpn", "/etc/wireguard", "/home", "/root", "/opt", "/var", "/tmp"}
absPath, err := filepath.Abs(path)
@@ -1979,7 +1984,7 @@ func HandleInterfaceControl(w http.ResponseWriter, r *http.Request) {
var req struct {
Interface string `json:"interface"`
Action string `json:"action"` // up/down
Action string `json:"action"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -2008,8 +2013,6 @@ func HandleInterfaceControl(w http.ResponseWriter, r *http.Request) {
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "ok"})
}
// ... весь код до этих функций остается без изменений ...
// HandleWiFiScan - сканирование WiFi сетей
func HandleWiFiScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
@@ -2023,6 +2026,8 @@ func HandleWiFiScan(w http.ResponseWriter, r *http.Request) {
return
}
// Для ARM используем контекст с таймаутом
nm := network.GetManager()
networks, err := nm.ScanWiFiNetworks(iface)
if err != nil {
@@ -2240,7 +2245,6 @@ func HandleTimeSettings(w http.ResponseWriter, r *http.Request) {
timezone = "Europe/Moscow"
}
// Получаем текущее системное время
currentTime := time.Now()
utcTime := time.Now().UTC()
@@ -2267,7 +2271,6 @@ func HandleTimeSettings(w http.ResponseWriter, r *http.Request) {
return
}
// Сохраняем настройки
if req.TimeMode != "" {
db.SetSetting("time_mode", req.TimeMode)
}
@@ -2290,7 +2293,6 @@ func HandleTimeSettings(w http.ResponseWriter, r *http.Request) {
return
}
// Устанавливаем часовой пояс
if req.Timezone != "" {
if err := setTimezone(req.Timezone); err != nil {
log.Printf("Warning: failed to set timezone: %v", err)
@@ -2338,13 +2340,11 @@ func getTimezoneOffset() string {
func syncWithNTP(server string) error {
log.Printf("Syncing time with NTP server: %s", server)
// Используем ntpdate
cmd := exec.Command("sudo", "ntpdate", "-u", server)
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("ntpdate failed: %v, output: %s", err, output)
// Пробуем timedatectl
cmd2 := exec.Command("sudo", "timedatectl", "set-ntp", "true")
if err2 := cmd2.Run(); err2 != nil {
return fmt.Errorf("failed to sync time: %v", err)
@@ -2358,7 +2358,6 @@ func syncWithNTP(server string) error {
func setSystemTime(dateTime string, timezone string) error {
log.Printf("Setting system time: %s with timezone: %s", dateTime, timezone)
// Парсим дату и время в местном времени
loc, err := time.LoadLocation(timezone)
if err != nil {
loc = time.Local
@@ -2369,10 +2368,8 @@ func setSystemTime(dateTime string, timezone string) error {
return fmt.Errorf("invalid datetime format: %v", err)
}
// Преобразуем в UTC для системных часов
utcTime := t.UTC()
// Устанавливаем системное время в UTC
dateStr := utcTime.Format("2006-01-02")
timeStr := utcTime.Format("15:04:05")
@@ -2382,7 +2379,6 @@ func setSystemTime(dateTime string, timezone string) error {
return fmt.Errorf("failed to set system time: %v", err)
}
// Синхронизируем аппаратные часы
exec.Command("sudo", "hwclock", "--systohc", "--utc").Run()
log.Printf("System time set to UTC: %s", utcTime.Format("2006-01-02 15:04:05"))
@@ -2392,18 +2388,14 @@ func setSystemTime(dateTime string, timezone string) error {
func setTimezone(timezone string) error {
log.Printf("Setting timezone: %s", timezone)
// Проверяем существование файла зоны
zoneFile := "/usr/share/zoneinfo/" + timezone
if _, err := os.Stat(zoneFile); err != nil {
return fmt.Errorf("timezone not found: %s", timezone)
}
// Устанавливаем часовой пояс через timedatectl
cmd := exec.Command("sudo", "timedatectl", "set-timezone", timezone)
if output, err := cmd.CombinedOutput(); err != nil {
log.Printf("timedatectl failed: %v, output: %s", err, output)
// Альтернативный способ через ln
exec.Command("sudo", "ln", "-sf", zoneFile, "/etc/localtime").Run()
}
@@ -2427,7 +2419,7 @@ func HandleGetDisks(w http.ResponseWriter, r *http.Request) {
sendJSONResponse(w, http.StatusOK, disks)
}
// HandleMountDisk - монтирование диска с сохранением настройки
// HandleMountDisk - монтирование диска
func HandleMountDisk(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -2466,7 +2458,7 @@ func HandleMountDisk(w http.ResponseWriter, r *http.Request) {
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "mounted"})
}
// HandleUnmountDisk - отмонтирование диска с удалением настройки
// HandleUnmountDisk - отмонтирование диска
func HandleUnmountDisk(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -2499,40 +2491,6 @@ func HandleUnmountDisk(w http.ResponseWriter, r *http.Request) {
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "unmounted"})
}
// HandleGetMountConfigs - получение сохраненных настроек монтирования
func HandleGetMountConfigs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
dm := network.GetDiskManager()
configs, err := dm.GetAllMountConfigsFromDB()
if err != nil {
sendJSONError(w, err.Error(), http.StatusInternalServerError)
return
}
sendJSONResponse(w, http.StatusOK, configs)
}
// HandleAutoMount - принудительный запуск автомонтирования
func HandleAutoMount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
dm := network.GetDiskManager()
err := dm.AutoMountDisks()
if err != nil {
sendJSONError(w, err.Error(), http.StatusInternalServerError)
return
}
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "auto_mount_completed"})
}
// HandleFormatDisk - форматирование диска
func HandleFormatDisk(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
@@ -2569,3 +2527,37 @@ func HandleFormatDisk(w http.ResponseWriter, r *http.Request) {
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "formatted"})
}
// HandleGetMountConfigs - получение сохраненных настроек монтирования
func HandleGetMountConfigs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
dm := network.GetDiskManager()
configs, err := dm.GetAllMountConfigsFromDB()
if err != nil {
sendJSONError(w, err.Error(), http.StatusInternalServerError)
return
}
sendJSONResponse(w, http.StatusOK, configs)
}
// HandleAutoMount - принудительный запуск автомонтирования
func HandleAutoMount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
dm := network.GetDiskManager()
err := dm.AutoMountDisks()
if err != nil {
sendJSONError(w, err.Error(), http.StatusInternalServerError)
return
}
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "auto_mount_completed"})
}
+36 -3
View File
@@ -1,9 +1,42 @@
/* style.css - Dark Theme, белый текст, исправлены фоны */
/* CSS Reset для совместимости с разными браузерами */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* Для select элементов возвращаем стандартный вид */
select {
-webkit-appearance: menulist;
-moz-appearance: menulist;
appearance: menulist;
}
/* Для input */
input[type="text"],
input[type="number"],
input[type="password"],
input[type="date"],
input[type="time"] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
}
/* Для checkbox */
input[type="checkbox"] {
-webkit-appearance: checkbox;
-moz-appearance: checkbox;
appearance: checkbox;
}
/* Для range slider */
input[type="range"] {
-webkit-appearance: slider-horizontal;
-moz-appearance: slider-horizontal;
appearance: slider-horizontal;
}
body {
+55 -11
View File
@@ -1,6 +1,7 @@
package vpn
import (
"context"
"fmt"
"io"
"log"
@@ -13,6 +14,7 @@ import (
"audio-recorder/database"
"audio-recorder/logger"
"audio-recorder/optimization"
)
type OpenVPNClient struct {
@@ -80,6 +82,11 @@ func (v *OpenVPNClient) Start() error {
return fmt.Errorf("config file not found: %s", v.ConfigPath)
}
// Для ARM добавляем задержку перед запуском
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
// Создаем auth файл с правильным форматированием
var authFile string
if v.AuthUser != "" && v.AuthPassword != "" {
@@ -113,8 +120,15 @@ func (v *OpenVPNClient) Start() error {
log.Printf("Starting OpenVPN with config: %s", v.ConfigPath)
logger.Add("INFO", "OPENVPN", "Запуск OpenVPN | Конфиг: "+v.ConfigPath)
// Запускаем openvpn с sudo для прав root
cmd := exec.Command("sudo", "openvpn", "--config", tempConfig)
// Для ARM используем контекст с таймаутом
var cmd *exec.Cmd
if optimization.IsARM() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sudo", "openvpn", "--config", tempConfig)
} else {
cmd = exec.Command("sudo", "openvpn", "--config", tempConfig)
}
// Создаем pipes для логов
stdout, err := cmd.StdoutPipe()
@@ -146,9 +160,14 @@ func (v *OpenVPNClient) Start() error {
db := database.GetDB()
db.SetSetting("openvpn_was_running", "true")
// Ждем установки подключения
// Ждем установки подключения с разным таймаутом для разных архитектур
timeout := 10 * time.Second
if optimization.IsARM() {
timeout = 15 * time.Second
}
go func() {
time.Sleep(10 * time.Second)
time.Sleep(timeout)
v.updateConnectionStatus()
if !v.Connected {
logger.Add("WARNING", "OPENVPN", "Подключение не установлено, проверьте логи")
@@ -276,7 +295,11 @@ func (v *OpenVPNClient) parseOutput(line string) {
}
func (v *OpenVPNClient) monitorConnection() {
ticker := time.NewTicker(5 * time.Second)
interval := 5 * time.Second
if optimization.IsARM() {
interval = 10 * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
@@ -290,7 +313,11 @@ func (v *OpenVPNClient) monitorConnection() {
}
func (v *OpenVPNClient) periodicHealthCheck() {
ticker := time.NewTicker(30 * time.Second)
interval := 30 * time.Second
if optimization.IsARM() {
interval = 45 * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
@@ -305,10 +332,14 @@ func (v *OpenVPNClient) periodicHealthCheck() {
if wasConnected && !v.Connected {
logger.Add("WARNING", "OPENVPN", "Потеряно подключение, попытка переподключения")
go func() {
time.Sleep(5 * time.Second)
sleepTime := 5 * time.Second
if optimization.IsARM() {
sleepTime = 8 * time.Second
}
time.Sleep(sleepTime)
v.Reconnect()
}()
} else if v.Connected {
} else if v.Connected && optimization.GetLogLevel() == "INFO" {
log.Printf("OpenVPN health check: OK, IP: %s", v.ConnectionIP)
}
}
@@ -328,7 +359,12 @@ func (v *OpenVPNClient) Reconnect() error {
logger.Add("INFO", "OPENVPN", "Переподключение OpenVPN")
v.stopOpenVPN()
time.Sleep(3 * time.Second)
sleepTime := 3 * time.Second
if optimization.IsARM() {
sleepTime = 5 * time.Second
}
time.Sleep(sleepTime)
// Запускаем заново через Start
v.mu.Unlock()
@@ -490,7 +526,11 @@ func (v *OpenVPNClient) UpdateConfig(config OpenVPNConfig) error {
go func() {
v.Stop()
if v.Enabled {
time.Sleep(2 * time.Second)
sleepTime := 2 * time.Second
if optimization.IsARM() {
sleepTime = 3 * time.Second
}
time.Sleep(sleepTime)
v.Start()
}
}()
@@ -537,7 +577,11 @@ func (v *OpenVPNClient) RestoreState() {
log.Printf("Restoring OpenVPN connection...")
logger.Add("INFO", "OPENVPN", "Восстановление подключения после перезагрузки")
go func() {
time.Sleep(5 * time.Second)
sleepTime := 5 * time.Second
if optimization.IsARM() {
sleepTime = 8 * time.Second
}
time.Sleep(sleepTime)
v.Start()
}()
}
+35 -63
View File
@@ -12,14 +12,9 @@ import (
"audio-recorder/database"
"audio-recorder/logger"
"audio-recorder/optimization"
)
type WireGuardConfig struct {
Enabled bool `json:"enabled"`
ConfigPath string `json:"config_path"`
Interface string `json:"interface"`
}
type WireGuardClient struct {
Enabled bool `json:"enabled"`
Interface string `json:"interface"`
@@ -38,6 +33,12 @@ type WireGuardClient struct {
started bool
}
type WireGuardConfig struct {
Enabled bool `json:"enabled"`
ConfigPath string `json:"config_path"`
Interface string `json:"interface"`
}
var wgInstance *WireGuardClient
var wgOnce sync.Once
@@ -97,17 +98,20 @@ func (w *WireGuardClient) Start() error {
w.stopWireGuard()
// Для ARM добавляем задержку
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
log.Printf("Starting WireGuard interface: %s", w.Interface)
logger.Add("INFO", "WIREGUARD", "Запуск WireGuard | Интерфейс: "+w.Interface)
// Создаем конфиг для wg setconf (без Address)
if err := w.createWgConfig(); err != nil {
log.Printf("Failed to create wg config: %v", err)
logger.Add("ERROR", "WIREGUARD", "Ошибка создания конфига: "+err.Error())
return err
}
// Ручной запуск
if err := w.startManual(); err != nil {
log.Printf("Failed to start WireGuard: %v", err)
logger.Add("ERROR", "WIREGUARD", "Ошибка запуска: "+err.Error())
@@ -139,7 +143,6 @@ func (w *WireGuardClient) createWgConfig() error {
}
defer file.Close()
// Только параметры для wg setconf
fmt.Fprintf(file, "[Interface]\n")
fmt.Fprintf(file, "PrivateKey = %s\n", w.PrivateKey)
@@ -163,11 +166,11 @@ func (w *WireGuardClient) createWgConfig() error {
func (w *WireGuardClient) startManual() error {
log.Printf("=== Starting WireGuard manual setup ===")
// Удаляем старый интерфейс
exec.Command("ip", "link", "delete", w.Interface).Run()
time.Sleep(500 * time.Millisecond)
if optimization.IsARM() {
time.Sleep(500 * time.Millisecond)
}
// Создаем новый интерфейс
log.Printf("Creating interface %s", w.Interface)
createCmd := exec.Command("ip", "link", "add", w.Interface, "type", "wireguard")
if output, err := createCmd.CombinedOutput(); err != nil {
@@ -176,7 +179,6 @@ func (w *WireGuardClient) startManual() error {
}
log.Printf("Interface %s created successfully", w.Interface)
// Поднимаем интерфейс
log.Printf("Bringing up interface %s", w.Interface)
upCmd := exec.Command("ip", "link", "set", w.Interface, "up")
if output, err := upCmd.CombinedOutput(); err != nil {
@@ -185,7 +187,6 @@ func (w *WireGuardClient) startManual() error {
}
log.Printf("Interface %s is up", w.Interface)
// Применяем конфигурацию WireGuard
configPath := "/etc/wireguard/" + w.Interface + ".conf"
setconfCmd := exec.Command("wg", "setconf", w.Interface, configPath)
if output, err := setconfCmd.CombinedOutput(); err != nil {
@@ -194,7 +195,6 @@ func (w *WireGuardClient) startManual() error {
}
log.Printf("WireGuard config applied successfully")
// Добавляем IP адреса (разделяем по запятой)
if w.Address != "" {
addresses := strings.Split(w.Address, ",")
for _, addr := range addresses {
@@ -202,7 +202,6 @@ func (w *WireGuardClient) startManual() error {
if addr == "" {
continue
}
// Пропускаем IPv6 если есть
if strings.Contains(addr, ":") {
log.Printf("Skipping IPv6 address: %s", addr)
continue
@@ -217,7 +216,6 @@ func (w *WireGuardClient) startManual() error {
}
}
// Добавляем маршруты для AllowedIPs
if w.PeerAllowedIPs != "" {
allowedIPs := strings.Split(w.PeerAllowedIPs, ",")
for _, ip := range allowedIPs {
@@ -225,11 +223,9 @@ func (w *WireGuardClient) startManual() error {
if ip == "" {
continue
}
// Пропускаем IPv6
if strings.Contains(ip, ":") {
continue
}
// Пропускаем 0.0.0.0/0 (default route) - добавим отдельно
if ip == "0.0.0.0/0" {
continue
}
@@ -243,31 +239,27 @@ func (w *WireGuardClient) startManual() error {
}
}
// Настраиваем MTU
if w.MTU > 0 {
log.Printf("Setting MTU to %d on %s", w.MTU, w.Interface)
mtuCmd := exec.Command("ip", "link", "set", "mtu", strconv.Itoa(w.MTU), "dev", w.Interface)
mtuCmd.Run()
}
// Показываем состояние WireGuard
showCmd := exec.Command("wg", "show", w.Interface)
showOutput, _ := showCmd.CombinedOutput()
log.Printf("WireGuard state:\n%s", string(showOutput))
// Показываем маршруты
routeCmd := exec.Command("ip", "route", "show", "dev", w.Interface)
routeOutput, _ := routeCmd.CombinedOutput()
log.Printf("Routes for %s:\n%s", w.Interface, string(routeOutput))
return nil
}
// testConnectivity проверяет реальную работоспособность VPN
func (w *WireGuardClient) testConnectivity() bool {
// Пытаемся пинговать стандартный IP сервера в VPN сети
testIPs := []string{"10.8.0.1", "8.8.8.8", "1.1.1.1"}
// Для ARM уменьшаем количество проверок
if optimization.IsARM() {
testIPs = []string{"10.8.0.1", "8.8.8.8"}
}
for _, testIP := range testIPs {
cmd := exec.Command("ping", "-c", "1", "-W", "2", testIP)
if err := cmd.Run(); err == nil {
@@ -276,30 +268,11 @@ func (w *WireGuardClient) testConnectivity() bool {
}
}
// Пробуем альтернативные IP из AllowedIPs
if w.PeerAllowedIPs != "" {
ips := strings.Split(w.PeerAllowedIPs, ",")
for _, ip := range ips {
ip = strings.TrimSpace(ip)
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
if strings.Contains(ip, ".") && !strings.Contains(ip, "0.0.0.0") && !strings.Contains(ip, "0.0.0") {
cmd := exec.Command("ping", "-c", "1", "-W", "2", ip)
if cmd.Run() == nil {
log.Printf("Connectivity test OK: ping to %s successful", ip)
return true
}
}
}
}
log.Printf("Connectivity test FAILED: no ping responses")
return false
}
func (w *WireGuardClient) stopWireGuard() {
// Удаляем маршруты
if w.PeerAllowedIPs != "" {
allowedIPs := strings.Split(w.PeerAllowedIPs, ",")
for _, ip := range allowedIPs {
@@ -313,7 +286,6 @@ func (w *WireGuardClient) stopWireGuard() {
exec.Command("ip", "route", "del", ip, "dev", w.Interface).Run()
}
}
// Удаляем интерфейс
exec.Command("ip", "link", "delete", w.Interface).Run()
w.started = false
w.Connected = false
@@ -321,7 +293,11 @@ func (w *WireGuardClient) stopWireGuard() {
}
func (w *WireGuardClient) monitorConnection() {
ticker := time.NewTicker(15 * time.Second)
interval := 15 * time.Second
if optimization.IsARM() {
interval = 20 * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
@@ -382,11 +358,9 @@ func (w *WireGuardClient) updateConnectionStatus() {
defer w.mu.Unlock()
if err == nil && strings.Contains(string(output), "transfer:") {
// WireGuard handshake есть, проверяем реальную связь
if w.testConnectivity() {
w.Connected = true
} else {
// Handshake есть, но трафик не проходит
log.Printf("⚠️ WireGuard handshake exists but no connectivity")
w.Connected = false
}
@@ -442,16 +416,6 @@ func (w *WireGuardClient) IsConnected() bool {
return w.Connected && w.Enabled
}
// IsReallyConnected проверяет реальное подключение (для VPN Manager)
func (w *WireGuardClient) IsReallyConnected() bool {
if !w.Enabled {
return false
}
w.mu.RLock()
defer w.mu.RUnlock()
return w.Connected && w.testConnectivity()
}
func (w *WireGuardClient) LoadFromDB() {
db := database.GetDB()
@@ -486,7 +450,11 @@ func (w *WireGuardClient) LoadFromDB() {
if w.Enabled && wasRunning == "true" && w.PrivateKey != "" && w.PeerPublicKey != "" {
log.Printf("Restoring WireGuard connection...")
go func() {
time.Sleep(5 * time.Second)
sleepTime := 5 * time.Second
if optimization.IsARM() {
sleepTime = 7 * time.Second
}
time.Sleep(sleepTime)
w.Start()
}()
}
@@ -554,7 +522,11 @@ func (w *WireGuardClient) UpdateConfig(interfaceName, privateKey, address, dns s
go func() {
w.Stop()
if w.Enabled {
time.Sleep(2 * time.Second)
sleepTime := 2 * time.Second
if optimization.IsARM() {
sleepTime = 3 * time.Second
}
time.Sleep(sleepTime)
w.Start()
}
}()