Проект NULL

в 19:18, , рубрики: Песочница, проектирование, С++, метки: ,

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

Ниже представлен «проект NULL», тот самый костяк, с которого обычно все и начинается. У меня.

Данный пост скорее всего не будет интересен тем кто уже матерый и тем кто на прямую не связан с разработкой на С++, т.к. ниже представленные материл несет одну единственную цель — дать готовый фундамент для начала.

Требования

Попробую для начала выделить ряд требований которые было бы неплохо реализовать в этом костяке

  1. Проектная папка
  2. скрипты сборки CMake и скрипты для генерации солюшинов
  3. скрипты запуска и останова
  4. приложение должно стартовать, работать и корректно завершаться, весь этот процесс должен логироваться
  5. логировать можно и в консоль, все равно в каждом проекте свой логгер, главное, что бы его было легко заменить.
  6. приложение должно разбирать командную строку
  7. приложение должно уметь разбирать конфигурационный файл вида key = value
  8. проект без boost? не, не слышал. Так что сразу интегрируем boost
  9. обработка ошибок. Так как это только костяк и тут, по сути, ни какого перформенса нет, то делаем на исключениях.
  10. делаем функцию захвата мира

Проектная папка

.
├── 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=`svn info | grep Revision | sed s/Revision: //`

if [[ "$REVISION" == "" ]]; then
	REVISION=`svn info | grep Редакция: | sed s/Редакция: //`
fi

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

if [ -f $PID_FILE ]; then
	echo "$APP_NAME already started (file $PID_FILE already exist)"
	exit
fi

mkdir -p $LOG_DIR

LOG_FILE=$LOG_DIR/start.log

echo =========================================== >> $LOG_FILE
date >> $LOG_FILE
./projectnull -l $LOG_DIR -c $CONF_FILE -p $PID_FILE >> $LOG_FILE &

Скрипт останова имеет куда более изощренную логику, для медленного останова сервера.

stop.sh

#!/bin/bash

source init.conf

if [ ! -f $PID_FILE ]; then
	echo "$APP_NAME not started (file $PID_FILE doesn't exist)"
	exit
fi

PID=`cat $PID_FILE`

kill -s SIGINT $PID

WAIT=0
while [ ! "$WAIT" = "30" ]; do
	STOPPED=`ps -p $PID | grep $PID`
	if [ "$STOPPED" = "" ];then
		echo "$APP_NAME stopped"
		exit
	else
		echo -n .
		sleep 1
		let "WAIT += 1"
	fi
done

echo
echo "Can't correctly stop $APP_NAME, finish him!"
kill -9 $PID
rm -f $PID_FILE

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

Функционал

И так, что же мне обычно нужно, что бы приступить непосредственно к программированию:

  1. Параметры командной строки первой важности
  2. Файл конфигурации
  3. Простой способ взаимодействия с логгером
  4. Возможность для корректной остановки приложения

Начнем по порядку:

Параметры командной строки первой важности

Я выделил для себя три таких параметра. Сейчас я попробую объяснить, почему именно они.
Директория для логирования. Причина, по которой я не храню этот параметр в конфигурационном файле, это то, что в процессе разбора конфигурационного файла уже могут случиться ошибки, которые я хочу логировать. Почему директория? Я привык, что каждый запуск это отдельный лог файл, таким образом, легче удалять старые логи.
Конфигурационный файл Если не через командную строку, то как? Особенно если у вас несколько конфигураций, которые вы хотите быстро переключать.
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

#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 или через спутник. Я просто ловлю некоторые сигналы, после чего инициирую процедуру корректного останова.

/*
 * Инициирует останов Mediator, вызывая Mediator::exit()
 */
void exit_sig_handler(int sig);

/*
 * Устанавливаем обработчик exit_sig_handler для сигналов
 * SIGINT(1) - сочетание клавиш Ctrl+C
 * SIGTERM(15) - «вежливое» завершение программы
 * SIGTSTP(20) - сочетание клавиш Ctrl+Z 
 */
void set_exit_sig_handlers();

Единственное что требуется в начале, это вызвать функцию set_exit_sig_handlers(), после запуска главного потока (не путайте с main, я говорю о вашем главном потоке, т.е. thread который вы стартовали для работы в фоне)

LOG("Appication started, version: %s (%s)", VERSION, BUILD_TYPE);
	{
		Mediator mediator;
		mediator.start();
		
		set_exit_sig_handlers();
		mediator.wait_exit();
	}
	LOG("Applicatiom stopped");

Структура приложения

Вот мы и добрались к финальной части. Как уже многим стало ясно, я использую паттерн «Медиатор». Конечно, не во всей его красе, ибо ни какой бизнес логики пока нет.

mediator.h

class Mediator: public Thread
{
public:
	Mediator();
	virtual ~Mediator();

	void wait_exit();
	static void exit();

private:
	virtual void run();

	void load_app_configuration();
	void create_pid();

private:
	static bool exit_;
	static boost::mutex exit_mtx_;
	static boost::condition_variable exit_cv_;

	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_;
};
Цель которого поддержать в едином формате процесс запуска и останова.

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

void Mediator::wait_exit()
{
	LOG("wait exit");

	boost::unique_lock<boost::mutex> lock(exit_mtx_);
	while (!exit_)
		exit_cv_.wait(lock);

	LOG("stop application");
	stop();
}

void Mediator::exit()
{
	LOG("set exit");
	{
		boost::lock_guard<boost::mutex> lock(exit_mtx_);
		exit_ = true;
	}
	exit_cv_.notify_one();
}

где void Mediator::exit(), та самая статическая функция вызываемая из обработчика сигналов.

void exit_sig_handler(int sig)
{
	Mediator::exit();
}

Репозитарий

Проект доступен в Google Code, но только для read-only. Это не потому что я жадный, я просто не знаю как открыть доступ для всех. При желании вносить правки, пишите ваш g-email, добавлю вас к проекту.

svn checkout http://project-null.googlecode.com/svn/trunk/ project-null-read-only

Пара слов...

Идея оформить данную наработку в осязаемый шаблон возникла у меня месяца два назад, в процессе очередного проекта который нужно было слобзать быстро и с чистого листа. В ходе написания проекта я пришел к выводу, что это уже не первый раз, когда у меня получается подобная архитектура, и что нет смысла каждый раз вспоминать все подводные камни и приходить к тому же решению, которое использовал раньше, нужно оформить его как шаблон. Я долго мучился на счет целесообразности написания этой статьи, но вспомнив притчу про учителя и стакан воды, я решил, что лучше мне скажут, что статья не стоит и ломаного гроша, чем я буду продолжать думать о ее целесообразности.

Спасибо за внимание.

PS. Я очень долго вычитывал эту стать и прогонял ее через Word, но я все равно уверен, что в ней остались ошибки. Прошу отнестись с пониманием, все предложения буду учтены.

Автор: Cupper

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js