Несмотря на развитие лингвистических моделей, я подумал, что моя версия супервизора может быть достаточно интересна для размещения в статье. Назначение супервизора - поднять повторно программу, которая по каким-то причинам упала с ошибкой. Причём если программа завершила работу без ошибки, то она перезапущена не будет, как и не будут создаваться логи. В логах пишется время падения и тип ошибки. Универсальный Makefile может быть интересен тем, что его достаточно закинуть в папку с исходниками, добавить необходимые пути вида:
LDFLAGS = -I/usr/include/boost
LIBS = -lboost_serialization

Тема статьи не претендует на новизну, но может оказаться кому-то полезной. В первую очередь - это бэкенд, так как непрерывность работы там более важна. Хочется отметить, что в настоящее время С++ итак достаточно надёжный язык программирования. Вопрос в том, что в учебных заведениях, как правило, сначала изучается Си, а только потом С++ и зачастую стиль кода на С++ - Си с классами. Естественно, это влияет на репутацию языка как недостаточно надёжного. С наступлением эпохи лингвистических моделей код на С++ стал существенно надёжнее, так как ошибок с памятью я вот не встречал в сгенерированном коде, а логические ошибки - явление нередкое, но сам код создаёт впечатление образцового.
Базовый код получился сравнительно небольшим, я решил его не перегружать функционалом. Основной поток оставлен пустым для возможностей дописывания под свои нужны, отслеживаемая программа запускается в дополнительном потоке.

Непосредственно код супервизора
#include <iostream>
#include <thread>
#include <atomic>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <csignal>
#include <fstream>
#include <ctime>
#include <string>
#include <iomanip>
#include <sstream>

// Функция, возвращающая строку с текущей датой и временем
std::string getCurrentDateTime() {
    // Получаем текущее время
    std::time_t now = std::time(nullptr);
    std::tm* timeInfo = std::localtime(&now);

    // Создаём поток для форматирования времени
    std::ostringstream oss;
    oss << std::put_time(timeInfo, "%Y-%m-%d %H:%M:%S");

    return oss.str();  // Возвращаем строку с датой и временем
}

std::ofstream createLogfile() {
    // Получаем текущее время
    std::time_t now = std::time(nullptr);
    std::tm* timeInfo = std::localtime(&now);

    // Форматируем дату и время (например: 2025-10-05_14-30-45)
    std::ostringstream oss;
    oss << std::put_time(timeInfo, "%Y-%m-%d_%H-%M-%S");

    std::string timestamp = oss.str();
    std::string filename = "logfile_" + timestamp + ".txt";

    // Создаем и открываем файл
    std::ofstream logFile(filename);

    if (logFile.is_open()) {
        std::cout  << "Лог-файл создан: " << std::ctime(&now) << filename << std::endl;
    } else {
        std::cerr << "Не удалось создать лог-файл!" << std::endl;
    }

    return logFile; // Возвращаем ofstream
}

