以下は、通常開始するまさにバックボーンである「NULLプロジェクト」です。 私と。
この投稿は、すでに経験が豊富な人や、C ++開発に直接関係のない人にとっては興味深いものではありません。 以下に示す資料には、1つの目的があります-開始の準備を整えるための基盤を提供することです。
必要条件
手始めに、このフレームワークに実装するのに適した多くの要件を強調してみます。
- プロジェクトフォルダ
- CMakeビルドスクリプトとシェルスクリプト
- スクリプトの開始と停止
- アプリケーションが正しく起動、動作、終了し、プロセス全体がログに記録されます
- とにかく、各プロジェクトには独自のロガーがあり、コンソールにログインできます。主なことは、簡単に置き換えることができるということです。
- アプリケーションはコマンドラインを解析する必要があります
- アプリケーションは、key = valueという形式の構成ファイルを解析できる必要があります。
- ブーストなしのプロジェクト? いいえ、聞いていません。 すぐにブーストを統合
- エラー処理。 これはバックボーンにすぎないため、実際にはパフォーマンスはないため、例外を設けて実行しています。
-
ワールドキャプチャ機能を作成する
プロジェクトフォルダ
. ├── 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を呼び出してデバッグおよびリリースソリューションを生成することです。 また、プロジェクトの現在のバージョンを設定します。 Linuxシステムでの開発は通常、Eclipse環境で行われるため、gen_eclipseという名前が付けられました。 しかし、CmakeとEclipseを完全に友達にするために、私は成功しませんでした。 生成されたプロジェクトをEclipseで開くには、既存のMAKEプロジェクトをリリースまたはデバッグしてインポートし、コンテキストメニューから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。 最初の2つの値はハンドルによって設定され、3番目は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ディレクトリに開始スクリプトと停止スクリプトを置くことが一般的であることを知っています。 しかし! 単一のサーバーでサービスの1つのバージョンのみが起動されるという単一のケースはありません。 そのため、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スクリプトのより使いやすいバージョンを提供してくれた同僚のAndreiに感謝します。
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
機能的
したがって、プログラミングに直接進むために通常必要なものは次のとおりです。
- 最優先のコマンドラインオプション
- 構成ファイル
- ロガーと対話する簡単な方法
- アプリケーションを正しく停止する機能
順番に始めましょう:
最優先のコマンドラインオプション
私はこのような3つのパラメーターを自分で特定しました。 次に、それらがなぜであるかを説明しようとします。
ロギング用のディレクトリ。 このパラメーターを構成ファイルに保存しないのは、構成ファイルの解析中にログに記録するエラーが既に発生している可能性があるためです。 なぜディレクトリなのか? 私は、各起動が個別のログファイルであるという事実に慣れているので、古いログを削除する方が簡単です。
構成ファイルコマンドラインを介していない場合、どのように? 特に、迅速に切り替えたいいくつかの構成がある場合。
PIDファイル。 構成ファイルに保存しない唯一の理由は、このパラメーターが2つの場所ですぐに使用されるためです。 起動および停止スクリプト。 また、別のinitファイルに保存して、開始停止スクリプトに接続し、2回よりも1回編集する方がはるかに簡単です(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
許可されるオプション:
-h [--help]ヘルプメッセージを生成します
-c [--config] arg(= project.conf)設定ファイルの設定
-l [--logdir] arg(= logs)ログディレクトリを設定します
-p [--pidfile] arg(= project.pid)pidファイルを設定
ロギング
原則として、各企業で独自のロガーを発明しませんでした。 このプロジェクトでは、ノートモードとエラーモードでコンソールに出力することに限定しました。 ロガーに対して行う唯一の要件は、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);
出力:
[N] [int main(int、char **)] Appicationが開始されました。バージョン:1.0.3(リリース)
やめて
私の意見では、正しい停止はしばしば忘れられる最も重要なソフトウェア機能の1つです。 原則として、すでに開発されたソフトウェアを正しく停止することは不可能な作業です。 もう1つは、特定の戦略を最初から順守していれば、些細なことです。 ネットワーク経由で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");
これらのニーズに対するシグナルハンドラの不適切な使用を指摘してくれたmajediに感謝します。 あなたの提案を正しく解釈(実装)したいと思います。
アプリケーション構造
それで最後の部分に行きました。 すでに多くの人に明らかになっているので、「メディエーター」パターンを使用します。 もちろん、その栄光のすべてではありません。ビジネスロジックがまだないからです。
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で利用できますが、読み取り専用のみです。 これは私が貪欲だからではなく、誰にでもアクセスを許可する方法がわからないだけです。 変更したい場合は、g-メールを書いてください。プロジェクトに追加します。
svn checkout http://project-null.googlecode.com/svn/trunk/ project-null-read-only
いくつかの言葉...
この経験を具体的なテンプレートにするアイデアは、約2か月前に、次のプロジェクトのプロセスで生まれました。このプロジェクトは迅速に一から作成する必要がありました。 プロジェクトの執筆中に、私はそのようなアーキテクチャを取得するのはこれが初めてではなく、毎回すべての落とし穴を思い出して以前に使用したのと同じソリューションに来ることは意味がないという結論に達しました。テンプレートとして設計する必要があります。 長い間、私はこの記事を書くことの賢明さに苦しんでいましたが、先生と水のコップについてのたとえを思い出して、私はその記事がその賢明さについて考え続けるよりも一銭も価値がないと言った方が良いと決めました。
ご清聴ありがとうございました。
PS。 起動時にPIDファイルのさまざまなチェックを追加しました。 ケースはPIDファイルで処理されます-プロセスはなく、PIDファイルは空です。