diff --git a/containers_part1.go b/containers_part1.go index 75b7887..f03f73f 100644 --- a/containers_part1.go +++ b/containers_part1.go @@ -179,6 +179,48 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, } if !containerRunning { startContainer(containerFullName, composeFile, containerDir) + } else { + // Проверяем порты только если контейнер запущен недавно (менее 5 минут) + // Это позволяет исправить ситуацию когда контейнер поднялся без портов + // но не трогает давно работающие контейнеры + startedAt, err := getContainerStartedAt(containerFullName) + if err == nil && time.Since(startedAt) < 5*time.Minute { + portsOK := true + containerPorts := []PortMapping{} + for _, res := range resources { + ports, ok := resourcePortMap[res.L7ResourceID] + if !ok || containerNum-1 >= len(ports) { + continue + } + containerPorts = append(containerPorts, ports[containerNum-1]) + } + for _, pm := range containerPorts { + for _, dockerPort := range pm.HTTPPorts { + if !checkPortListening("127.0.0.1", dockerPort) { + log.Printf(" ⚠ Контейнер %s запущен недавно но порт %d не слушает — пересоздаю", containerFullName, dockerPort) + portsOK = false + break + } + } + if !portsOK { + break + } + for _, dockerPort := range pm.HTTPSPorts { + if !checkPortListening("127.0.0.1", dockerPort) { + log.Printf(" ⚠ Контейнер %s запущен недавно но порт %d не слушает — пересоздаю", containerFullName, dockerPort) + portsOK = false + break + } + } + if !portsOK { + break + } + } + if !portsOK { + recreateContainer(containerFullName, composeFile, containerDir, clientTitle, resources, resourcePortMap, containerNum) + continue + } + } } if containerChanges[containerNum] { log.Printf(" Конфигурация изменилась, проверяем и перезагружаем nginx в %s", containerFullName) @@ -196,17 +238,80 @@ func setupContainers(config Config, clientTitle string, clientInfo *ClientInfo, } } else { log.Printf(" Запуск нового контейнера %s", containerName) - cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d") - cmd.Dir = containerDir - output, _ := cmd.CombinedOutput() - log.Printf(" → Вывод docker-compose: %s", string(output)) - time.Sleep(5 * time.Second) - _, isRunning := checkContainerStatus(containerFullName) - if !isRunning { - log.Printf(" ⚠ Контейнер %s не запустился. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, containerDir) + + // Собираем порты которые должен занять контейнер + portsToCheck := []int{} + for _, res := range resources { + ports, ok := resourcePortMap[res.L7ResourceID] + if !ok || containerNum-1 >= len(ports) { + continue + } + pm := ports[containerNum-1] + for _, dockerPort := range pm.HTTPPorts { + portsToCheck = append(portsToCheck, dockerPort) + } + for _, dockerPort := range pm.HTTPSPorts { + portsToCheck = append(portsToCheck, dockerPort) + } + } + + // Ждём освобождения портов перед запуском + if len(portsToCheck) > 0 { + portsReady := false + for attempt := 0; attempt < 30; attempt++ { + busy := false + for _, port := range portsToCheck { + if isPortBusyByAnyProcess(port) { + if attempt == 0 { + log.Printf(" → Порт %d занят, ожидаем освобождения...", port) + } + busy = true + break + } + } + if !busy { + portsReady = true + if attempt > 0 { + log.Printf(" ✓ Порты освободились (попытка %d)", attempt+1) + } + break + } + time.Sleep(1 * time.Second) + } + if !portsReady { + log.Printf(" ⚠ Порты не освободились за 30 секунд, пробуем запустить всё равно") + } + } + + const maxRetries = 3 + started := false + for attempt := 1; attempt <= maxRetries; attempt++ { + cmd := exec.Command("docker-compose", "-f", composeFile, "up", "-d") + cmd.Dir = containerDir + output, err := cmd.CombinedOutput() + log.Printf(" → Вывод docker-compose: %s", string(output)) + if err != nil && strings.Contains(string(output), "address already in use") { + log.Printf(" ⚠ Попытка %d/%d: порт занят, жду 3 секунды и повторяю...", attempt, maxRetries) + time.Sleep(3 * time.Second) + continue + } + time.Sleep(5 * time.Second) + _, isRunning := checkContainerStatus(containerFullName) + if !isRunning { + log.Printf(" ⚠ Попытка %d/%d: контейнер %s не запустился", attempt, maxRetries, containerFullName) + if attempt < maxRetries { + time.Sleep(3 * time.Second) + } + continue + } + log.Printf(" ✓ Контейнер %s успешно запущен (попытка %d)", containerName, attempt) + started = true + break + } + if !started { + log.Printf(" ⚠ Контейнер %s не запустился после %d попыток. Попробуйте вручную: cd %s && docker-compose up -d", containerFullName, maxRetries, containerDir) continue } - log.Printf(" ✓ Контейнер %s успешно запущен", containerName) } } diff --git a/containers_part2.go b/containers_part2.go index 8333b67..4adc8c2 100644 --- a/containers_part2.go +++ b/containers_part2.go @@ -14,6 +14,63 @@ import ( "time" ) +// isAngieEstabOnPort проверяет есть ли активные ESTAB соединения Angie на данном порту +func isAngieEstabOnPort(port int) bool { + cmd := exec.Command("ss", "-tnap") + output, err := cmd.Output() + if err != nil { + return false + } + portStr := fmt.Sprintf(":%d", port) + for _, line := range strings.Split(string(output), "\n") { + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + if fields[0] != "ESTAB" { + continue + } + localAddr := fields[3] + if strings.HasSuffix(localAddr, portStr) && strings.Contains(line, "angie") { + return true + } + } + return false +} + +// isPortBusyByAnyProcess проверяет занят ли порт процессом который мешает биндингу. +// Проверяет только LISTEN сокеты и docker-proxy ESTAB — соединения Angie не блокируют биндинг Docker. +func isPortBusyByAnyProcess(port int) bool { + cmd := exec.Command("ss", "-tnap") + output, err := cmd.Output() + if err != nil { + return false + } + portStr := fmt.Sprintf(":%d", port) + for _, line := range strings.Split(string(output), "\n") { + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + localAddr := fields[3] // Local address:port + if !strings.HasSuffix(localAddr, portStr) { + continue + } + state := fields[0] + // LISTEN — порт занят, нельзя биндить + if state == "LISTEN" { + return true + } + // ESTAB от docker-proxy — старый контейнер ещё не остановился + if state == "ESTAB" && strings.Contains(line, "docker-proxy") { + return true + } + // TIME-WAIT — Docker может биндить поверх TIME-WAIT через SO_REUSEADDR + // поэтому не блокируем + } + return false +} + func recreateContainer(containerFullName, composeFile, containerDir, clientTitle string, resources []ResourceData, resourcePortMap map[int][]PortMapping, containerNum int) { log.Printf(" docker-compose.yml изменился, пересоздаём контейнер %s...", containerFullName) portsToFree := make([]int, 0) @@ -58,20 +115,39 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle } } + // Reload Angie после остановки контейнера чтобы закрыть keepalive ESTAB соединения + log.Printf(" → Reload Angie для закрытия keepalive соединений к остановленному контейнеру...") + reloadAngie() + + // Ждём пока Angie закроет ESTAB соединения к портам старого контейнера + log.Printf(" → Ожидаем закрытия ESTAB соединений Angie на портах %v...", portsToFree) + for attempt := 0; attempt < 30; attempt++ { + angieEstab := false + for _, port := range portsToFree { + if isAngieEstabOnPort(port) { + angieEstab = true + break + } + } + if !angieEstab { + log.Printf(" ✓ Все ESTAB соединения Angie закрыты (попытка %d)", attempt+1) + break + } + if attempt%5 == 0 && attempt > 0 { + log.Printf(" → Angie ещё держит соединения (попытка %d/30)...", attempt+1) + } + time.Sleep(1 * time.Second) + } + time.Sleep(1 * time.Second) + if len(portsToFree) > 0 { log.Printf(" → Ожидаем освобождения %d портов...", len(portsToFree)) allPortsFree := false - for attempt := 0; attempt < 180; attempt++ { // Увеличено с 60 до 180 секунд - usedPorts, err := getUsedPorts() - if err != nil { - log.Printf(" ⚠ Ошибка проверки портов: %v", err) - time.Sleep(1 * time.Second) - continue - } + for attempt := 0; attempt < 180; attempt++ { portsBusy := 0 busyPortsList := make([]int, 0) for _, port := range portsToFree { - if usedPorts[port] { + if isPortBusyByAnyProcess(port) { portsBusy++ busyPortsList = append(busyPortsList, port) } @@ -82,12 +158,12 @@ func recreateContainer(containerFullName, composeFile, containerDir, clientTitle break } if attempt%5 == 0 && attempt > 0 { - log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1) // Обновлено с 60 на 180 + log.Printf(" → Ещё заняты %d портов: %v (попытка %d/180)", portsBusy, busyPortsList, attempt+1) } time.Sleep(1 * time.Second) } if !allPortsFree { - log.Printf(" ⚠ Порты не освободились за 180 секунд!") // Обновлено с 60 на 180 + log.Printf(" ⚠ Порты не освободились за 180 секунд!") log.Printf(" → Проверьте вручную: sudo lsof -i :%d", portsToFree[0]) return } @@ -129,6 +205,20 @@ func startContainer(containerFullName, composeFile, containerDir string) { log.Printf(" ✓ Контейнер %s запущен", containerFullName) } +// getContainerStartedAt возвращает время запуска контейнера +func getContainerStartedAt(containerName string) (time.Time, error) { + cmd := exec.Command("docker", "inspect", "--format", "{{.State.StartedAt}}", containerName) + output, err := cmd.Output() + if err != nil { + return time.Time{}, fmt.Errorf("ошибка получения времени запуска: %w", err) + } + startedAt, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(string(output))) + if err != nil { + return time.Time{}, fmt.Errorf("ошибка парсинга времени: %w", err) + } + return startedAt, nil +} + func checkContainerStatus(containerName string) (exists bool, running bool) { cmd := exec.Command("docker", "ps", "-a", "--filter", fmt.Sprintf("name=^%s$", containerName), "--format", "{{.Names}}") output, err := cmd.Output() diff --git a/ports.go b/ports.go index fcffd8c..e86a423 100644 --- a/ports.go +++ b/ports.go @@ -114,7 +114,8 @@ func (pa *PortAllocator) allocatePortsForResources(resourcesData []ResourceData, func getUsedPorts() (map[int]bool, error) { usedPorts := make(map[int]bool) - cmd := exec.Command("ss", "-tuln") + // Используем -tuanl чтобы видеть все состояния: LISTEN, ESTAB, TIME-WAIT и др. + cmd := exec.Command("ss", "-tuanl") output, err := cmd.Output() if err != nil { return usedPorts, fmt.Errorf("ошибка выполнения ss: %w", err) @@ -123,10 +124,12 @@ func getUsedPorts() (map[int]bool, error) { lines := strings.Split(string(output), "\n") for _, line := range lines { fields := strings.Fields(line) + // Формат ss: Netid State Recv-Q Send-Q Local-Address:Port Peer-Address:Port if len(fields) < 5 { continue } + // Local address в 5м поле (индекс 4) localAddr := fields[4] parts := strings.Split(localAddr, ":") if len(parts) < 2 {