int runApp(const std::string& program, int maxRestarts, std::atomic<bool>& shouldExit) {
    int restartCount = 0;
	 int status = 0;
		std::ofstream log;
		bool log_is_created=false;
    while (restartCount < maxRestarts) {
        pid_t pid = fork();
        if (pid == 0) {
            // В дочернем процессе запускаем указанную программу
            execl(program.c_str(), program.c_str(), nullptr);
            perror("execl");
            exit(EXIT_FAILURE);
        } else if (pid < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else {
            // В родительском процессе ждем завершения дочернего
            waitpid(pid, &status, 0);
            if (WIFEXITED(status)) {
                std::cerr << "Program exited with status " << WEXITSTATUS(status) << std::endl;
                shouldExit = true;
                return 0;
            } else if (WIFSIGNALED(status)) {
            	int sig=WTERMSIG(status);
                std::cerr << "Program was killed by signal " << sig << std::endl;
				 switch (sig) {
        case SIGSEGV:
            std::cout << "Segmentation fault" << std::endl;
            break;
        case SIGABRT:
            std::cout << "Aborted" << std::endl;
            break;
        case SIGFPE:
            std::cout << "Floating point exception" << std::endl;
            break;
        case SIGILL:
            std::cout << "Illegal instruction" << std::endl;
            break;
        case SIGINT:
            std::cout << "Interrupted by user (Ctrl+C)" << std::endl;
            break;
        case SIGTERM:
            std::cout << "Termination signal received" << std::endl;
            break;
        default:
            std::cout << "Unknown signal." << std::endl;
    	}
    	if(log_is_created==false) {log = createLogfile();log_is_created=true;}
	 	if (log.is_open())
	 	{
			log<<getCurrentDateTime();
			switch (sig) {
        case SIGSEGV:
            log << " Segmentation fault" << std::endl;
            break;
        case SIGABRT:
            log << " Aborted" << std::endl;
            break;
        case SIGFPE:
            log << " Floating point exception" << std::endl;
            break;
        case SIGILL:
            log << " Illegal instruction" << std::endl;
            break;
        case SIGINT:
            log << " Interrupted by user (Ctrl+C)" << std::endl;
            break;
        case SIGTERM:
            log << " Termination signal received" << std::endl;
            break;
        default:
            log << " Unknown signal." << std::endl;
    	}
			
		}
    }
        restartCount++;
        std::cout << "Restart count: " << restartCount << "/" << maxRestarts << std::endl;
        }
    }

    if (restartCount >= maxRestarts) {
        std::cerr << "Max restarts reached. Exiting." << std::endl;
        shouldExit = true; // Устанавливаем флаг завершения
    }
    return 0;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <program> <max_restarts>" << std::endl;
        return EXIT_FAILURE;
    }

    std::string program = argv[1];
    int maxRestarts = std::stoi(argv[2]);

    // Переменная для синхронизации между потоками
    std::atomic<bool> shouldExit(false);

    try {
        std::thread appThread(runApp, program, maxRestarts, std::ref(shouldExit));
        appThread.detach();
    } catch (const std::system_error& e) {
        std::cerr << "Failed to create thread: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    // Основной поток ждет завершения работы runApp
    while (!shouldExit.load()) {
        sleep(1);  // Снижаем нагрузку на процессор
    }

    std::cout << "Main thread exiting." << std::endl;
    return 0;
}

Далее перейдём к универсальному (частично универсальному) Makefile. Он не является прямым конкурентом другим системам сборки, может служить для мелких и средних проектов, для быстрого прототипирования и проверок сгенерированного LLM кода. Например в папке есть какое-то количество исходников (.cpp и .h файлов). То есть это может быть небольшой проект с тестами. Определяются файлы, которые содержат "int main". Из них получаются исполняемые файлы. Из остальных .cpp файлов получаются объектные файлы, которые хранятся в obj. Да, возможна ситуация, когда "int main" будет где-то в комментариях или в строках. Регулярное выражение служит чтобы можно было в этой же папке компилировать исходники тестов, иначе будет ошибка, что функция main уже дублируется.

Код Makefile
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++2a -O2 -s -fdata-sections -ffunction-sections -flto
LDFLAGS = -I/usr/include/boost
LIBS = -lboost_serialization

# Получение списка всех .cpp файлов в текущей директории
SRCS = $(wildcard *.cpp)

# Находим файлы, содержащие функцию main (с использованием регулярного выражения)
MAIN_SRCS = $(shell grep -l "^\s*int\s\+main\s*" $(SRCS))
MAIN_EXES = $(MAIN_SRCS:.cpp=)

# Остальные файлы для компиляции только в объектные файлы
OTHER_SRCS = $(filter-out $(MAIN_SRCS),$(SRCS))
OTHER_OBJS = $(patsubst %.cpp, obj/%.o, $(OTHER_SRCS))
MAIN_OBJS = $(patsubst %.cpp, obj/%.o, $(MAIN_SRCS))

# Основная цель
all: obj $(OTHER_OBJS) $(MAIN_OBJS) $(MAIN_EXES)
	@echo "Проверка наличия Makefile в поддиректориях..."
	@for dir in */ ; do \
		if [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ] && [ -f "$$dir/Makefile" ]; then \
			echo "Найден Makefile в директории: $$dir"; \
			$(MAKE) -C "$${dir%*/}"; \
		elif [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ]; then \
			echo "В директории $$dir нет файла Makefile"; \
		fi; \
	done

# Создание папки obj, если она не существует
obj:
	mkdir -p obj

# Правило для создания объектных файлов с зависимостями от .h файлов
obj/%.o: %.cpp
	$(CXX) $(CXXFLAGS) -MMD -MP -c -o $@ $<

# Правило для создания исполняемых файлов из файлов с main
# Каждый .cpp файл с main компилируется в отдельный исполняемый файл
# и линкуется только с общими объектными файлами
%: obj/%.o $(OTHER_OBJS)
	@echo "Linking $@"
	$(CXX) -o $@ $< $(OTHER_OBJS) $(LDFLAGS) $(LIBS)

# Подключение сгенерированных зависимостей
-include $(OTHER_OBJS:.o=.d) $(MAIN_OBJS:.o=.d)

# Очистка
clean:
	rm -rf obj $(MAIN_EXES)
	@echo "Очистка поддиректорий..."
	@for dir in */ ; do \
		if [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ] && [ -f "$$dir/Makefile" ]; then \
			echo "Выполняется очистка в директории: $$dir"; \
			$(MAKE) -C "$${dir%*/}" clean; \
		elif [ "$$dir" != "obj/" ] && [ "$$dir" != "*/" ]; then \
			echo "В директории $$dir нет файла Makefile для очистки"; \
		fi; \
	done

.PHONY: all clean

Комментарии (3)


  1. Gapon65
    22.06.2025 22:37

    Это не очень хороший способ ожидания завершения операции поскольку он вносит почти секундную (статистически 0.5 секунды) задержу:

        // Основной поток ждет завершения работы runApp
        while (!shouldExit.load()) {
            sleep(1);  // Снижаем нагрузку на процессор
        }

    Начиная с C++20, можно делать:

    shouldExit.wait(false);

    Подробности в: https://en.cppreference.com/w/cpp/atomic/atomic/wait

    Для более старого компилятора, можно использовать стандартное решение на основе std::condition_variable + std::mutex. Подробности и примеры в: https://en.cppreference.com/w/cpp/thread/condition_variable.html


  1. dan_sw
    22.06.2025 22:37

    Несмотря на развитие лингвистических моделей, я подумал, что моя версия супервизора может быть достаточно интересна для размещения в статье. 

    Как у Вас связано развитие лингвистических моделей и "своя версия" "супервизора"? Непонятно.

    Вопрос в том, что в учебных заведениях, как правило, сначала изучается Си, а только потом С++ и зачастую стиль кода на С++ - Си с классами. Естественно, это влияет на репутацию языка как недостаточно надёжного.

    Что? Откуда вообще взялись такие выводы? В каких-то учебных заведениях изучение происходит начиная с C++, в каких-то с Си, а в каких-то и то и другое изучается. Но это ни в коем случае не влияет на надёжность Си или C++. Вообще никак это не связано.

    Что Си, что C++ - достаточно надёжные языки программирования. Разработчик может написать не надёжный код и на Java, и на Kotlin, C#, Go или JavaScript. И по памяти не надёжный (да-да, в таких языках встречаются и утечки памяти, но более высокоуровневые), и по работоспособности.

    С наступлением эпохи лингвистических моделей код на С++ стал существенно надёжнее

    Опять вопрос - чего? Автор статью с LLM моделью писал? Какие-то очень глупые выводы. Типа "пришла эпоха LLM, код на C++ стал существенно надёжнее" - полная чушь. Код надёжен или не надёжен в зависимости от опыта программиста, который этот код пишет. Всё, точка. В LLM загружены огромные базы кода, написанные людьми, которые и используются LLM как фундамент. Т.е. этот вывод буквально сам себе противоречит - как он стал надёжнее (причём язык программирования), если код, на котором обучалась LLM был написан людьми? Ну глупость же.

    но сам код создаёт впечатление образцового

    Вот именно: создаёт впечатление. Атомарной переменной явно присваивать значение true

    shouldExit = true;

    вообще не стоит, вместо этого нужно вызывать метод store, т.к. он достаточно универсален и даёт большую гибкость:

    shouldExit.store(true);

    И вместо ожидания в цикле:

    // Основной поток ждет завершения работы runApp
    while (!shouldExit.load()) {
        sleep(1);  // Снижаем нагрузку на процессор
    }

    Стоило использовать condition_variable, поскольку нагрузка на процессор в данном случае всё равно будет.

    отслеживаемая программа 

    Кхм... громко сказано, но тут ничего не отслеживается. Тут тупо представлен код для перезагрузки уже не работающей программы, вот и всё. С n-ым числом попыток. Причём даже валидации аргументов нет (вдруг, argv[2] будет не числом?).

    Короче статью, как и код, написала LLM :) Вот уж заменила она автора, так заменила.


    1. dyadyaSerezha
      22.06.2025 22:37

      Почти со всем согласен, но С/C++ все же потенциально более опасны, чем "управляемые" языки. Но даже если написал всё правильно, никуда не девается фрагментация памяти, а писать так, чтобы её не было, это ещё одно умение, которым мало кто владеет даже из сишников (легче прикрутить тот же watchdog/супервизор, или вообще нет требования, чтобы сервис работал 24х7 много дней подряд без падений).