The main entry point to the Ice run time is represented by the local interface Ice::Communicator. As for the client side, you must initialize the Ice run time by calling
Ice::initialize before you can do anything else in your server.
Ice::initialize returns a smart pointer to an instance of an
Ice::Communicator:
int
main(int argc, char* argv[])
{
Ice::CommunicatorPtr ic
= Ice::initialize(argc, argv);
// ...
}
Ice::initialize accepts a C++ reference to
argc and
argv. The function scans the argument vector for any command-line options that are relevant to the Ice run time; any such options are removed from the argument vector so, when
Ice::initialize returns, the only options and arguments remaining are those that concern your application. If anything goes wrong during initialization,
initialize throws an exception.
1
Before leaving your main function, you
must call
Communicator::destroy. The
destroy operation is responsible for finalizing the Ice run time. In particular,
destroy waits for any operation invocations that may still be running to complete. In addition,
destroy ensures that any outstanding threads are joined with and reclaims a number of operating system resources, such as file descriptors and memory. Never allow your
main function to terminate without calling
destroy first; doing so has undefined behavior.
The general shape of our server-side main function is therefore as follows:
#include <Ice/Ice.h>
int
main(int argc, char* argv[])
{
int status = 0;
Ice::CommunicatorPtr ic;
try {
ic = Ice::initialize(argc, argv);
// Server code here...
} catch (const Ice::Exception& e) {
cerr << e << endl;
status = 1;
} catch (const std::string& msg) {
cerr << msg << endl;
status = 1;
} catch (const char* msg) {
cerr << msg << endl;
status = 1;
}
if (ic) {
try {
ic‑>destroy();
} catch (const std::string& msg) {
cerr << msg << endl;
status = 1;
}
}
return status;
}
Note that the code places the call to Ice::initialize in to a
try block and takes care to return the correct exit status to the operating system. Also note that an attempt to destroy the communicator is made only if the initialization succeeded.
The catch handlers for
const std::string & and
const char * are in place as a convenience feature: if we encounter a fatal error condition anywhere in the server code, we can simply throw a string or a string literal containing an error message; this causes the stack to be unwound back to
main, at which point the error message is printed and, after destroying the communicator,
main terminates with non-zero exit status.
8.3.1 The Ice::Application Class
The preceding structure for the main function is so common that Ice offers a class,
Ice::Application, that encapsulates all the correct initialization and finalization activities. The definition of the class is as follows (with some detail omitted for now):
namespace Ice {
enum SignalPolicy { HandleSignals, NoSignalHandling };
class Application /* ... */ {
public:
Application(SignalPolicy = HandleSignals);
virtual ~Application();
int main(int argc, char*[] argv);
int main(int, char*[], const char* config);
int main(int argc, char*[] argv,
const Ice::InitializationData& id);
int main(const Ice::StringSeq&);
int main(const Ice::StringSeq&, const char* config);
int main(const Ice::StringSeq&,
const Ice::InitializationData& id);
virtual int run(int, char*[]) = 0;
static const char* appName();
static CommunicatorPtr communicator();
// ...
};
}
The intent of this class is that you specialize Ice::Application and implement the pure virtual
run method in your derived class. Whatever code you would normally place in
main goes into the
run method instead. Using
Ice::Application, our program looks as follows:
#include <Ice/Ice.h>
class MyApplication : virtual public Ice::Application {
public:
virtual int run(int, char*[]) {
// Server code here...
return 0;
}
};
int
main(int argc, char* argv[])
{
MyApplication app;
return app.main(argc, argv);
}
Note that Application::main is overloaded: you can pass a string sequence instead of an
argc/
argv pair. This is useful if you need to parse application-specific property settings on the command line (see
Section 26.8.3). You also can call main with an optional file name or an
InitializationData structure (see
Section 28.3 and
Section 26.8). If you pass a configuration file name, settings on the command line override settings in the configuration file. The
Application::main function does the following:
1. It installs an exception handler for Ice::Exception. If your code fails to handle an Ice exception,
Application::main prints the exception details on
stderr before returning with a non-zero return value.
2.
It installs exception handlers for const std::string & and
const char *. This allows you to terminate your server in response to a fatal error condition by throwing a
std::string or a string literal.
Application::main prints the string on
stderr before returning a non-zero return value.
3.
It initializes (by calling Ice::initialize) and finalizes (by calling
Communicator::destroy) a communicator. You can get access to the communicator for your server by calling the static
communicator() member.
6.
It creates an IceUtil::CtrlCHandler that properly destroys the communicator.
7.
It installs a per-process logger (see Section 28.19.5) if the application has not already configured one. The per-process logger uses the value of the
Ice.ProgramName property (see
Section 26.7) as a prefix for its messages and sends its output to the standard error channel. An application can specify an alternate logger by including it in the
InitializationData structure.
Using Ice::Application ensures that your program properly finalizes the Ice run time, whether your server terminates normally or in response to an exception or signal. We recommend that all your programs use this class; doing so makes your life easier. In addition,
Ice::Application also provides features for signal handling and configuration that you do not have to implement yourself when you use this class.
Using Ice::Application on the Client Side
You can use Ice::Application for your clients as well: simply implement a class that derives from
Ice::Application and place the client code into its
run method. The advantage of this approach is the same as for the server side:
Ice::Application ensures that the communicator is destroyed correctly even in the presence of exceptions.
The simple server we developed in Chapter 3 had no way to shut down cleanly: we simply interrupted the server from the command line to force it to exit. Terminating a server in this fashion is unacceptable for many real-life server applications: typically, the server has to perform some cleanup work before terminating, such as flushing database buffers or closing network connections. This is particularly important on receipt of a signal or keyboard interrupt to prevent possible corruption of database files or other persistent data.
To make it easier to deal with signals, Ice::Application encapsulates the platform-independent signal handling capabilities provided by the class
IceUtil::CtrlCHandler (see
Section 27.12). This allows you to cleanly shut down on receipt of a signal and to use the same source code regardless of the underlying operating system and threading package:
namespace Ice {
class Application : /* ... */ {
public:
// ...
static void destroyOnInterrupt();
static void shutdownOnInterrupt();
static void ignoreInterrupt();
static void callbackOnInterrupt();
static void holdInterrupt();
static void releaseInterrupt();
static bool interrupted();
virtual void interruptCallback(int);
};
}
You can use Ice::Application under both Windows and Unix: for Unix, the member functions control the behavior of your application for
SIGINT,
SIGHUP, and
SIGTERM; for Windows, the member functions control the behavior of your application for
CTRL_C_EVENT,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT, and
CTRL_SHUTDOWN_EVENT.
This function creates an IceUtil::CtrlCHandler that destroys the communicator when one of the monitored signals is raised. This is the default behavior.
This function creates an IceUtil::CtrlCHandler that shuts down the communicator when one of the monitored signals is raised.
This function configures Ice::Application to invoke
interruptCallback when a signal occurs, thereby giving the subclass responsibility for handling the signal. Note that if the signal handler needs to terminate the program, you must call
_exit (instead of
exit). This prevents global destructors from running which, depending on the activities of other threads in the program, could cause deadlock or assertion failures.
This function returns true if a signal caused the communicator to shut down,
false otherwise. This allows us to distinguish intentional shutdown from a forced shutdown that was caused by a signal. This is useful, for example, for logging purposes.
By default, Ice::Application behaves as if
destroyOnInterrupt was invoked, therefore our server
main function requires no change to ensure that the program terminates cleanly on receipt of a signal. (You can disable the signal-handling functionality of
Ice::Application by passing the enumerator
NoSignalHandling to the constructor. In that case, signals retain their default behavior, that is, terminate the process.) However, we add a diagnostic to report the occurrence of a signal, so our
main function now looks like:
#include <Ice/Ice.h>
class MyApplication : virtual public Ice::Application {
public:
virtual int run(int, char*[]) {
// Server code here...
if (interrupted())
cerr << appName() << ": terminating" << endl;
return 0;
}
};
int
main(int argc, char* argv[])
{
MyApplication app;
return app.main(argc, argv);
}
Note that, if your server is interrupted by a signal, the Ice run time waits for all currently executing operations to finish. This means that an operation that updates persistent state cannot be interrupted in the middle of what it was doing and cause partial update problems.
Under Unix, if you handle signals with your own handler (by deriving a subclass from
Ice::Application and calling
callbackOnInterrupt), the handler is invoked synchronously from a separate thread. This means that the handler can safely call into the Ice run time or make system calls that are not async-signal-safe without fear of deadlock or data corruption. Note that
Ice::Application blocks delivery of
SIGINT,
SIGHUP, and
SIGTERM. If your application calls
exec, this means that the child process will also ignore these signals; if you need the default behavior of these signals in the exec’d process, you must explicitly reset them to
SIG_DFL before calling
exec.
Apart from the functionality shown in this section, Ice::Application also takes care of initializing the Ice run time with property values. Properties allow you to configure the run time in various ways. For example, you can use properties to control things such as the thread pool size or port number for a server. We discuss Ice properties in more detail in
Chapter 26.
Ice::Application is a singleton class that creates a single communicator. If you are using multiple communicators, you cannot use
Ice::Application. Instead, you must structure your code as we saw in
Chapter 3 (taking care to always destroy the communicators).
8.3.2 The Ice::Service Class
The Ice::Application class described in
Section 8.3.1 is very convenient for general use by Ice client and server applications. In some cases, however, an application may need to run at the system level as a Unix daemon or Win32 service. For these situations, Ice includes
Ice::Service, a singleton class that is comparable to
Ice::Application but also encapsulates the low-level, platform-specific initialization and shutdown procedures common to system services. The
Ice::Service class is defined as follows:
namespace Ice {
class Service {
public:
Service();
virtual bool shutdown();
virtual void interrupt();
int main(int&, char*[],
const Ice::InitializationData& =
Ice::InitializationData());
int main(Ice::StringSeq&,
const Ice::InitializationData& =
Ice::InitializationData());
Ice::CommunicatorPtr communicator() const;
static Service* instance();
bool service() const;
std::string name() const;
bool checkSystem() const;
int run(int&, char*[], const Ice::InitializationData&);
void configureService(const std::string&);
void configureDaemon(bool, bool, const std::string&);
virtual void handleInterrupt(int);
protected:
virtual bool start(int, char*[]) = 0;
virtual void waitForShutdown();
virtual bool stop();
virtual Ice::CommunicatorPtr initializeCommunicator(
int&, char*[], const Ice::InitializationData&);
virtual void syserror(const std::string&);
virtual void error(const std::string&);
virtual void warning(const std::string&);
virtual void trace(const std::string&);
void enableInterrupt();
void disableInterrupt();
// ...
};
}
At a minimum, an Ice application that uses the Ice::Service class must define a subclass and override the
start member function, which is where the service must perform its startup activities, such as processing command-line arguments, creating an object adapter, and registering servants. The application’s
main function must instantiate the subclass and typically invokes its
main member function, passing the program’s argument vector as parameters. The example below illustrates a minimal
Ice::Service subclass:
#include <Ice/Service.h>
class MyService : public Ice::Service {
protected:
virtual bool start(int, char*[]);
private:
Ice::ObjectAdapterPtr _adapter;
};
bool
MyService::start(int argc, char* argv[])
{
_adapter = communicator()‑>createObjectAdapter("MyAdapter");
_adapter‑>addWithUUID(new MyServantI);
_adapter‑>activate();
return true;
}
int
main(int argc, char* argv[])
{
MyService svc;
return svc.main(argc, argv);
}
The Service::main member function performs the following sequence of tasks:
3.
Invokes the run member function and returns its result.
Note that, as for Application::main,
Service::main is overloaded to accept a string sequence instead of an
argc/
argv pair. This is useful if you need to parse application-specific property settings on the command line (see
Section 26.8.3).
The Service::run member function executes the service in the steps shown below:
2.
Invokes the initializeCommunicator member function to obtain a communicator. The communicator instance can be accessed using the
communicator member function.
3.
Invokes the start member function. If
start returns
false to indicate failure,
run destroys the communicator and returns immediately.
4.
Invokes the waitForShutdown member function, which should block until
shutdown is invoked.
5.
Invokes the stop member function. If
stop returns
true,
run considers the application to have terminated successfully.
If an unhandled exception is caught by Service::run, a descriptive message is logged, the communicator is destroyed and the service is terminated.
The virtual member functions in Ice::Service represent the points at which a subclass can intercept the service activities. All of the virtual member functions (except
start) have default implementations.
Invoked by the CtrlCHandler when a signal occurs. The default implementation ignores the signal if it represents a logoff event and the
Ice.Nohup property is set to a value larger than zero, otherwise it invokes the
interrupt member function.
•
Ice::CommunicatorPtr
initializeCommunicator(int & argc, char * argv[],
const Ice::InitializationData & data)
Configures the program to run as a Unix daemon. The chdir parameter determines whether the daemon changes its working directory to the root directory. The
close parameter determines whether the daemon closes unnecessary file descriptors (i.e., stdin, stdout, etc.). If a non-empty string is provided in the
pidFile parameter, the daemon writes its process ID to the given file.
The primary entry point of the Ice::Service class. The tasks performed by this function are described earlier in this section. The function returns
EXIT_SUCCESS for success,
EXIT_FAILURE for failure.
On Unix platforms, Ice::Service recognizes the following command-line options:
Indicates that the program should run as a daemon. This involves the creation of a background child process in which
Service::main performs its tasks. The parent process does not terminate until the child process has successfully invoked the
start member function
2. Unless instructed otherwise,
Ice::Service changes the current working directory of the child process to the root directory, and closes all unnecessary file descriptors. Note that the file descriptors are not closed until after the communicator is initialized, meaning standard input, standard output, and standard error are available for use during this time. For example, the IceSSL plug-in may need to prompt for a passphrase on standard input, or Ice may print the child’s process id on standard output if the property
Ice.PrintProcessId is set.
Prevents Ice::Service from closing unnecessary file descriptors. This can be useful during debugging and diagnosis because it provides access to the output from the daemon’s standard output and standard error.
Prevents Ice::Service from changing the current working directory.
The ‑‑noclose and
‑‑nochdir options can only be specified in conjunction with
‑‑daemon. These options are removed from the argument vector that is passed to the
start member function.
Ice::Service attempts to start the application as a Windows service if the
‑‑service option is specified:
Run as a Windows service named NAME, which must already be installed. This option is removed from the argument vector that is passed to the
start member function.
Installing and configuring a Windows service is outside the scope of the Ice::Service class. Ice includes a utility for installing its services (see
Appendix H) which you can use as a model for your own applications.
The Ice::Service class supports the Windows service control codes
SERVICE_CONTROL_INTERROGATE and
SERVICE_CONTROL_STOP. Upon receipt of
SERVICE_CONTROL_STOP,
Ice::Service invokes the
shutdown member function.
•
in the InitializationData argument that is passed to
main,
On Windows, Ice::Service installs its own logger that uses the Windows
Application event log if no custom logger is defined. The source name for the event log is the service’s name unless a different value is specified using the property
Ice.EventLog.Source (see
Appendix C).
On Unix, the default Ice logger (which logs to the standard error output) is used when no other logger is configured. For daemons, this is not appropriate because the output will be lost. To change this, you can either implement a custom logger or set the
Ice.UseSyslog property, which selects a logger implementation that logs to the syslog facility. Alternatively, you can set the
Ice.StdErr property to redirect standard error output to a file.
Note that Ice::Service may encounter errors before the communicator is initialized. In this situation,
Ice::Service uses its default logger unless a process-wide logger is configured. Therefore, even if a failing service is configured to use a different logger implementation, you may find useful diagnostic information in the
Application event log (on Windows) or sent to standard error (on Unix).