Несмотря на развитие лингвистических моделей, я подумал, что моя версия супервизора может быть достаточно интересна для размещения в статье. Назначение супервизора - поднять повторно программу, которая по каким-то причинам упала с ошибкой. Причём если программа завершила работу без ошибки, то она перезапущена не будет, как и не будут создаваться логи. В логах пишется время падения и тип ошибки. Универсальный 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)
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 :) Вот уж заменила она автора, так заменила.
dyadyaSerezha
22.06.2025 22:37Почти со всем согласен, но С/C++ все же потенциально более опасны, чем "управляемые" языки. Но даже если написал всё правильно, никуда не девается фрагментация памяти, а писать так, чтобы её не было, это ещё одно умение, которым мало кто владеет даже из сишников (легче прикрутить тот же watchdog/супервизор, или вообще нет требования, чтобы сервис работал 24х7 много дней подряд без падений).
Gapon65
Это не очень хороший способ ожидания завершения операции поскольку он вносит почти секундную (статистически 0.5 секунды) задержу:
Начиная с C++20, можно делать:
Подробности в: https://en.cppreference.com/w/cpp/atomic/atomic/wait
Для более старого компилятора, можно использовать стандартное решение на основе
std::condition_variable
+std::mutex
. Подробности и примеры в: https://en.cppreference.com/w/cpp/thread/condition_variable.html