NULL Project

    I don’t know about you, but usually when I need to write something, a fever and a complete prostration of thoughts begin from scratch. Various abstract models already fly in my head, what comes from what and where. But it’s impossible to grab one of them, because you have a blank sheet and tearing one thought out of your head, there’s nothing to apply it to, and you can’t pull out the whole skeleton because you are already thinking about solving the problem, but you just need to write the backbone of the application.

    Below is the “NULL project”, the very backbone with which it usually starts. I have.

    This post is most likely not to be interesting to those who are already seasoned and those who are not directly related to C ++ development, because The material presented below has one single purpose - to give a ready foundation for a start.

    Requirements


    First, I’ll try to highlight a number of requirements that would be nice to implement in this framework
    1. Project folder
    2. CMake build scripts and shell scripts
    3. start and stop scripts
    4. the application should start, work and terminate correctly, the whole process should be logged
    5. You can log into the console, anyway, each project has its own logger, the main thing is that it would be easy to replace it.
    6. the application must parse the command line
    7. the application should be able to parse a 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, we do it with exceptions.
    10. make 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
    

    Solyushin generator


    The purpose of the gen_eclipse.sh script is to prepare the folder structure and invoke cmake to generate debug and release solutions. And also set the current version of the project. It so happened that development on Linux systems is usually conducted in the Eclipse environment, hence the name gen_eclipse. But to fully make friends Cmake and Eclipse I did not succeed. In order to open the generated project in Eclipse, you need to import an existing MAKE project, 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_Dif [ -d $BUILD_DIR_R ]; thenif [ -f $BUILD_DIR_R/CMakeCache.txt ]; then
    		rm $BUILD_DIR_R/CMakeCache.txt
    	fifiif [ -d $BUILD_DIR_D ]; thenif [ -f $BUILD_DIR_D/CMakeCache.txt ]; then
    		rm $BUILD_DIR_D/CMakeCache.txt
    	fifiecho"[[ Generate Release solution]]"cd$BUILD_DIR_R
    cmake -G "Eclipse CDT4 - Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING="Release" --build $BUILD_DIR_R ../../
    echoecho"[[ 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 worth noting is that I use Subversion and rely on revision numbers as versions. I usually adhere to the following version format: MAJOR.MINOR.REVISION. The first two values ​​are set by handles, the third is svn revision. As far as I know, the subversion client is not able to 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" == "" ]]; thenecho"Cannot recognize number of revision"exit 1
    fi
    ...
    VER_CPP=src/version.cpp
    echo"#include \"version.h\"" > $VER_CPPecho"const char* VERSION = \"$VERSION\";" >> $VER_CPP

    Scripts start, break


    As a rule, all the software that had to be written under Linux, these were 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 never had a single case where only one version of a service would be launched on a single server. Therefore, I adhere to the practice of start stop scripts with control over 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 ]; thenecho"File '$APP_PID' exist but it's empty, delete it"
                    rm $APP_PIDelif ! ps h -p $PID > /dev/null; thenecho"File '$APP_PID' exist but process with pid '$PID' doesn't exist, delete it"
                    rm $APP_PIDelseecho"$APP_NAME already started (file $APP_PID exist)"exitfifi
    mkdir -p $APP_LOG_DIRif [ $APP_EXPORT_LIB_DIR ]; thenexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$APP_EXPORT_LIB_DIR; fiecho =========================================== >> $MAIN_LOG
    date >> $MAIN_LOGif [ -f $APP_BIN ]; then
            ./$APP_BIN -l $APP_LOG_DIR -c $APP_CONF -p $APP_PID >> $MAIN_LOG &
    elseecho"Error: binary file '$APP_BIN' doesn't exist"exit 1
    fiif [[ $? != 0 ]]; thenecho"Not started"elseecho"Started"fi


    The shutdown script has much more sophisticated logic for slow server shutdown.
    stop.sh
    #!/bin/bash
    source init.conf
    if [ ! -f $APP_PID ]; thenecho"'$APP_NAME' not started (file $APP_PID doesn't exist)"exitfi
    PID=`cat $APP_PID`
    if ! ps h -p $PID > /dev/null
    thenecho"'$APP_NAME' not started, removing old $APP_PID file"
            rm $APP_PIDexitfiif ! kill -s SIGTERM $PIDthenecho"Cannot stop process"exitfifor i in {1..10}
    doif ps h -p $PID > /dev/null
            thenecho -n .
                    sleep 1
            elseecho"Stopped"exitfidoneechoecho"Can't correctly stop application, finish him"kill -9 $PID
    rm $APP_PID


    PS. Thanks to my colleague Andrei for offering a more usable version of the stop.sh script

    Package.sh


    In each of the projects, I have a package.sh script whose purpose is to create a sufficient installation package. Typically, this is an archived application folder with a set of files sufficient for the application to work. The minimum set is break start scripts, a configuration file, the application itself, and a 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_FOLDERfi
    mkdir $TEMP_FOLDERfor i in"${FILES[@]}"doecho"copy '$i'"
            cp $i$TEMP_FOLDERdoneecho creat $LOG_DIR
    mkdir $TEMP_FOLDER/$LOG_DIR
    tar -cjf $PACKAGE$TEMP_FOLDER
    rm -rf $TEMP_FOLDERecho Finished
    


    Functional


    And so, what do I usually need in order to proceed directly to programming:
    1. First priority command line options
    2. configuration file
    3. A simple way to interact with the logger
    4. The ability to correctly stop the application

    Let's start in order:

    First priority command line options


    I have identified three such parameters for myself. 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 because errors that I want to log may already occur during parsing of the configuration file. Why a directory? I'm used to the fact that each launch is a separate log file, so it is easier to delete old logs.
    Configuration file If not through the command line, then how? Especially if you have several configurations that you want to switch quickly.
    PID file.The only reason why I do not store it in the configuration file is that this parameter is used immediately in 2 places. In start and stop scripts. And it’s much easier to put it in a separate init file, which connects to start stop scripts, and edit it once than two (I'm talking about a conf file).

    Parsing the command line and configuration file by 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 a default value.
    ./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 invent my own logger, as a rule, in each company it is its own. In this project, I limited myself to outputting to the console in Note and Error modes. The only requirement that I make for the logger is that it must support an interface like printf. Agree with me because printf is great. 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, a correct stop is one of the most important software functions that is often forgotten. As a rule, to make a correct stop in already developed software is an impossible task. Another thing, if you adhere to a certain strategy from the very beginning, it becomes a trifle. I do not consider all sorts of sophisticated ways to get a stop command over the network, via SMS or via satellite. I just catch some signals, after which I initiate the correct stop procedure.
    void Mediator::wait_exit()
    {
    	LOG("Set up waiting exit");
    	sigset_tset;
    	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 all active actions have been completed.
    	LOG("Appication started, version: %s (%s)", VERSION, BUILD_TYPE);
    	{
    		Mediator mediator;
    		mediator.start();
    		mediator.wait_exit();
    	}
    	LOG("Applicatiom stopped");
    

    Thanks to majedi for pointing out the incorrect use of the 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, for there is no business logic yet.
    classMediator:public Thread
    {
    public:
    	Mediator();
    	virtual ~Mediator();
    	voidwait_exit();
    private:
    	virtualvoidrun();
    	voidload_app_configuration();
    	voidcreate_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.
    classThread
    {public:
    	voidstart(){th_ = boost::thread(boost::bind(&Thread::run, this));}
    	voidstop(){th_.interrupt(); th_.join();}
    	virtual ~Thread(){}
    private:
    	virtualvoidrun()= 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. This is not because I'm greedy, I just don’t know how to open access for everyone. If you want to make changes, write your g-email, I will 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 make this experience into a tangible template arose about two months ago, in the process of the next project, which needed to be quickly and cleaned from scratch. During the writing of the project, I came to the conclusion that this is not the first time that I get such an architecture, and that it makes no sense to recall all the pitfalls every time and come to the same solution that I used before, you need to design it as a template. For a long time I was tormented by the advisability of writing this article, but remembering the parable about the teacher and a glass of water, I decided that it would be better to tell me that the article was not worth a penny than I would continue to think about its advisability.

    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.

    Also popular now: