Простой и эффективный супервизор на С++ с универсальным Makefile

от автора

Несмотря на развитие лингвистических моделей, я подумал, что моя версия супервизора может быть достаточно интересна для размещения в статье. Назначение супервизора — поднять повторно программу, которая по каким-то причинам упала с ошибкой. Причём если программа завершила работу без ошибки, то она перезапущена не будет, как и не будут создаваться логи. В логах пишется время падения и тип ошибки. Универсальный 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 


ссылка на оригинал статьи https://habr.com/ru/articles/920790/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *