Не знаю как у вас, но у меня обычно, когда мне нужно, что-то написать с нуля начинается лихорадка и полная прострация в мыслях. В голове уже летают различные абстрактные модели, что от чего и куда. Но ни за одну из них ухватиться не получается, потому что перед тобой чистый лист и вырвав из головы одну мысль, применить ее не к чему, а вытащить весь скелет не получается потому, что ты уже думаешь о решении задачи, а тебе еще только нужно написать костяк приложения.
Ниже представлен «проект NULL», тот самый костяк, с которого обычно все и начинается. У меня.
Данный пост скорее всего не будет интересен тем кто уже матерый и тем кто на прямую не связан с разработкой на С++, т.к. ниже представленные материл несет одну единственную цель — дать готовый фундамент для начала.
Попробую для начала выделить ряд требований которые было бы неплохо реализовать в этом костяке
Цель скрипта gen_eclipse.sh это подготовить структуру папок и вызвать cmake для генерации debug и release солюшинов. А так же задать текущую версию проекта. Так сложилось, что разработка на Linux системах у меня обычно ведется в среде Eclipse, отсюда и название gen_eclipse. Но полноценно сдружить Cmake и Eclipse у меня так и не получилось. Для того, что бы сгенерированный проект открыть в Eclipse нужно сделать импорт уже существующего MAKE проекта, при том или release или debug, и через контекстное меню добавить ссылки на директории include и src.
Первое что стоит отметить, это то, что я использую Subversion и в качестве версий полагаюсь на номера ревизий. Я обычно придерживаюсь следующего формата версии: MAJOR.MINOR.REVISION. Первые два значение задаются ручками, третий это ревизия svn. Насколько мне известно, клиент subversion не умеет возвращать просто номер ревизии, поэтому я использую следующий механизм
Как правило, весь софт, который приходилось писать под Linux, это были сервера, большие и маленькие. Особенность их в том, что они работают в фоне, это службы. Я знаю, что для таких вещей принято иметь в директории init.d скрипты запуска и останова. Но! У меня еще не было ни одного случая, когда на одном сервере запускали бы только одну версию службы. Поэтому я придерживаюсь практики start stop скриптов с контролем по PID файлу.
Скрипт останова имеет куда более изощренную логику, для медленного останова сервера.
PS. Спасибо моему коллеге Андрею за то, что предложил более юзабельную версию скрипта stop.sh
В каждом из проектов у меня есть скрипт package.sh цель которого, создать достаточный инсталляционный пакет. Как правило это заархивированная папка приложения с набором файлов достаточным для работы приложения. Минимальный набор это скприты запуска останова, конфигурационный файл, само приложение, и папка для логов.
И так, что же мне обычно нужно, что бы приступить непосредственно к программированию:
Начнем по порядку:
Я выделил для себя три таких параметра. Сейчас я попробую объяснить, почему именно они.
Директория для логирования. Причина, по которой я не храню этот параметр в конфигурационном файле, это то, что в процессе разбора конфигурационного файла уже могут случиться ошибки, которые я хочу логировать. Почему директория? Я привык, что каждый запуск это отдельный лог файл, таким образом, легче удалять старые логи.
Конфигурационный файл Если не через командную строку, то как? Особенно если у вас несколько конфигураций, которые вы хотите быстро переключать.
PID файл. Единственная причина, почему я не храню его в конфигурационном файле это то, что данный параметр используется сразу в 2 местах. В start и stop скриптах. И гораздо проще вынести его в отдельный файл init, который подключается к start stop скриптам, и править его один раз, чем два (я про conf файл).
Разбор командной строки и файла конфигурации, производится средствами boost::program_options
Каждый из параметров имеет значения по умолчанию
Я не стал изобретать свой логгер, как правило, в каждой фирме он свой. В данном проекте я ограничился выводом в консоль, в режимах Note и Error. Единственно требование, которое я предъявляю к логгеру это то, что он должен поддерживать интерфейс подобный printf. Согласитесь со мной ведь printf это прекрасно. Я лишь добавил макросы для удобного процесса логирования.
На мой взгляд, корректная остановка, это одна из важнейших функций ПО о которой часто забывают. Как правило, сделать корректную остановку в уже разработанном ПО это непосильная задача. Другое дело, если придерживаться определенной стратегии с самого начала, то это становится пустяком. Я не рассматриваю всякие изощренные способы получения команды останова по сети, по SMS или через спутник. Я просто ловлю некоторые сигналы, после чего инициирую процедуру корректного останова.
Единственное что требуется это вызвать функцию wait_exit() в главном потоке, после выполнения всех активный действий.
Спасибо mejedi за указание некорректности использования обработчика сигналов для данных нужд. Надеюсь я правильно истолковал(реализовал) ваше предложение.
Вот мы и добрались к финальной части. Как уже многим стало ясно, я использую паттерн «Медиатор». Конечно, не во всей его красе, ибо никакой бизнес логики пока нет.
Если какую то работу предполагается выполнять в отдельном потоке, то для такой задачи должен существовать отдельный класс, унаследованный от специального класса Thread.
Цель которого поддержать в едином формате процесс запуска и останова.
Проект доступен в Google Code, но только для read-only. Это не потому что я жадный, я просто не знаю как открыть доступ для всех. При желании вносить правки, пишите ваш g-email, добавлю вас к проекту.
Идея оформить данную наработку в осязаемый шаблон возникла у меня месяца два назад, в процессе очередного проекта который нужно было слобзать быстро и с чистого листа. В ходе написания проекта я пришел к выводу, что это уже не первый раз, когда у меня получается подобная архитектура, и что нет смысла каждый раз вспоминать все подводные камни и приходить к тому же решению, которое использовал раньше, нужно оформить его как шаблон. Я долго мучился на счет целесообразности написания этой статьи, но вспомнив притчу про учителя и стакан воды, я решил, что лучше мне скажут, что статья не стоит и ломаного гроша, чем я буду продолжать думать о ее целесообразности.
Спасибо за внимание.
PS. Добавлены различные проверки PID файла при запуске. Обрабатываются кейсы файл с PID есть — процесса нет, PID файл пустой.
Ниже представлен «проект NULL», тот самый костяк, с которого обычно все и начинается. У меня.
Данный пост скорее всего не будет интересен тем кто уже матерый и тем кто на прямую не связан с разработкой на С++, т.к. ниже представленные материл несет одну единственную цель — дать готовый фундамент для начала.
Требования
Попробую для начала выделить ряд требований которые было бы неплохо реализовать в этом костяке
- Проектная папка
- скрипты сборки CMake и скрипты для генерации солюшинов
- скрипты запуска и останова
- приложение должно стартовать, работать и корректно завершаться, весь этот процесс должен логироваться
- логировать можно и в консоль, все равно в каждом проекте свой логгер, главное, что бы его было легко заменить.
- приложение должно разбирать командную строку
- приложение должно уметь разбирать конфигурационный файл вида key = value
- проект без boost? не, не слышал. Так что сразу интегрируем boost
- обработка ошибок. Так как это только костяк и тут, по сути, никакого перформенса нет, то делаем на исключениях.
делаем функцию захвата мира
Проектная папка
.
├── CMakeLists.txt
├── gen_eclipse.sh
├── include
│ ├── logger.h
│ ├── mediator.h
│ ├── pid.h
│ ├── program_options.h
│ ├── thread.h
│ └── version.h
├── package.sh
├── src
│ ├── logger.cpp
│ ├── main.cpp
│ ├── mediator.cpp
│ ├── pid.cpp
│ ├── program_options.cpp
│ └── version.cpp
├── start.sh
├── stop.sh
└── version.sh
Генератор солюшинов
Цель скрипта gen_eclipse.sh это подготовить структуру папок и вызвать cmake для генерации debug и release солюшинов. А так же задать текущую версию проекта. Так сложилось, что разработка на Linux системах у меня обычно ведется в среде Eclipse, отсюда и название gen_eclipse. Но полноценно сдружить Cmake и Eclipse у меня так и не получилось. Для того, что бы сгенерированный проект открыть в Eclipse нужно сделать импорт уже существующего MAKE проекта, при том или release или debug, и через контекстное меню добавить ссылки на директории include и src.
gen_eclipse.sh
#!/bin/bash
ROOT_DIR=$PWD
BUILD_DIR=$PWD/"build"
BUILD_DIR_R=$BUILD_DIR/release
BUILD_DIR_D=$BUILD_DIR/debug
mkdir -p $BUILD_DIR
mkdir -p $BUILD_DIR_R
mkdir -p $BUILD_DIR_D
if [ -d $BUILD_DIR_R ]; then
if [ -f $BUILD_DIR_R/CMakeCache.txt ]; then
rm $BUILD_DIR_R/CMakeCache.txt
fi
fi
if [ -d $BUILD_DIR_D ]; then
if [ -f $BUILD_DIR_D/CMakeCache.txt ]; then
rm $BUILD_DIR_D/CMakeCache.txt
fi
fi
echo "[[ Generate Release solution]]"
cd $BUILD_DIR_R
cmake -G "Eclipse CDT4 - Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING="Release" --build $BUILD_DIR_R ../../
echo
echo "[[ Generate Debug solution]]"
cd $BUILD_DIR_D
cmake -G "Eclipse CDT4 - Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING="Debug" --build $BUILD_DIR_D ../../
cd $ROOT_DIR
./version.sh
Версия
Первое что стоит отметить, это то, что я использую Subversion и в качестве версий полагаюсь на номера ревизий. Я обычно придерживаюсь следующего формата версии: MAJOR.MINOR.REVISION. Первые два значение задаются ручками, третий это ревизия svn. Насколько мне известно, клиент subversion не умеет возвращать просто номер ревизии, поэтому я использую следующий механизм
REVISION=`LANG=C svn info | grep "Last Changed Rev:" | sed s/"Last Changed Rev":\ //`
if [[ "$REVISION" == "" ]]; then
echo "Cannot recognize number of revision"
exit 1
fi
...
VER_CPP=src/version.cpp
echo "#include \"version.h\"" > $VER_CPP
echo "const char* VERSION = \"$VERSION\";" >> $VER_CPP
Скрипты запуска, останова
Как правило, весь софт, который приходилось писать под Linux, это были сервера, большие и маленькие. Особенность их в том, что они работают в фоне, это службы. Я знаю, что для таких вещей принято иметь в директории init.d скрипты запуска и останова. Но! У меня еще не было ни одного случая, когда на одном сервере запускали бы только одну версию службы. Поэтому я придерживаюсь практики start stop скриптов с контролем по PID файлу.
start.sh
#!/bin/bash
source init.conf
MAIN_LOG="$APP_LOG_DIR"/start.log
echo "Start application '$APP_NAME'"
if [ -f $APP_PID ]; then
PID=`cat $APP_PID`
if [ -z $PID ]; then
echo "File '$APP_PID' exist but it's empty, delete it"
rm $APP_PID
elif ! ps h -p $PID > /dev/null; then
echo "File '$APP_PID' exist but process with pid '$PID' doesn't exist, delete it"
rm $APP_PID
else
echo "$APP_NAME already started (file $APP_PID exist)"
exit
fi
fi
mkdir -p $APP_LOG_DIR
if [ $APP_EXPORT_LIB_DIR ]; then export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$APP_EXPORT_LIB_DIR; fi
echo =========================================== >> $MAIN_LOG
date >> $MAIN_LOG
if [ -f $APP_BIN ]; then
./$APP_BIN -l $APP_LOG_DIR -c $APP_CONF -p $APP_PID >> $MAIN_LOG &
else
echo "Error: binary file '$APP_BIN' doesn't exist"
exit 1
fi
if [[ $? != 0 ]]; then
echo "Not started"
else
echo "Started"
fi
Скрипт останова имеет куда более изощренную логику, для медленного останова сервера.
stop.sh
#!/bin/bash
source init.conf
if [ ! -f $APP_PID ]; then
echo "'$APP_NAME' not started (file $APP_PID doesn't exist)"
exit
fi
PID=`cat $APP_PID`
if ! ps h -p $PID > /dev/null
then
echo "'$APP_NAME' not started, removing old $APP_PID file"
rm $APP_PID
exit
fi
if ! kill -s SIGTERM $PID
then
echo "Cannot stop process"
exit
fi
for i in {1..10}
do
if ps h -p $PID > /dev/null
then
echo -n .
sleep 1
else
echo "Stopped"
exit
fi
done
echo
echo "Can't correctly stop application, finish him"
kill -9 $PID
rm $APP_PID
PS. Спасибо моему коллеге Андрею за то, что предложил более юзабельную версию скрипта stop.sh
Package.sh
В каждом из проектов у меня есть скрипт package.sh цель которого, создать достаточный инсталляционный пакет. Как правило это заархивированная папка приложения с набором файлов достаточным для работы приложения. Минимальный набор это скприты запуска останова, конфигурационный файл, само приложение, и папка для логов.
package.sh
#!/bin/bash
APP_NAME=projectnull
VERSION=`./version.sh show`
PACKAGE=$APP_NAME.$VERSION.tar.bz2
echo "Create instalation package of '$APP_NAME' ($PACKAGE)"
TEMP_FOLDER=$APP_NAME
FILES=( "build/release/projectnull"
"start.sh"
"stop.sh"
"init.conf"
"*.conf" )
LOG_DIR=logs
if [ -d $TEMP_FOLDER ]; then
rm -rf $TEMP_FOLDER
fi
mkdir $TEMP_FOLDER
for i in "${FILES[@]}"
do
echo "copy '$i'"
cp $i $TEMP_FOLDER
done
echo creat $LOG_DIR
mkdir $TEMP_FOLDER/$LOG_DIR
tar -cjf $PACKAGE $TEMP_FOLDER
rm -rf $TEMP_FOLDER
echo Finished
Функционал
И так, что же мне обычно нужно, что бы приступить непосредственно к программированию:
- Параметры командной строки первой важности
- Файл конфигурации
- Простой способ взаимодействия с логгером
- Возможность для корректной остановки приложения
Начнем по порядку:
Параметры командной строки первой важности
Я выделил для себя три таких параметра. Сейчас я попробую объяснить, почему именно они.
Директория для логирования. Причина, по которой я не храню этот параметр в конфигурационном файле, это то, что в процессе разбора конфигурационного файла уже могут случиться ошибки, которые я хочу логировать. Почему директория? Я привык, что каждый запуск это отдельный лог файл, таким образом, легче удалять старые логи.
Конфигурационный файл Если не через командную строку, то как? Особенно если у вас несколько конфигураций, которые вы хотите быстро переключать.
PID файл. Единственная причина, почему я не храню его в конфигурационном файле это то, что данный параметр используется сразу в 2 местах. В start и stop скриптах. И гораздо проще вынести его в отдельный файл init, который подключается к start stop скриптам, и править его один раз, чем два (я про conf файл).
Разбор командной строки и файла конфигурации, производится средствами boost::program_options
program_options.cpp
void ProgramOptions::load(int argc, char* argv[])
{
options_description desc("Allowed options");
desc.add_options()
("help,h", "produce help message")
("config,c", value<std::string>(&conf_file)->default_value(std::string(CONF_FILE)), "set configuration file")
("logdir,l", value<std::string>(&log_dir)->default_value(std::string(LOG_DIR)), "set log directory")
("pidfile,p", value<std::string>(&pid_file)->default_value(std::string(PID_FILE)), "set pid file")
;
variables_map vm;
store(parse_command_line(argc, argv, desc), vm);
notify(vm);
if (vm.count("help")) {
std::cout << desc << "\n";
exit(0);
}
std::cout << "Will be used the next options:" << std::endl
<< "CONF_FILE = " << conf_file << std::endl
<< "LOG_DIR = " << log_dir << std::endl
<< "PID_DIR = " << pid_file << std::endl
;
}
Каждый из параметров имеет значения по умолчанию
./projectnull -h
Allowed options:
-h [ --help ] produce help message
-c [ --config ] arg (=project.conf) set configuration file
-l [ --logdir ] arg (=logs) set log directory
-p [ --pidfile ] arg (=project.pid) set pid file
Логирование
Я не стал изобретать свой логгер, как правило, в каждой фирме он свой. В данном проекте я ограничился выводом в консоль, в режимах Note и Error. Единственно требование, которое я предъявляю к логгеру это то, что он должен поддерживать интерфейс подобный printf. Согласитесь со мной ведь printf это прекрасно. Я лишь добавил макросы для удобного процесса логирования.
logger.h
Output:
#define ENTRY __PRETTY_FUNCTION__
#define LOG_0(s) ;
#define LOG_1(s) Log::note(ENTRY, s)
#define LOG_2(s, p1) Log::note(ENTRY, s, p1)
#define LOG_3(s, p1, p2) Log::note(ENTRY, s, p1, p2)
#define LOG_4(s, p1, p2, p3) Log::note(ENTRY, s, p1, p2, p3)
#define LOG_5(s, p1, p2, p3, p4) Log::note(ENTRY, s, p1, p2, p3, p4)
#define LOG_X(x,s,p1,p2,p3,p4,FUNC, ...) FUNC
#define LOG(...) LOG_X(,##__VA_ARGS__,\
LOG_5(__VA_ARGS__),\
LOG_4(__VA_ARGS__),\
LOG_3(__VA_ARGS__),\
LOG_2(__VA_ARGS__),\
LOG_1(__VA_ARGS__),\
LOG_0(__VA_ARGS__)\
)
LOG("Appication started, version: %s (%s)", VERSION, BUILD_TYPE);
Output:
[N][int main(int, char**)]Appication started, version: 1.0.3 (RELEASE)
Останов
На мой взгляд, корректная остановка, это одна из важнейших функций ПО о которой часто забывают. Как правило, сделать корректную остановку в уже разработанном ПО это непосильная задача. Другое дело, если придерживаться определенной стратегии с самого начала, то это становится пустяком. Я не рассматриваю всякие изощренные способы получения команды останова по сети, по SMS или через спутник. Я просто ловлю некоторые сигналы, после чего инициирую процедуру корректного останова.
void Mediator::wait_exit()
{
LOG("Set up waiting exit");
sigset_t set;
int sig;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGTSTP);
sigprocmask(SIG_BLOCK, &set, NULL);
sigwait(&set, &sig);
switch (sig) {
case SIGINT:
case SIGQUIT:
case SIGTERM:
case SIGTSTP:
LOG("Catched signal to stopping application");
stop();
break;
}
}
Единственное что требуется это вызвать функцию wait_exit() в главном потоке, после выполнения всех активный действий.
LOG("Appication started, version: %s (%s)", VERSION, BUILD_TYPE);
{
Mediator mediator;
mediator.start();
mediator.wait_exit();
}
LOG("Applicatiom stopped");
Спасибо mejedi за указание некорректности использования обработчика сигналов для данных нужд. Надеюсь я правильно истолковал(реализовал) ваше предложение.
Структура приложения
Вот мы и добрались к финальной части. Как уже многим стало ясно, я использую паттерн «Медиатор». Конечно, не во всей его красе, ибо никакой бизнес логики пока нет.
class Mediator: public Thread
{
public:
Mediator();
virtual ~Mediator();
void wait_exit();
private:
virtual void run();
void load_app_configuration();
void create_pid();
private:
Pid pid_;
};
Если какую то работу предполагается выполнять в отдельном потоке, то для такой задачи должен существовать отдельный класс, унаследованный от специального класса Thread.
class Thread
{
public:
void start() {th_ = boost::thread(boost::bind(&Thread::run, this));}
void stop() {th_.interrupt(); th_.join();}
virtual ~Thread(){}
private:
virtual void run() = 0;
private:
boost::thread th_;
};
Цель которого поддержать в едином формате процесс запуска и останова.
Репозиторий
Проект доступен в Google Code, но только для read-only. Это не потому что я жадный, я просто не знаю как открыть доступ для всех. При желании вносить правки, пишите ваш g-email, добавлю вас к проекту.
svn checkout http://project-null.googlecode.com/svn/trunk/ project-null-read-only
Пара слов...
Идея оформить данную наработку в осязаемый шаблон возникла у меня месяца два назад, в процессе очередного проекта который нужно было слобзать быстро и с чистого листа. В ходе написания проекта я пришел к выводу, что это уже не первый раз, когда у меня получается подобная архитектура, и что нет смысла каждый раз вспоминать все подводные камни и приходить к тому же решению, которое использовал раньше, нужно оформить его как шаблон. Я долго мучился на счет целесообразности написания этой статьи, но вспомнив притчу про учителя и стакан воды, я решил, что лучше мне скажут, что статья не стоит и ломаного гроша, чем я буду продолжать думать о ее целесообразности.
Спасибо за внимание.
PS. Добавлены различные проверки PID файла при запуске. Обрабатываются кейсы файл с PID есть — процесса нет, PID файл пустой.