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.
First, I’ll try to highlight a number of requirements that would be nice to implement in this framework
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.
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
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.
The shutdown script has much more sophisticated logic for slow server shutdown.
PS. Thanks to my colleague Andrei for offering a more usable version of the stop.sh script
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.
And so, what do I usually need in order to proceed directly to programming:
Let's start in order:
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
Each parameter has a default value.
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.
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.
The only thing that is required is to call the wait_exit () function in the main thread, after all active actions have been completed.
Thanks to majedi for pointing out the incorrect use of the signal handler for these needs. I hope I correctly interpreted (implemented) your proposal.
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.
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.
The purpose of which is to support the start and stop process in a single format.
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.
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.
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
- Project folder
- CMake build scripts and shell scripts
- start and stop scripts
- the application should start, work and terminate correctly, the whole process should be logged
- 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.
- the application must parse the command line
- the application should be able to parse a configuration file of the form key = value
- project without boost? no, have not heard. So immediately integrate boost
- error processing. Since this is only the backbone and here, in fact, there is no performance, we do it with exceptions.
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:
- First priority command line options
- configuration file
- A simple way to interact with the logger
- 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
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)
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.