feat(cli): add settings menu and VLESS log viewer with core selection
- Add settings menu to switch between Xray and V2Ray cores for VLESS connections - Implement core type persistence in configuration with LoadSettings/SaveSettings - Add VLESS error and access log viewer showing last 30 and 20 lines respectively - Display current core type and system time in main menu - Update VLESS connection to use selected core dynamically - Refactor monitor.go to accept 'q' key input for graceful exit instead of signal handling - Add proxy platform-specific implementations (proxy_unix.go, proxy_windows.go) - Add downloader module for managing binary resources - Include V2Ray and Xray configuration files and geodata (geoip.dat, geosite.dat) - Update CLI imports to include path/filepath and time packages - Improve user experience with core selection visibility and log diagnostics
This commit is contained in:
@@ -5,9 +5,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/subscription"
|
||||
@@ -69,6 +71,14 @@ func Run() error {
|
||||
fmt.Printf("%s %v\n", red("✗"), err)
|
||||
}
|
||||
pause()
|
||||
} else {
|
||||
// Настройки
|
||||
settingsMenu()
|
||||
}
|
||||
case "7":
|
||||
if isConnected {
|
||||
// Настройки
|
||||
settingsMenu()
|
||||
} else {
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
@@ -86,12 +96,25 @@ func Run() error {
|
||||
func showMainMenu() {
|
||||
state, _ := vpn.GetStatus()
|
||||
|
||||
// Получаем актуальный статус с временем
|
||||
statusLine := ShowQuickStatus()
|
||||
|
||||
// Показываем текущее ядро
|
||||
settings, _ := config.LoadSettings()
|
||||
coreName := "Xray"
|
||||
if settings != nil && settings.CoreType == "v2ray" {
|
||||
coreName = "V2Ray"
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println(bold("VPN Клиент (Go)"))
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Printf("Статус: %s\n", statusLine)
|
||||
fmt.Printf("Ядро VLESS: %s\n", cyan(coreName))
|
||||
|
||||
// Показываем текущее время
|
||||
fmt.Printf("Время: %s\n", time.Now().Format("15:04:05"))
|
||||
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println("1. WireGuard")
|
||||
fmt.Println("2. VLESS")
|
||||
@@ -101,8 +124,10 @@ func showMainMenu() {
|
||||
if state != nil && state.Connected {
|
||||
fmt.Println("5. " + green("Мониторинг в реальном времени"))
|
||||
fmt.Println("6. Отключиться от VPN")
|
||||
fmt.Println("7. " + yellow("Настройки (выбор ядра)"))
|
||||
} else {
|
||||
fmt.Println("5. Отключиться от VPN")
|
||||
fmt.Println("6. " + yellow("Настройки (выбор ядра)"))
|
||||
}
|
||||
|
||||
fmt.Println("0. Выход")
|
||||
@@ -120,6 +145,7 @@ func vlessMenu() {
|
||||
fmt.Println("3. Удалить конфиг")
|
||||
fmt.Println("4. Подключиться")
|
||||
fmt.Println("5. Тестировать конфиг (пинг)")
|
||||
fmt.Println("6. " + yellow("Показать логи ошибок"))
|
||||
fmt.Println("0. Назад")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
@@ -143,6 +169,9 @@ func vlessMenu() {
|
||||
case "5":
|
||||
testVLESSConfig()
|
||||
pause()
|
||||
case "6":
|
||||
showVLESSLogs()
|
||||
pause()
|
||||
default:
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
@@ -270,9 +299,16 @@ func connectVLESS() {
|
||||
|
||||
configName := configs.VLESS[num-1].Name
|
||||
|
||||
fmt.Printf("\nПодключение к '%s' через Xray...\n", configName)
|
||||
// Показываем текущее ядро
|
||||
settings, _ := config.LoadSettings()
|
||||
coreName := "Xray"
|
||||
if settings != nil && settings.CoreType == "v2ray" {
|
||||
coreName = "V2Ray"
|
||||
}
|
||||
|
||||
if err := vless.Connect(configName, config.LogsDir, config.XrayDir); err != nil {
|
||||
fmt.Printf("\nПодключение к '%s' через %s...\n", configName, coreName)
|
||||
|
||||
if err := vless.Connect(configName, config.LogsDir); err != nil {
|
||||
fmt.Printf("%s Ошибка подключения: %v\n", red("✗"), err)
|
||||
return
|
||||
}
|
||||
@@ -904,3 +940,105 @@ func connectWireGuard() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func settingsMenu() {
|
||||
clearScreen()
|
||||
|
||||
settings, err := config.LoadSettings()
|
||||
if err != nil {
|
||||
fmt.Printf("%s Ошибка загрузки настроек: %v\n", red("✗"), err)
|
||||
pause()
|
||||
return
|
||||
}
|
||||
|
||||
currentCore := "Xray"
|
||||
if settings.CoreType == "v2ray" {
|
||||
currentCore = "V2Ray"
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println(bold("Настройки"))
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Printf("Текущее ядро для VLESS: %s\n", cyan(currentCore))
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println("\nВыберите ядро для подключения к VLESS:")
|
||||
fmt.Println("1. Xray (рекомендуется)")
|
||||
fmt.Println("2. V2Ray")
|
||||
fmt.Println("0. Назад")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
choice := readInput("\nВыберите действие: ")
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
settings.CoreType = "xray"
|
||||
if err := config.SaveSettings(settings); err != nil {
|
||||
fmt.Printf("%s Ошибка сохранения настроек: %v\n", red("✗"), err)
|
||||
} else {
|
||||
fmt.Printf("%s Ядро изменено на Xray\n", green("✓"))
|
||||
}
|
||||
pause()
|
||||
case "2":
|
||||
settings.CoreType = "v2ray"
|
||||
if err := config.SaveSettings(settings); err != nil {
|
||||
fmt.Printf("%s Ошибка сохранения настроек: %v\n", red("✗"), err)
|
||||
} else {
|
||||
fmt.Printf("%s Ядро изменено на V2Ray\n", green("✓"))
|
||||
}
|
||||
pause()
|
||||
case "0":
|
||||
return
|
||||
default:
|
||||
fmt.Printf("%s Неверный выбор\n", red("✗"))
|
||||
pause()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showVLESSLogs() {
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println(bold("Логи VLESS/Xray/V2Ray"))
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
errorLog := filepath.Join(config.LogsDir, "vless_error.log")
|
||||
accessLog := filepath.Join(config.LogsDir, "vless_access.log")
|
||||
|
||||
// Показываем лог ошибок
|
||||
fmt.Println("\n" + yellow("=== Лог ошибок (последние 30 строк) ==="))
|
||||
if data, err := os.ReadFile(errorLog); err == nil && len(data) > 0 {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
start := len(lines) - 30
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for i := start; i < len(lines); i++ {
|
||||
if strings.TrimSpace(lines[i]) != "" {
|
||||
fmt.Println(lines[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Лог пуст или не найден")
|
||||
}
|
||||
|
||||
fmt.Println("\n" + cyan("=== Лог доступа (последние 20 строк) ==="))
|
||||
if data, err := os.ReadFile(accessLog); err == nil && len(data) > 0 {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
start := len(lines) - 20
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for i := start; i < len(lines); i++ {
|
||||
if strings.TrimSpace(lines[i]) != "" {
|
||||
fmt.Println(lines[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Лог пуст или не найден")
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Printf("Полные логи:\n")
|
||||
fmt.Printf(" Ошибки: %s\n", errorLog)
|
||||
fmt.Printf(" Доступ: %s\n", accessLog)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -25,20 +26,22 @@ func MonitorConnection() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Нажмите Ctrl+C для выхода из мониторинга\n")
|
||||
fmt.Println("Нажмите 'q' или Ctrl+C для выхода из мониторинга\n")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Создаем канал для обработки сигналов
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Создаем канал для остановки мониторинга
|
||||
stopChan := make(chan bool, 1)
|
||||
|
||||
// Запускаем горутину для обработки сигналов
|
||||
// Запускаем горутину для чтения клавиш
|
||||
go func() {
|
||||
<-sigChan
|
||||
stopChan <- true
|
||||
for {
|
||||
var input string
|
||||
fmt.Scanln(&input)
|
||||
if strings.ToLower(input) == "q" || strings.ToLower(input) == "й" {
|
||||
stopChan <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Запускаем мониторинг
|
||||
@@ -50,9 +53,8 @@ func MonitorConnection() error {
|
||||
case <-ticker.C:
|
||||
clearScreen()
|
||||
displayRealtimeStatus(state)
|
||||
fmt.Printf("\n%s Нажмите 'q' и Enter для выхода\n", cyan("ℹ"))
|
||||
case <-stopChan:
|
||||
// Восстанавливаем обработку сигналов по умолчанию
|
||||
signal.Reset(os.Interrupt, syscall.SIGTERM)
|
||||
fmt.Println("\n\nВыход из мониторинга...")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return nil
|
||||
@@ -104,7 +106,7 @@ func displayRealtimeStatus(state *config.ConnectionState) {
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("═", 70))
|
||||
fmt.Printf("\n%s Обновление каждую секунду | Нажмите Ctrl+C для выхода\n",
|
||||
fmt.Printf("\n%s Нажмите 'q' и Enter для выхода\n",
|
||||
cyan("ℹ"))
|
||||
}
|
||||
|
||||
@@ -126,17 +128,39 @@ func displayVLESSStats(state *config.ConnectionState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, что процесс еще работает
|
||||
// Проверяем, что процесс еще работает (улучшенная проверка для Windows)
|
||||
if state.ProcessPID > 0 {
|
||||
process, err := os.FindProcess(state.ProcessPID)
|
||||
if err == nil {
|
||||
if err := process.Signal(os.Signal(nil)); err != nil {
|
||||
fmt.Printf("\n%s Процесс Xray не отвечает!\n", red("⚠"))
|
||||
}
|
||||
processRunning := checkProcessRunning(state.ProcessPID)
|
||||
if !processRunning {
|
||||
fmt.Printf("\n%s Процесс Xray не отвечает!\n", red("⚠"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkProcessRunning проверяет, работает ли процесс
|
||||
func checkProcessRunning(pid int) bool {
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// На Windows FindProcess всегда успешен, нужна дополнительная проверка
|
||||
// Пытаемся получить информацию о процессе через tasklist
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Если процесс существует, в выводе будет его PID
|
||||
return strings.Contains(string(output), fmt.Sprintf("%d", pid))
|
||||
}
|
||||
|
||||
// На Unix используем сигнал 0
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func displayWireGuardStats(state *config.ConnectionState) {
|
||||
fmt.Println("\n" + bold("WireGuard Туннель"))
|
||||
fmt.Printf("Интерфейс: %s\n", state.Interface)
|
||||
@@ -198,7 +222,8 @@ func ShowQuickStatus() string {
|
||||
duration := time.Since(startTime)
|
||||
hours := int(duration.Hours())
|
||||
minutes := int(duration.Minutes()) % 60
|
||||
timeStr = fmt.Sprintf(" [%02d:%02d]", hours, minutes)
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
timeStr = fmt.Sprintf(" [%02d:%02d:%02d]", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user