ApplicationPoolServer.h

00001 /*
00002  *  Phusion Passenger - http://www.modrails.com/
00003  *  Copyright (C) 2008  Phusion
00004  *
00005  *  Phusion Passenger is a trademark of Hongli Lai & Ninh Bui.
00006  *
00007  *  This program is free software; you can redistribute it and/or modify
00008  *  it under the terms of the GNU General Public License as published by
00009  *  the Free Software Foundation; version 2 of the License.
00010  *
00011  *  This program is distributed in the hope that it will be useful,
00012  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00013  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014  *  GNU General Public License for more details.
00015  *
00016  *  You should have received a copy of the GNU General Public License along
00017  *  with this program; if not, write to the Free Software Foundation, Inc.,
00018  *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
00019  */
00020 #ifndef _PASSENGER_APPLICATION_POOL_SERVER_H_
00021 #define _PASSENGER_APPLICATION_POOL_SERVER_H_
00022 
00023 #include <boost/shared_ptr.hpp>
00024 #include <boost/thread/mutex.hpp>
00025 
00026 #include <sys/types.h>
00027 #include <sys/stat.h>
00028 #include <sys/wait.h>
00029 #include <sys/socket.h>
00030 #include <cstdio>
00031 #include <cstdlib>
00032 #include <limits.h>
00033 #include <errno.h>
00034 #include <unistd.h>
00035 #include <signal.h>
00036 
00037 #include "MessageChannel.h"
00038 #include "ApplicationPool.h"
00039 #include "Application.h"
00040 #include "Exceptions.h"
00041 #include "Logging.h"
00042 #include "System.h"
00043 
00044 namespace Passenger {
00045 
00046 using namespace std;
00047 using namespace boost;
00048 
00049 
00050 /**
00051  * Multi-process usage support for ApplicationPool.
00052  *
00053  * ApplicationPoolServer implements a client/server architecture for ApplicationPool.
00054  * This allows one to use ApplicationPool in a multi-process environment (unlike
00055  * StandardApplicationPool). The cache/pool data is stored in the server. Different
00056  * processes can then access the pool through the server.
00057  *
00058  * ApplicationPoolServer itself does not inherit ApplicationPool. Instead, it returns
00059  * an ApplicationPool object via the connect() call. For example:
00060  * @code
00061  *   // Create an ApplicationPoolServer.
00062  *   ApplicationPoolServer server(...);
00063  *   
00064  *   // Now fork a child process, like Apache's prefork MPM eventually will.
00065  *   pid_t pid = fork();
00066  *   if (pid == 0) {
00067  *       // Child process
00068  *       
00069  *       // Connect to the server. After connection, we have an ApplicationPool
00070  *       // object!
00071  *       ApplicationPoolPtr pool(server.connect());
00072  *
00073  *       // We don't need to connect to the server anymore, so we detach from it.
00074  *       // This frees up some resources, such as file descriptors.
00075  *       server.detach();
00076  *
00077  *       ApplicationPool::SessionPtr session(pool->get("/home/webapps/foo"));
00078  *       do_something_with(session);
00079  *
00080  *       _exit(0);
00081  *   } else {
00082  *       // Parent process
00083  *       waitpid(pid, NULL, 0);
00084  *   }
00085  * @endcode
00086  *
00087  * <h2>Implementation notes</h2>
00088  *
00089  * <h3>Separate server executable</h3>
00090  * The actual server is implemented in ApplicationPoolServerExecutable.cpp, this class is
00091  * just a convenience class for starting/stopping the server executable and connecting
00092  * to it.
00093  *
00094  * In the past, the server logic itself was implemented in this class. This implies that
00095  * the ApplicationPool server ran inside the Apache process. This presented us with several
00096  * problems:
00097  * - Because of the usage of threads in the ApplicationPool server, the Apache VM size would
00098  *   go way up. This gave people the (wrong) impression that Passenger uses a lot of memory,
00099  *   or that it leaks memory.
00100  * - Although it's not entirely confirmed, we suspect that it caused heap fragmentation as
00101  *   well. Apache allocates lots and lots of small objects on the heap, and ApplicationPool
00102  *   server isn't exactly helping. This too gave people the (wrong) impression that
00103  *   Passenger leaks memory.
00104  * - It would unnecessarily bloat the VM size of Apache worker processes.
00105  * - We had to resort to all kinds of tricks to make sure that fork()ing a process doesn't
00106  *   result in file descriptor leaks.
00107  * - Despite everything, there was still a small chance that file descriptor leaks would
00108  *   occur, and this could not be fixed. The reason for this is that the Apache control
00109  *   process may call fork() right after the ApplicationPool server has established a new
00110  *   connection with a client.
00111  *
00112  * Because of these problems, it was decided to split the ApplicationPool server to a
00113  * separate executable. This comes with no performance hit.
00114  *
00115  * <h3>Anonymous server socket</h3>
00116  * Notice that ApplicationPoolServer does do not use TCP sockets at all, or even named Unix
00117  * sockets, despite being a server that can handle multiple clients! So ApplicationPoolServer
00118  * will expose no open ports or temporary Unix socket files. Only child processes are able
00119  * to use the ApplicationPoolServer.
00120  *
00121  * This is implemented through anonymous Unix sockets (<tt>socketpair()</tt>) and file descriptor
00122  * passing. It allows one to emulate <tt>accept()</tt>. ApplicationPoolServer is connected to
00123  * the server executable through a Unix socket pair. connect() sends a connect request to the
00124  * server through that socket. The server will then create a new socket pair, and pass one of
00125  * them back. This new socket pair represents the newly established connection.
00126  *
00127  * @ingroup Support
00128  */
00129 class ApplicationPoolServer {
00130 private:
00131         /**
00132          * Contains data shared between RemoteSession and Client.
00133          * Since RemoteSession and Client have different life times, i.e. one may be
00134          * destroyed before the other, they both use a smart pointer that points to
00135          * a SharedData. This way, the SharedData object is only destroyed when
00136          * both the RemoteSession and the Client object has been destroyed.
00137          */
00138         struct SharedData {
00139                 /**
00140                  * The socket connection to the ApplicationPool server, as was
00141                  * established by ApplicationPoolServer::connect().
00142                  */
00143                 int server;
00144                 
00145                 mutex lock;
00146                 
00147                 ~SharedData() {
00148                         int ret;
00149                         do {
00150                                 ret = close(server);
00151                         } while (ret == -1 && errno == EINTR);
00152                 }
00153         };
00154         
00155         typedef shared_ptr<SharedData> SharedDataPtr;
00156         
00157         /**
00158          * An Application::Session which works together with ApplicationPoolServer.
00159          */
00160         class RemoteSession: public Application::Session {
00161         private:
00162                 SharedDataPtr data;
00163                 int id;
00164                 int fd;
00165                 pid_t pid;
00166         public:
00167                 RemoteSession(SharedDataPtr data, pid_t pid, int id, int fd) {
00168                         this->data = data;
00169                         this->pid = pid;
00170                         this->id = id;
00171                         this->fd = fd;
00172                 }
00173                 
00174                 virtual ~RemoteSession() {
00175                         closeStream();
00176                         mutex::scoped_lock(data->lock);
00177                         MessageChannel(data->server).write("close", toString(id).c_str(), NULL);
00178                 }
00179                 
00180                 virtual int getStream() const {
00181                         return fd;
00182                 }
00183                 
00184                 virtual void shutdownReader() {
00185                         if (fd != -1) {
00186                                 int ret = InterruptableCalls::shutdown(fd, SHUT_RD);
00187                                 if (ret == -1) {
00188                                         throw SystemException("Cannot shutdown the writer stream",
00189                                                 errno);
00190                                 }
00191                         }
00192                 }
00193                 
00194                 virtual void shutdownWriter() {
00195                         if (fd != -1) {
00196                                 int ret = InterruptableCalls::shutdown(fd, SHUT_WR);
00197                                 if (ret == -1) {
00198                                         throw SystemException("Cannot shutdown the writer stream",
00199                                                 errno);
00200                                 }
00201                         }
00202                 }
00203                 
00204                 virtual void closeStream() {
00205                         if (fd != -1) {
00206                                 int ret = InterruptableCalls::close(fd);
00207                                 if (ret == -1) {
00208                                         throw SystemException("Cannot close the session stream",
00209                                                 errno);
00210                                 }
00211                                 fd = -1;
00212                         }
00213                 }
00214                 
00215                 virtual void discardStream() {
00216                         fd = -1;
00217                 }
00218                 
00219                 virtual pid_t getPid() const {
00220                         return pid;
00221                 }
00222         };
00223         
00224         /**
00225          * An ApplicationPool implementation that works together with ApplicationPoolServer.
00226          * It doesn't do much by itself, its job is mostly to forward queries/commands to
00227          * the server and returning the result. Most of the logic is in the server executable.
00228          */
00229         class Client: public ApplicationPool {
00230         private:
00231                 // The smart pointer only serves to keep the shared data alive.
00232                 // We access the shared data via a normal pointer, for performance.
00233                 SharedDataPtr dataSmartPointer;
00234                 SharedData *data;
00235                 
00236         public:
00237                 /**
00238                  * Create a new Client.
00239                  *
00240                  * @param sock The newly established socket connection with the ApplicationPoolServer.
00241                  */
00242                 Client(int sock) {
00243                         dataSmartPointer = ptr(new SharedData());
00244                         data = dataSmartPointer.get();
00245                         data->server = sock;
00246                 }
00247                 
00248                 virtual void clear() {
00249                         MessageChannel channel(data->server);
00250                         mutex::scoped_lock l(data->lock);
00251                         channel.write("clear", NULL);
00252                 }
00253                 
00254                 virtual void setMaxIdleTime(unsigned int seconds) {
00255                         MessageChannel channel(data->server);
00256                         mutex::scoped_lock l(data->lock);
00257                         channel.write("setMaxIdleTime", toString(seconds).c_str(), NULL);
00258                 }
00259                 
00260                 virtual void setMax(unsigned int max) {
00261                         MessageChannel channel(data->server);
00262                         mutex::scoped_lock l(data->lock);
00263                         channel.write("setMax", toString(max).c_str(), NULL);
00264                 }
00265                 
00266                 virtual unsigned int getActive() const {
00267                         MessageChannel channel(data->server);
00268                         mutex::scoped_lock l(data->lock);
00269                         vector<string> args;
00270                         
00271                         channel.write("getActive", NULL);
00272                         channel.read(args);
00273                         return atoi(args[0].c_str());
00274                 }
00275                 
00276                 virtual unsigned int getCount() const {
00277                         MessageChannel channel(data->server);
00278                         mutex::scoped_lock l(data->lock);
00279                         vector<string> args;
00280                         
00281                         channel.write("getCount", NULL);
00282                         channel.read(args);
00283                         return atoi(args[0].c_str());
00284                 }
00285                 
00286                 virtual void setMaxPerApp(unsigned int max) {
00287                         MessageChannel channel(data->server);
00288                         mutex::scoped_lock l(data->lock);
00289                         channel.write("setMaxPerApp", toString(max).c_str(), NULL);
00290                 }
00291                 
00292                 virtual pid_t getSpawnServerPid() const {
00293                         this_thread::disable_syscall_interruption dsi;
00294                         MessageChannel channel(data->server);
00295                         mutex::scoped_lock l(data->lock);
00296                         vector<string> args;
00297                         
00298                         channel.write("getSpawnServerPid", NULL);
00299                         channel.read(args);
00300                         return atoi(args[0].c_str());
00301                 }
00302                 
00303                 virtual Application::SessionPtr get(
00304                         const string &appRoot,
00305                         bool lowerPrivilege = true,
00306                         const string &lowestUser = "nobody",
00307                         const string &environment = "production",
00308                         const string &spawnMethod = "smart",
00309                         const string &appType = "rails"
00310                 ) {
00311                         this_thread::disable_syscall_interruption dsi;
00312                         MessageChannel channel(data->server);
00313                         mutex::scoped_lock l(data->lock);
00314                         vector<string> args;
00315                         int stream;
00316                         bool result;
00317                         
00318                         try {
00319                                 channel.write("get", appRoot.c_str(),
00320                                         (lowerPrivilege) ? "true" : "false",
00321                                         lowestUser.c_str(),
00322                                         environment.c_str(),
00323                                         spawnMethod.c_str(),
00324                                         appType.c_str(),
00325                                         NULL);
00326                         } catch (const SystemException &) {
00327                                 throw IOException("The ApplicationPool server exited unexpectedly.");
00328                         }
00329                         try {
00330                                 result = channel.read(args);
00331                         } catch (const SystemException &e) {
00332                                 throw SystemException("Could not read a message from "
00333                                         "the ApplicationPool server", e.code());
00334                         }
00335                         if (!result) {
00336                                 throw IOException("The ApplicationPool server unexpectedly "
00337                                         "closed the connection.");
00338                         }
00339                         if (args[0] == "ok") {
00340                                 stream = channel.readFileDescriptor();
00341                                 return ptr(new RemoteSession(dataSmartPointer,
00342                                         atoi(args[1]), atoi(args[2]), stream));
00343                         } else if (args[0] == "SpawnException") {
00344                                 if (args[2] == "true") {
00345                                         string errorPage;
00346                                         
00347                                         if (!channel.readScalar(errorPage)) {
00348                                                 throw IOException("The ApplicationPool server "
00349                                                         "unexpectedly closed the connection.");
00350                                         }
00351                                         throw SpawnException(args[1], errorPage);
00352                                 } else {
00353                                         throw SpawnException(args[1]);
00354                                 }
00355                         } else if (args[0] == "BusyException") {
00356                                 throw BusyException(args[1]);
00357                         } else if (args[0] == "IOException") {
00358                                 throw IOException(args[1]);
00359                         } else {
00360                                 throw IOException("The ApplicationPool server returned "
00361                                         "an unknown message: " + toString(args));
00362                         }
00363                 }
00364         };
00365         
00366         
00367         static const int SERVER_SOCKET_FD = 3;
00368         
00369         string m_serverExecutable;
00370         string m_spawnServerCommand;
00371         string m_logFile;
00372         string m_rubyCommand;
00373         string m_user;
00374         string statusReportFIFO;
00375         
00376         /**
00377          * The PID of the ApplicationPool server process. If no server process
00378          * is running, then <tt>serverPid == 0</tt>.
00379          *
00380          * @invariant
00381          *    if serverPid == 0:
00382          *       serverSocket == -1
00383          */
00384         pid_t serverPid;
00385         
00386         /**
00387          * The connection to the ApplicationPool server process. If no server
00388          * process is running, then <tt>serverSocket == -1</tt>.
00389          *
00390          * @invariant
00391          *    if serverPid == 0:
00392          *       serverSocket == -1
00393          */
00394         int serverSocket;
00395         
00396         /**
00397          * Shutdown the currently running ApplicationPool server process.
00398          *
00399          * @pre System call interruption is disabled.
00400          * @pre serverSocket != -1 && serverPid != 0
00401          * @post serverSocket == -1 && serverPid == 0
00402          */
00403         void shutdownServer() {
00404                 this_thread::disable_syscall_interruption dsi;
00405                 int ret;
00406                 time_t begin;
00407                 bool done = false;
00408                 
00409                 InterruptableCalls::close(serverSocket);
00410                 if (!statusReportFIFO.empty()) {
00411                         do {
00412                                 ret = unlink(statusReportFIFO.c_str());
00413                         } while (ret == -1 && errno == EINTR);
00414                 }
00415                 
00416                 P_TRACE(2, "Waiting for existing ApplicationPoolServerExecutable (PID " <<
00417                         serverPid << ") to exit...");
00418                 begin = InterruptableCalls::time(NULL);
00419                 while (!done && InterruptableCalls::time(NULL) < begin + 5) {
00420                         /*
00421                          * Some Apache modules fork(), but don't close file descriptors.
00422                          * mod_wsgi is one such example. Because of that, closing serverSocket
00423                          * won't always cause the ApplicationPool server to exit. So we send it a
00424                          * signal.
00425                          */
00426                         InterruptableCalls::kill(serverPid, SIGINT);
00427                         
00428                         ret = InterruptableCalls::waitpid(serverPid, NULL, WNOHANG);
00429                         done = ret > 0 || ret == -1;
00430                         if (!done) {
00431                                 InterruptableCalls::usleep(100000);
00432                         }
00433                 }
00434                 if (done) {
00435                         P_TRACE(2, "ApplicationPoolServerExecutable exited.");
00436                 } else {
00437                         P_DEBUG("ApplicationPoolServerExecutable not exited in time. Killing it...");
00438                         InterruptableCalls::kill(serverPid, SIGTERM);
00439                         InterruptableCalls::waitpid(serverPid, NULL, 0);
00440                 }
00441                 
00442                 serverSocket = -1;
00443                 serverPid = 0;
00444         }
00445         
00446         /**
00447          * Start an ApplicationPool server process. If there's already one running,
00448          * then the currently running one will be shutdown.
00449          *
00450          * @pre System call interruption is disabled.
00451          * @post serverSocket != -1 && serverPid != 0
00452          * @throw SystemException Something went wrong.
00453          */
00454         void restartServer() {
00455                 int fds[2];
00456                 pid_t pid;
00457                 
00458                 if (serverPid != 0) {
00459                         shutdownServer();
00460                 }
00461                 
00462                 if (InterruptableCalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) {
00463                         throw SystemException("Cannot create a Unix socket pair", errno);
00464                 }
00465                 
00466                 createStatusReportFIFO();
00467                 
00468                 pid = InterruptableCalls::fork();
00469                 if (pid == 0) { // Child process.
00470                         dup2(fds[0], SERVER_SOCKET_FD);
00471                         
00472                         // Close all unnecessary file descriptors
00473                         for (long i = sysconf(_SC_OPEN_MAX) - 1; i > SERVER_SOCKET_FD; i--) {
00474                                 close(i);
00475                         }
00476                         
00477                         execlp(
00478                                 #if 0
00479                                         "valgrind",
00480                                         "valgrind",
00481                                 #else
00482                                         m_serverExecutable.c_str(),
00483                                 #endif
00484                                 m_serverExecutable.c_str(),
00485                                 toString(Passenger::getLogLevel()).c_str(),
00486                                 m_spawnServerCommand.c_str(),
00487                                 m_logFile.c_str(),
00488                                 m_rubyCommand.c_str(),
00489                                 m_user.c_str(),
00490                                 statusReportFIFO.c_str(),
00491                                 NULL);
00492                         int e = errno;
00493                         fprintf(stderr, "*** Passenger ERROR: Cannot execute %s: %s (%d)\n",
00494                                 m_serverExecutable.c_str(), strerror(e), e);
00495                         fflush(stderr);
00496                         _exit(1);
00497                 } else if (pid == -1) { // Error.
00498                         InterruptableCalls::close(fds[0]);
00499                         InterruptableCalls::close(fds[1]);
00500                         throw SystemException("Cannot create a new process", errno);
00501                 } else { // Parent process.
00502                         InterruptableCalls::close(fds[0]);
00503                         serverSocket = fds[1];
00504                         serverPid = pid;
00505                 }
00506         }
00507         
00508         void createStatusReportFIFO() {
00509                 char filename[PATH_MAX];
00510                 int ret;
00511                 
00512                 snprintf(filename, sizeof(filename), "/tmp/passenger_status.%d.fifo",
00513                         getpid());
00514                 filename[PATH_MAX - 1] = '\0';
00515                 do {
00516                         ret = mkfifo(filename, S_IRUSR | S_IWUSR);
00517                 } while (ret == -1 && errno == EINTR);
00518                 if (ret == -1 && errno != EEXIST) {
00519                         int e = errno;
00520                         P_WARN("*** WARNING: Could not create FIFO '" << filename <<
00521                                 "': " << strerror(e) << " (" << e << ")" << endl <<
00522                                 "Disabling Passenger ApplicationPool status reporting.");
00523                         statusReportFIFO = "";
00524                 } else {
00525                         statusReportFIFO = filename;
00526                 }
00527         }
00528 
00529 public:
00530         /**
00531          * Create a new ApplicationPoolServer object.
00532          *
00533          * @param serverExecutable The filename of the ApplicationPool server
00534          *            executable to use.
00535          * @param spawnServerCommand The filename of the spawn server to use.
00536          * @param logFile Specify a log file that the spawn server should use.
00537          *            Messages on its standard output and standard error channels
00538          *            will be written to this log file. If an empty string is
00539          *            specified, no log file will be used, and the spawn server
00540          *            will use the same standard output/error channels as the
00541          *            current process.
00542          * @param rubyCommand The Ruby interpreter's command.
00543          * @param user The user that the spawn manager should run as. This
00544          *             parameter only has effect if the current process is
00545          *             running as root. If the empty string is given, or if
00546          *             the <tt>user</tt> is not a valid username, then
00547          *             the spawn manager will be run as the current user.
00548          * @throws SystemException An error occured while trying to setup the spawn server
00549          *            or the server socket.
00550          * @throws IOException The specified log file could not be opened.
00551          */
00552         ApplicationPoolServer(const string &serverExecutable,
00553                      const string &spawnServerCommand,
00554                      const string &logFile = "",
00555                      const string &rubyCommand = "ruby",
00556                      const string &user = "")
00557         : m_serverExecutable(serverExecutable),
00558           m_spawnServerCommand(spawnServerCommand),
00559           m_logFile(logFile),
00560           m_rubyCommand(rubyCommand),
00561           m_user(user) {
00562                 serverSocket = -1;
00563                 serverPid = 0;
00564                 this_thread::disable_syscall_interruption dsi;
00565                 restartServer();
00566         }
00567         
00568         ~ApplicationPoolServer() {
00569                 if (serverSocket != -1) {
00570                         this_thread::disable_syscall_interruption dsi;
00571                         shutdownServer();
00572                 }
00573         }
00574         
00575         /**
00576          * Connects to the server and returns a usable ApplicationPool object.
00577          * All cache/pool data of this ApplicationPool is actually stored on
00578          * the server and shared with other clients, but that is totally
00579          * transparent to the user of the ApplicationPool object.
00580          *
00581          * @note
00582          *   All methods of the returned ApplicationPool object may throw
00583          *   SystemException, IOException or boost::thread_interrupted.
00584          *
00585          * @warning
00586          * One may only use the returned ApplicationPool object for handling
00587          * one session at a time. For example, don't do stuff like this:
00588          * @code
00589          *   ApplicationPoolPtr pool = server.connect();
00590          *   Application::SessionPtr session1 = pool->get(...);
00591          *   Application::SessionPtr session2 = pool->get(...);
00592          * @endcode
00593          * Otherwise, a deadlock can occur under certain circumstances.
00594          * @warning
00595          * Instead, one should call connect() multiple times:
00596          * @code
00597          *   ApplicationPoolPtr pool1 = server.connect();
00598          *   Application::SessionPtr session1 = pool1->get(...);
00599          *   
00600          *   ApplicationPoolPtr pool2 = server.connect();
00601          *   Application::SessionPtr session2 = pool2->get(...);
00602          * @endcode
00603          *
00604          * @throws SystemException Something went wrong.
00605          * @throws IOException Something went wrong.
00606          */
00607         ApplicationPoolPtr connect() {
00608                 try {
00609                         this_thread::disable_syscall_interruption dsi;
00610                         MessageChannel channel(serverSocket);
00611                         int clientConnection;
00612                         
00613                         // Write some random data to wake up the server.
00614                         channel.writeRaw("x", 1);
00615                         
00616                         clientConnection = channel.readFileDescriptor();
00617                         return ptr(new Client(clientConnection));
00618                 } catch (const SystemException &e) {
00619                         throw SystemException("Could not connect to the ApplicationPool server", e.code());
00620                 } catch (const IOException &e) {
00621                         string message("Could not connect to the ApplicationPool server: ");
00622                         message.append(e.what());
00623                         throw IOException(message);
00624                 }
00625         }
00626         
00627         /**
00628          * Detach the server, thereby telling it that we don't want to connect
00629          * to it anymore. This frees up some resources in the current process,
00630          * such as file descriptors.
00631          *
00632          * This method is particularily useful to Apache worker processes that
00633          * have just established a connection with the ApplicationPool server.
00634          * Any sessions that are opened prior to calling detach(), will keep
00635          * working even after a detach().
00636          *
00637          * This method may only be called once. The ApplicationPoolServer object
00638          * will become unusable once detach() has been called, so call connect()
00639          * before calling detach().
00640          */
00641         void detach() {
00642                 int ret;
00643                 do {
00644                         ret = close(serverSocket);
00645                 } while (ret == -1 && errno == EINTR);
00646                 serverSocket = -1;
00647         }
00648 };
00649 
00650 typedef shared_ptr<ApplicationPoolServer> ApplicationPoolServerPtr;
00651 
00652 } // namespace Passenger
00653 
00654 #endif /* _PASSENGER_APPLICATION_POOL_SERVER_H_ */

Generated on Mon Jun 9 13:34:30 2008 for Passenger by  doxygen 1.5.3