πŸ“œ ⬆️ ⬇️

NULL project

I don’t know about you, but I usually have a fever and complete prostration in my thoughts when I need to write something from scratch. In my head various abstract models are already flying, from what and where. But you can’t grasp for any of them, because you’ve got a clean sheet and pulled one thought out of my head, there’s nothing to apply it to, and you’re not able to pull out the whole skeleton because you are already thinking about solving the problem, but you still need write the backbone of the application.

Below is the β€œNULL project”, the very backbone with which everything usually begins. I have.

This post most likely will not be of interest to those who are already experienced and those who are not directly involved in C ++ development, since The material presented below carries one single goal - to give a ready-made foundation to begin with.

Requirements


I will try to highlight for the beginning a number of requirements that it would be nice to implement in this core
  1. Project folder
  2. CMake build scripts and scripts for generating solyushinov
  3. start and stop scripts
  4. the application should start, work and exit correctly, the whole process should be logged
  5. you can log in to the console, anyway, each project has its own logger, the main thing is that it would be easy to replace.
  6. the application should parse the command line
  7. the application should be able to parse the configuration file of the form key = value
  8. project without boost? no, have not heard. So immediately integrate boost
  9. error processing. Since this is only the backbone and here, in fact, there is no performance, then we do on exceptions.
  10. do the world capture function

Project folder


. β”œβ”€β”€ 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 

Solyushinov generator


The goal of the gen_eclipse.sh script is to prepare a folder structure and call cmake to generate debug and release solos. And also set the current version of the project. It so happened that I usually develop on Linux systems in the Eclipse environment, hence the name gen_eclipse. But I did not succeed in fully fostering Cmake and Eclipse. In order to open a generated project in Eclipse, you need to import an existing MAKE project, with either release or debug, and add links to the include and src directories via the context menu.
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 


Version


The first thing to note is that I use Subversion and rely on revision numbers as versions. I usually stick to the following version format: MAJOR.MINOR.REVISION. The first two values ​​are given by pens, the third is the svn revision. As far as I know, the subversion client cannot return just the revision number, so I use the following mechanism
 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 

Startup, Stop Scripts


As a rule, all the software that had to be written under Linux was servers, large and small. Their peculiarity is that they work in the background, these are services. I know that for such things it is customary to have start and stop scripts in the init.d directory. But! I have not had a single case where only one version of the service would be started on one server. Therefore, I adhere to the practice of start stop scripts with control on the PID file.
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 


The shutdown script has a much more sophisticated logic, for slow shutdown of the server.
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. Thanks to my colleague Andrew for offering a more usable version of the stop.sh script

Package.sh


In each of the projects I have a script package.sh whose goal is to create an adequate installation package. As a rule, this is an archived application folder with a set of files sufficient for the application to work. The minimum set is the stop start scripts, the configuration file, the application itself, and the folder for logs.
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 


Functional


And so, what I usually need in order to proceed directly to programming:
  1. Command Line Parameters
  2. configuration file
  3. A simple way to interact with the logger
  4. The ability to stop the application correctly

Let's start in order:

Command Line Parameters


I selected for myself three such parameters. Now I will try to explain why they are.
Directory for logging. The reason why I do not store this parameter in the configuration file is that during the parsing of the configuration file errors that I want to log can happen. Why a directory? I'm used to the fact that each launch is a separate log file, thus, it is easier to delete old logs.
Configuration file If not through the command line, how? Especially if you have several configurations that you want to quickly switch.
PID file. The only reason why I do not store it in the configuration file is that this parameter is used in 2 places at once. In start and stop scripts. And it is much easier to put it into a separate init file, which is connected to start stop scripts, and edit it once, rather than two (I’m talking about the conf file).
')
Analysis of the command line and the configuration file is done by means of 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 ; } 


Each parameter has default values.
./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

Logging


I did not begin to invent the logger, as a rule, in each firm it the. In this project, I limited myself to outputting to the console, in the Note and Error modes. The only requirement I place on the logger is that it must support an interface like printf. Agree with me because printf is wonderful. I just added macros for a convenient logging process.
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)


Stop


In my opinion, the correct stop is one of the most important functions of the software which is often forgotten. As a rule, to make a correct stop in an already developed software is an impossible task. Another thing, if you stick to a certain strategy from the very beginning, then it becomes a trifle. I do not consider all sorts of sophisticated ways to get a shutdown command over the network, via SMS or via satellite. I just catch some signals, then initiate the procedure for a correct stop.
 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; } } 

The only thing that is required is to call the wait_exit () function in the main thread, after performing all the active actions.
  LOG("Appication started, version: %s (%s)", VERSION, BUILD_TYPE); { Mediator mediator; mediator.start(); mediator.wait_exit(); } LOG("Applicatiom stopped"); 

Thanks to mejedi for pointing out the incorrect use of a signal handler for these needs. I hope I correctly interpreted (implemented) your proposal.

Application structure


So we got to the final part. As it has already become clear to many, I use the β€œMediator” pattern. Of course, not in all its glory, because no business has logic yet.
 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_; }; 

If some work is supposed to be performed in a separate thread, then for such a task there should be a separate class inherited from the special class 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_; }; 

The purpose of which is to support the start and stop process in a single format.

Repository


The project is available in Google Code, but only for read-only. It's not because I'm greedy, I just don't know how to open access for everyone. If you wish to make changes, write your g-email, add you to the project.
 svn checkout http://project-null.googlecode.com/svn/trunk/ project-null-read-only 

A couple of words...


The idea to arrange this development in a tangible pattern came to me about two months ago, in the process of the next project that needed to be installed quickly and from scratch. During the writing of the project, I came to the conclusion that this is not the first time that I have a similar architecture, and that it makes no sense to remember all the pitfalls each time and come to the same solution that I used before, you need to arrange it as a template. I suffered for a long time at the expense of writing this article, but remembering the parable about the teacher and a glass of water, I decided that they would better tell me that the article was not worth a penny than I would continue to think about its expediency.

Thanks for attention.

Ps. Added various PID file checks at startup. Cases are processed with a PID file - there is no process, the PID file is empty.

Source: https://habr.com/ru/post/158985/


All Articles