Introduction
This article presents the new version of proposed library ‘Application’ to boost.org (0.4.6), and shows how you can build long time running applications (e.g. Server Application).
Basic Setup
- The first step is to download the last version of boost and build it.
The build process of boost is not very easy, but a good documentation is provided, refer to this link.
Or you can check this good article to help with build.
- The second step is to download:
You can unzip each of it on your ‘c:\’ or other directory. We will configure Visual Studio Project to find these directories.
Caution
The Boost.Application
is not yet an official Boost C++ library. It wasn't reviewed and can't be downloaded from www.boost.org. This beta is available to boost community to know the real interest and get comments for refinement. The intention is to submit the library for formal review, if the community thinks that it is interesting.
Boost.Application Resources
You can download the library from GitHub here.
An online documentation (under construction) is available here.
Motivation
Sometimes, we need to run application for long time (e.g. Server Application). We need a mechanism to extend this application on runtime, and we need some facilities to access paths, ensure single instance instantiation on system, manage and catch signals and so on.
This work is recurrent each time that we need build and deploy an application for a particular system, and the way to do this, change a lot on each of these systems.
For instance, on Windows side, we have ‘services’ and on Unix (POSIX) side, we have daemons that are used to build long-running executable applications (e.g. server) and these two APIs have no similarity.
Thus, in this scenario, it is so difficult for the developer to have his/her application running on Windows or on POSIX as server without a lot of work. The work is harder if we need to run the same application in both systems.
Other problem is raised when user wants to provide a way to extend your application using a plug-in mechanism. Like Service/Daemon, the shared modules (DSO) manipulation changes a lot on Windows and POSIX.
Obtaining simple things like paths, arguments, manipulate signal can be annoying, since it also does not have a common interface to do this on both systems.
Application
The ‘application’ library provides a cross-platform C++ way to do a service/daemon, provides a mechanism to help with a plug-in and provide a way to do annoying things using ‘aspects’ and other things.
Our Sample
Sometimes, a common task executes or checks something at specified intervals of time (e.g. unix cron). This can be known as time-based job (task) scheduler.
We will use ‘application’ to build a server application that processes a timer event as tasks on plug-ins (our jobs). Thus our application will have a timer that will expire and trigger tasks that are on a plug-in
At the end, if you are on Windows, you will have a service that loads a ‘.dll’ on runtime, and if you are on POSIX, you will have a Daemon that loads a ‘.so’ on runtime and trigger a task of it.
When our timer (asio) expires, we will scan a folder (filesystem), and look for plug-ins (application) on it, and then execute the job inside of the plug-ins.
Sample Business Logic and Used Boost Libraries
Or sample will contact a web service (api.openweathermap.org) to get weather information.
We are build a server for Windows, then we will use Boot.Program_Options
to handle service installation, see details below on ‘Project Sample and Development environment’.
The Web API returns a ‘json’ response, then we will use Boost.Property_Tree
and Boost.Foreach
to handle this response.
We will compare the “humidity” value that Web API returns and log it on text file if it is less than 80%.
The plugin uses a ‘sync
’ Boost.Asio
sample client to connect on web API. User can implement ‘async
‘ plugin as exercise, if desired.
We will need to use Boost.Filesystem
, Boost.Bind
, Boost.Function
, Boost.System
to do some things.
So, in our sample, we will use the following Boost Libraries:
Boost.Asio
Boost.Property_Tree
Boost.Foreach
Boost.Program_Options
Boost.Filesystem
Boost.Bind
Boost.Function
Boost.System
Project Sample and Development Environment
I will use Windows and Visual Studio as a development platform due to its popularity. But as I said earlier, the code compiles on POSIX generating a daemon without modifications.
In the download link, you will find a Visual Studio 2013 project. If you use POSIX, you will need to create your own make/jam file to compile the project. If you have any difficulties in this process, please enter in contact then I can help you.
Note that you need to change the paths of project to reflect your environment; in my case, I have this:
C/C++ | General
Linker | General
1. Basic
Boost.Application
requires a ‘Functor
’ class that will be used to model the concept of an “application”, thus, before your ‘main
’ function, you need to add your ‘functor
’ class, like this:
class time_based_job : boost::noncopyable
{
public:
int operator()()
{
return 0;
}
};
int main(int argc, char *argv[])
{
time_based_job app;
return 0;
}
2. Context and Aspects
Boost.Application
has a concept of ‘aspects’ to allow easy extension, customization and make things more flexible.
Any C++ class can be an ‘aspect’, and you will use an ‘application context’ to have access to applications ‘aspects’, thus this takes us to another concept: the ‘application context’.
Note that Boost.Application
provides a lot of ready-to-use aspects, and we will use some of these ‘aspects’ in our sample.
The ‘application context’ is like a container that you can add your ‘application aspects’. The ‘application context’ can be accessed using 2 ways:
- As function parameter
- As a global object (singleton)
The user is free to choose the best option to your application design. Here we will use ‘global_context
’, so let’s add it to our source file.
inline global_context_ptr this_application_cxt() {
return global_context::get();
}
class time_based_job : boost::noncopyable
{
public:
int operator()()
{
return 0;
}
};
int main(int argc, char *argv[])
{
time_based_job app;
global_context::create();
boost::shared_ptr<void> global_context_destroy(nullptr, [&](void*) {
global_context::destroy();
});
return 0;
}
The ‘global_context::create
’ will create our context, and we use a trick to delete our context when main
ends (exit of scope). To do this, we use ‘shared_ptr
’.
3. Ready to Use Aspects
Now, we will add 2 ‘ready to use’ aspects that we will need to our ‘application context’. The first aspect will help us to handle ‘args
’ and another is to help us with ‘paths
’.
Add after: ‘// 1
’ ;
this_application_cxt()->insert<args>(
boost::make_shared<args>(argc, argv));
this_application_cxt()->insert<path>(
boost::make_shared<path_default_behaviour>(argc, argv));
4. Launch Application
Boost.Application
supports basically 2 flavors of applications that can be:
- Common Application
This kind of application is a usual Interactive Terminal application.
- Server Application
This kind of application generates a Service (Windows), or a background process/Daemon (Unix).
Note that other kinds of application can be added be user. Refer to this article that extends Boost.Application
to support ‘Apache Http Module’ application mode.
Now we will launch a ‘Common Application’. Add after: ‘// 2
’ ;
boost::system::error_code ec;
return launch<common>(app, this_application_cxt(), ec);
Note that Boost.Application
can handle error setting ‘boost::system::error_code
’ or use 'exception version' that throws an exception of type boost::system::system_error
.
At this point, we have a complete skeleton of application.
5. Buid an Aspect
Our application will process a timer event as tasks, thus now we will model the behavior on one aspect that we will add on our application context.
template< typename Handler >
class timer_job : boost::noncopyable
{
typedef handler<bool>::global_context_callback job_callback;
public:
timer_job(Handler h, unsigned int timeout)
: jtimer_(io_service_), timeout_(timeout), job_(h)
{
trigger();
boost::thread(
boost::bind(&boost::asio::io_service::run,
boost::ref(io_service_))).detach();
}
void stop()
{
io_service_.stop();
}
protected:
bool job()
{
if(job_())
return true;
trigger();
return false;
}
void trigger()
{
jtimer_.expires_from_now(boost::posix_time::seconds(timeout_));
jtimer_.async_wait(
[this](const boost::system::error_code& ec) -> void {
if(!ec) {
if(this->job()) {
io_service_.stop();
}
}
else
io_service_.stop();
}
);
}
private:
unsigned int timeout_;
job_callback job_;
boost::asio::io_service io_service_;
boost::asio::deadline_timer jtimer_;
};
typedef timer_job< handler<bool>::global_context_callback > timer_callback;
In this class (aspect), we use ‘asio::deadline_timer
’ to schedule and call a callback method that will do our required action.
6. Add Our Handmade Aspect to Context
To do this, we need to add our callback handler to our functor class, like this:
class time_based_job : boost::noncopyable
{
public:
int operator()()
{
return 0;
}
bool doit()
{
return false;
}
};
And add the below code after ‘path
’ aspect on main.
this_application_cxt()->insert<
timer_callback >(boost::make_shared<
timer_callback >(
handler<bool>::make_global_callback(app, &time_based_job::doit), 5));
Boost Application has a class that we can use to make a callback (handler<bool>::make_global_callback
). Here, we use it to bind our timer event to ‘doit
’ on our functor
class.
Thus, when time event expires, it will call “doit
” on functor class.
Note that the time event will expire in 5 seconds.
7. Make our Plug-in
A plugin is a shared library that exports some functions (C function) that gives access to our behavior, see:
The first thing is create our plugin project, refer to sample file for details. Basically it is a “Win32 Console Application (DLL)” Project type.
Now, we will create a file called “job_plugin_api.hpp”, this represents a plugin API interface. In this case, we will build a plugin that will “call a web service” using a sync Boost.Asio
interface.
class my_plugin_api
{
public:
virtual ~my_plugin_api(){};
virtual float version() = 0;
virtual bool do_the_job(
const std::string& query, const std::string& resource,
std::string& result, std::string& err) = 0;
};
The implementation goes on “job_plugin_library.cpp”, here the code is based on Boost.Asio
sample:
http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/example/cpp03/http/client/sync_client.cpp.
I will comment only the plugin access interface, which is that:
my_plugin_api* create_my_plugin(void)
{
my_plugin_api *myplugin = new my_job_plugin();
return myplugin;
}
void delete_my_plugin(my_plugin_api* myplugin)
{
delete myplugin;
}
If we respect this interface, we can have other plug-ins, e.g.: here we use Boost.Asio
“sync
” sample, but we can make another plugin that uses Boost.Asio
“async
” sample:
http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/example/cpp03/http/client/async_client.cpp
8. Load our Plug-in using Boost.Application "Shared Library Class"
Now, we will modify our “doit
” method like this:
bool doit()
{
boost::shared_ptr<path> pt
= this_application_cxt()->find<path>();
boost::filesystem::path scan_dir(pt->executable_path());
std::vector<boost::filesystem::path> v;
std::copy(boost::filesystem::directory_iterator(scan_dir),
boost::filesystem::directory_iterator(), back_inserter(v));
int count_synced_files = 0;
for (std::vector<boost::filesystem::path>::const_iterator it
(v.begin()); it != v.end(); ++it)
{
boost::filesystem::path p(*it);
std::string base = boost::to_lower_copy(p.extension().string());
if( base == shared_library::suffix() )
{
std::cout << "our plugin: " << p.string() << std::endl;
my_plugin_api* plugin = NULL;
shared_library sl(library(p.string()));
if(sl.search_symbol(symbol("create_my_plugin")))
{
plugin = ((pluginapi_create)sl(symbol("create_my_plugin")))();
}
if(plugin != NULL)
{
boost::shared_ptr<void> delete_plugin(nullptr, [&](void*) {
((pluginapi_delete) sl(symbol("delete_my_plugin")))(plugin);
});
std::cout << "Plugin Version: " << plugin->version() << std::endl;
std::string query = "api.openweathermap.org", resource = "/data/2.5/find?q=Americana,br&mode=json", aswer, err;
if(plugin->do_the_job(query, resource, aswer, err))
std::cout << "Error from plugin: " << err << std::endl;
else
{
pt::ptree json_reponse; std::istringstream is(aswer);
read_json(is, json_reponse);
BOOST_FOREACH(boost::property_tree::ptree::value_type& v, json_reponse.get_child("list"))
{
if(v.second.get<int>("main.humidity") < 80)
{
weather_log_ << v.second.get<std::string>("dt") << ":" << v.second.get<std::string>("main.humidity") << std::endl;
}
}
}
}
}
}
return false;
}
Well, here we have a lot of code.
Here we use “boost::filesystem
” to scan folder to get all shared modules. To do that, we use the aspect:
boost::shared_ptr<path> pt
= this_application_cxt()->find<path>();
boost::filesystem::path scan_dir(pt->executable_path());
That gives us the path of our executable module.
The “shared_library::suffix()
” return a string
‘dll
’ on windows and ‘so
’ on unix.
Then we use “application::shared_library
” to load a plugin, check if specific simbol is provided, and call it.
shared_library sl(library(p.string()));
if(sl.search_symbol(symbol("create_my_plugin")))
{
plugin = ((pluginapi_create)sl(symbol("create_my_plugin")))();
}
The plugin response is a json string
that we ‘read’ using Boost.Property_Tree
:
pt::ptree json_reponse; std::istringstream is(aswer);
read_json(is, json_reponse);
BOOST_FOREACH(boost::property_tree::ptree::value_type& v, json_reponse.get_child("list"))
{
if(v.second.get<int>("main.humidity") < 80)
{
weather_log_ << v.second.get<std::string>("dt") << ":" << v.second.get<std::string>("main.humidity") << std::endl;
}
}
9. Instantiate Application as Service or Daemon
To do this, we need change our main
function a little. We will provide a way to user to choose if he wants to start the applications as service or as common application.
std::vector<std::string> arg_list
= this_application_cxt()->find<args>()->arg_vector();
int ret = 0;
if(std::find(arg_list.begin(), arg_list.end(), "--common") != arg_list.end())
ret = launch<common>(app, this_application_cxt(), ec);
else
{
ret = launch<server>(app, this_application_cxt(), ec);
}
10. Installation
In this version of lib, we don’t provide official support for "service or daemon" installation. This is because we need maintain symmetry between functionality provided on all platforms that the lib support, and on POSIX side, the installation process varies a lot. Thus, we provide installation functionality for major system as sample on examples folder. In future, we have a plan to bring these functionalities to the core lib.
Note: For POSIX, you need to use some shell script to control a daemon, refer to:
\example\setup\posix\ubuntu
That has some samples from Ubuntu.
On Windows, we will use this sample code:
\example\setup\windows\setup\service_setup.hpp
Now, it is time to code our setup method for Windows.
bool setup()
{
boost::shared_ptr<args> myargs
= this_application_cxt()->find<args>();
boost::shared_ptr<path> mypath
= this_application_cxt()->find<path>();
#if defined(BOOST_WINDOWS_API)
boost::filesystem::path executable_path_name = mypath->executable_path_name();
std::string exename = mypath->executable_name().stem().string();
po::options_description install("service options");
install.add_options()
("help", "produce a help message")
(",i", "install service")
(",u", "unistall service")
("user", po::value<std::string>()->default_value(""),
"user logon (optional, installation only)")
("pass", po::value<std::string>()->default_value(""),
"user password (optional, installation only)")
("name", po::value<std::string>()->default_value(exename),
"service name")
("display", po::value<std::string>()->default_value(""),
"service display name (optional, installation only)")
("description", po::value<std::string>()->default_value(""),
"service description (optional, installation only)")
;
po::parsed_options parsed =
po::command_line_parser(myargs->argc(), myargs->argv()
).options(install).allow_unregistered().run();
po::variables_map vm;
po::store(parsed, vm);
boost::system::error_code ec;
if (vm.count("help"))
{
std::cout << install << std::cout;
return true;
}
if (vm.count("-i"))
{
example::install_windows_service(
setup_arg(vm["name"].as<std::string>()),
setup_arg(vm["display"].as<std::string>()),
setup_arg(vm["description"].as<std::string>()),
setup_arg(executable_path_name),
setup_arg(vm["user"].as<std::string>()),
setup_arg(vm["pass"].as<std::string>())).install(ec);
std::cout << ec.message() << std::endl;
return true;
}
if (vm.count("-u"))
{
example::uninstall_windows_service(
setup_arg(vm["name"].as<std::string>()),
setup_arg(executable_path_name)).uninstall(ec);
std::cout << ec.message() << std::endl;
return true;
}
#endif
return false;
}
Here, we use Boost.Program_Options
to know user desired options.
Now, we have a command line interface on our server to install and uninstall if the service is installed. When the user requests installation, e.g.: ‘time_based_plugin_job.exe -i
', the installation requisition is identified by Boost.Program_options
and the code on ‘-i if
' is executed, installing the service.
Note that you can provide “user login
” to install the service for a specific Windows User.
The command line options are:
C:\Users\Renato Tegon Forti\Desktop\time_based_plugin_job\Debug>time_based_plugin_job.exe –i
[I] Setup changed the current configuration.
C:\Users\Renato Tegon Forti\Desktop\time_based_plugin_job\Debug>time_based_plugin_job.exe –u
[I] Setup changed the current configuration.
To install service, you need have permission to do that. An easy way is execute installation as Admin like:
After that, you will see on SCM our application as Service.
11. Other Handlers
Boost.Application
allows you to add some handlers, e.g. code some action when user clicks on stop link (Windows) or sends CTRL-C signal on POSIX.
We will add code to stop. First, we need to add ‘stop
’ method to our functor class, like this:
bool stop()
{
if (weather_log_.is_open())
{
weather_log_.close();
}
this_application_cxt()->find<timer_callback>()->stop();
return true;
}
Here, we close our log file and stop our timer engine.
Now, we need to add handler to ‘stop
’ on main
in your application context, like this:
this_application_cxt()->insert<termination_handler>(
boost::make_shared<termination_handler_default_behaviour>(
handler<bool>::make_global_callback(app, &time_based_job::stop)));
Now, when user clicks stop or send ctrl-c signal to application, the method ‘stop
’ will be called!
Note if you return false
from ‘stop
’, the application engine will ignore action, and the application will continue.
You have other handles available, like pause, or limit single instance. Check documentation for more details.
Conclusion
Boost.Application
can save time when the task is to build a cross platform server; otherwise the developer must work directly with a complex API provided for that. Furthermore Boost.Application
provides an extension mechanism for an application (based on plugins) simple and efficient, and many other ready-to-use features.
Feedback
If you are a Boost user, and use Boost Mailing Lists, please provide your feedback about the library, directly on list. (Specify if you think the library should be accepted as part of boost.org.)
If you are a CodeProject user, please provide your feedback about the library directly on this page.
Use
If you intend to use ‘Application’ on your application, please send-me your name and project. I am looking for create a list of users of ‘Application’.
re.tf@acm.or (Renato Tegon Forti)