::const_iterator it;
- const char *uri = r->uri;
- size_t uri_len = strlen(uri);
-
- if (uri_len == 0 || uri[0] != '/') {
- baseURIKnown = true;
- return NULL;
- }
-
- for (it = config->railsBaseURIs.begin(); it != config->railsBaseURIs.end(); it++) {
- const string &base(*it);
- if ( base == "/"
- || ( uri_len == base.size() && memcmp(uri, base.c_str(), uri_len) == 0 )
- || ( uri_len > base.size() && memcmp(uri, base.c_str(), base.size()) == 0
- && uri[base.size()] == '/' )
- ) {
- baseURIKnown = true;
- baseURI = base.c_str();
- appType = RAILS;
- return baseURI;
- }
- }
-
- for (it = config->rackBaseURIs.begin(); it != config->rackBaseURIs.end(); it++) {
- const string &base(*it);
- if ( base == "/"
- || ( uri_len == base.size() && memcmp(uri, base.c_str(), uri_len) == 0 )
- || ( uri_len > base.size() && memcmp(uri, base.c_str(), base.size()) == 0
- && uri[base.size()] == '/' )
- ) {
- baseURIKnown = true;
- baseURI = base.c_str();
- appType = RACK;
- return baseURI;
- }
- }
+ struct AprDestructable {
+ virtual ~AprDestructable() { }
- if (shouldAutoDetectRails() && verifyRailsDir(ap_document_root(r))) {
- baseURIKnown = true;
- baseURI = "/";
- appType = RAILS;
- return baseURI;
- }
- if (shouldAutoDetectRack() && verifyRackDir(ap_document_root(r))) {
- baseURIKnown = true;
- baseURI = "/";
- appType = RACK;
- return baseURI;
- }
- if (shouldAutoDetectWSGI() && verifyWSGIDir(ap_document_root(r))) {
- baseURIKnown = true;
- baseURI = "/";
- appType = WSGI;
- return baseURI;
+ static apr_status_t cleanup(void *p) {
+ delete (AprDestructable *) p;
+ return APR_SUCCESS;
}
-
- baseURIKnown = true;
- return NULL;
- }
+ };
- /**
- * Returns the filename of the 'public' directory of the Rails/Rack application
- * that's associated with the HTTP request.
- *
- * Returns an empty string if the document root of the HTTP request
- * cannot be determined, or if it isn't a valid folder.
- *
- * @throws SystemException An error occured while examening the filesystem.
- */
- string getPublicDirectory() {
- if (!baseURIKnown) {
- getBaseURI();
- }
- if (baseURI == NULL) {
- return "";
- }
+ struct RequestNote: public AprDestructable {
+ DirectoryMapper mapper;
+ DirConfig *config;
+ bool forwardToBackend;
+ const char *handlerBeforeModRewrite;
+ char *filenameBeforeModRewrite;
+ apr_filetype_e oldFileType;
+ const char *handlerBeforeModAutoIndex;
- const char *docRoot = ap_document_root(r);
- size_t len = strlen(docRoot);
- if (len > 0) {
- string path;
- if (docRoot[len - 1] == '/') {
- path.assign(docRoot, len - 1);
- } else {
- path.assign(docRoot, len);
- }
- if (strcmp(baseURI, "/") != 0) {
- path.append(baseURI);
- }
- return path;
- } else {
- return "";
+ RequestNote(const DirectoryMapper &m)
+ : mapper(m) {
+ forwardToBackend = false;
+ filenameBeforeModRewrite = NULL;
}
- }
+ };
- /**
- * Returns the application type that's associated with the HTTP request.
- *
- * @throws SystemException An error occured while examening the filesystem.
- */
- ApplicationType getApplicationType() {
- if (!baseURIKnown) {
- getBaseURI();
- }
- return appType;
- }
+ struct ErrorReport: public AprDestructable {
+ virtual int report(request_rec *r) = 0;
+ };
- /**
- * Returns the application type (as a string) that's associated
- * with the HTTP request.
- *
- * @throws SystemException An error occured while examening the filesystem.
- */
- const char *getApplicationTypeString() {
- if (!baseURIKnown) {
- getBaseURI();
- }
- switch (appType) {
- case RAILS:
- return "rails";
- case RACK:
- return "rack";
- case WSGI:
- return "wsgi";
- default:
- return NULL;
- };
- }
-};
-
-
-/**
- * Apache hook functions, wrapped in a class.
- *
- * @ingroup Core
- */
-class Hooks {
-private:
- struct Container {
- Application::SessionPtr session;
+ struct ReportFileSystemError: public ErrorReport {
+ FileSystemException e;
- static apr_status_t cleanup(void *p) {
- try {
- this_thread::disable_interruption di;
- this_thread::disable_syscall_interruption dsi;
- delete (Container *) p;
- } catch (const thread_interrupted &) {
- P_TRACE(3, "A system call was interrupted during closing "
- "of a session. Apache is probably restarting or "
- "shutting down.");
- } catch (const exception &e) {
- P_TRACE(3, "Exception during closing of a session: " <<
- e.what());
+ ReportFileSystemError(const FileSystemException &ex): e(ex) { }
+
+ int report(request_rec *r) {
+ ap_set_content_type(r, "text/html; charset=UTF-8");
+ ap_rputs("Passenger error #2
\n", r);
+ ap_rputs("An error occurred while trying to access '", r);
+ ap_rputs(ap_escape_html(r->pool, e.filename().c_str()), r);
+ ap_rputs("': ", r);
+ ap_rputs(ap_escape_html(r->pool, e.what()), r);
+ if (e.code() == EPERM) {
+ ap_rputs("", r);
+ ap_rputs("Apache doesn't have read permissions to that file. ", r);
+ ap_rputs("Please fix the relevant file permissions.", r);
+ ap_rputs("
", r);
}
- return APR_SUCCESS;
+ P_ERROR("A filesystem exception occured.\n" <<
+ " Message: " << e.what() << "\n" <<
+ " Backtrace:\n" << e.backtrace());
+ return OK;
}
};
+
+ enum Threeway { YES, NO, UNKNOWN };
ApplicationPoolServerPtr applicationPoolServer;
thread_specific_ptr threadSpecificApplicationPool;
+ Threeway m_hasModRewrite, m_hasModDir, m_hasModAutoIndex;
+ CachedMultiFileStat *mstat;
- DirConfig *getDirConfig(request_rec *r) {
+ inline DirConfig *getDirConfig(request_rec *r) {
return (DirConfig *) ap_get_module_config(r->per_dir_config, &passenger_module);
}
- ServerConfig *getServerConfig(server_rec *s) {
+ inline ServerConfig *getServerConfig(server_rec *s) {
return (ServerConfig *) ap_get_module_config(s->module_config, &passenger_module);
}
+ inline RequestNote *getRequestNote(request_rec *r) {
+ // The union is needed in order to be compliant with
+ // C99/C++'s strict aliasing rules. http://tinyurl.com/g5hgh
+ union {
+ RequestNote *note;
+ void *pointer;
+ } u;
+ u.note = 0;
+ apr_pool_userdata_get(&u.pointer, "Phusion Passenger", r->pool);
+ return u.note;
+ }
+
/**
+ * Returns a usable ApplicationPool object.
+ *
* When using the worker MPM and global queuing, deadlocks can occur, for
* the same reason described in ApplicationPoolServer::connect(). This
* method allows us to avoid this deadlock by making sure that each
* thread gets its own connection to the application pool server.
+ *
+ * It also checks whether the currently cached ApplicationPool object
+ * is disconnected (which can happen if an error previously occured).
+ * If so, it will reconnect to the ApplicationPool server.
*/
ApplicationPoolPtr getApplicationPool() {
ApplicationPoolPtr *pool_ptr = threadSpecificApplicationPool.get();
if (pool_ptr == NULL) {
pool_ptr = new ApplicationPoolPtr(applicationPoolServer->connect());
threadSpecificApplicationPool.reset(pool_ptr);
+ } else if (!(*pool_ptr)->connected()) {
+ P_DEBUG("Reconnecting to ApplicationPool server");
+ *pool_ptr = applicationPoolServer->connect();
}
return *pool_ptr;
}
+ bool hasModRewrite() {
+ if (m_hasModRewrite == UNKNOWN) {
+ if (ap_find_linked_module("mod_rewrite.c")) {
+ m_hasModRewrite = YES;
+ } else {
+ m_hasModRewrite = NO;
+ }
+ }
+ return m_hasModRewrite == YES;
+ }
+
+ bool hasModDir() {
+ if (m_hasModDir == UNKNOWN) {
+ if (ap_find_linked_module("mod_dir.c")) {
+ m_hasModDir = YES;
+ } else {
+ m_hasModDir = NO;
+ }
+ }
+ return m_hasModDir == YES;
+ }
+
+ bool hasModAutoIndex() {
+ if (m_hasModAutoIndex == UNKNOWN) {
+ if (ap_find_linked_module("mod_autoindex.c")) {
+ m_hasModAutoIndex = YES;
+ } else {
+ m_hasModAutoIndex = NO;
+ }
+ }
+ return m_hasModAutoIndex == YES;
+ }
+
int reportDocumentRootDeterminationError(request_rec *r) {
ap_set_content_type(r, "text/html; charset=UTF-8");
ap_rputs("Passenger error #1
\n", r);
@@ -327,28 +221,322 @@ class Hooks {
return OK;
}
- int reportFileSystemError(request_rec *r, const FileSystemException &e) {
- ap_set_content_type(r, "text/html; charset=UTF-8");
- ap_rputs("Passenger error #2
\n", r);
- ap_rputs("An error occurred while trying to access '", r);
- ap_rputs(ap_escape_html(r->pool, e.filename().c_str()), r);
- ap_rputs("': ", r);
- ap_rputs(ap_escape_html(r->pool, e.what()), r);
- if (e.code() == EPERM) {
- ap_rputs("", r);
- ap_rputs("Apache doesn't have read permissions to that file. ", r);
- ap_rputs("Please fix the relevant file permissions.", r);
- ap_rputs("
", r);
- }
- return OK;
- }
-
int reportBusyException(request_rec *r) {
ap_custom_response(r, HTTP_SERVICE_UNAVAILABLE,
"This website is too busy right now. Please try again later.");
return HTTP_SERVICE_UNAVAILABLE;
}
+ /**
+ * Gather some information about the request and do some preparations. In this method,
+ * it will be determined whether the request URI should be served statically by Apache
+ * (in case of static assets or in case there's a page cache file available) or
+ * whether it should be forwarded to the backend application.
+ *
+ * The strategy is as follows:
+ *
+ * We check whether Phusion Passenger is enabled for this URI (A).
+ * If so, then we check whether the following situations are true:
+ * (B) There is a backend application defined for this URI.
+ * (C) r->filename already exists.
+ * (D) There is a page cache file for the URI.
+ *
+ * - If A is not true, or if B is not true, or if C is true, then won't do anything.
+ * Passenger will be disabled during the rest of this request.
+ * - If D is true, then we first transform r->filename to the page cache file's
+ * filename, and then we let Apache serve it statically.
+ * - If D is not true, then we forward the request to the backend application.
+ *
+ * @pre The (A) condition must be true.
+ * @param coreModuleWillBeRun Whether the core.c map_to_storage hook might be called after this.
+ * @return Whether the Passenger handler hook method should be run.
+ */
+ bool prepareRequest(request_rec *r, DirConfig *config, const char *filename, bool coreModuleWillBeRun = false) {
+ TRACE_POINT();
+ DirectoryMapper mapper(r, config, mstat, config->getStatThrottleRate());
+ try {
+ if (mapper.getBaseURI() == NULL) {
+ // (B) is not true.
+ return false;
+ }
+ } catch (const FileSystemException &e) {
+ /* DirectoryMapper tried to examine the filesystem in order
+ * to autodetect the application type (e.g. by checking whether
+ * environment.rb exists. But something went wrong, probably
+ * because of a permission problem. This usually
+ * means that the user is trying to deploy an application, but
+ * set the wrong permissions on the relevant folders.
+ * Later, in the handler hook, we inform the user about this
+ * problem so that he can either disable Phusion Passenger's
+ * autodetection routines, or fix the permissions.
+ */
+ apr_pool_userdata_set(new ReportFileSystemError(e),
+ "Phusion Passenger: error report",
+ ReportFileSystemError::cleanup,
+ r->pool);
+ return true;
+ }
+
+ /* Save some information for the hook methods that are called later.
+ * The existance of this note indicates that the URI belongs to a Phusion
+ * Passenger-served application.
+ */
+ RequestNote *note = new RequestNote(mapper);
+ note->config = config;
+ apr_pool_userdata_set(note, "Phusion Passenger", RequestNote::cleanup, r->pool);
+
+ try {
+ // (B) is true.
+ FileType fileType = getFileType(filename);
+ if (fileType == FT_REGULAR) {
+ // (C) is true.
+ return false;
+ }
+
+ // (C) is not true. Check whether (D) is true.
+ char *pageCacheFile;
+ /* Only GET requests may hit the page cache. This is
+ * important because of REST conventions, e.g.
+ * 'POST /foo' maps to 'FooController#create',
+ * while 'GET /foo' maps to 'FooController#index'.
+ * We wouldn't want our page caching support to interfere
+ * with that.
+ */
+ if (r->method_number == M_GET) {
+ if (fileType == FT_DIRECTORY) {
+ size_t len;
+
+ len = strlen(filename);
+ if (len > 0 && filename[len - 1] == '/') {
+ pageCacheFile = apr_pstrcat(r->pool, filename,
+ "index.html", NULL);
+ } else {
+ pageCacheFile = apr_pstrcat(r->pool, filename,
+ ".html", NULL);
+ }
+ } else {
+ pageCacheFile = apr_pstrcat(r->pool, filename,
+ ".html", NULL);
+ }
+ if (!fileExists(pageCacheFile)) {
+ pageCacheFile = NULL;
+ }
+ } else {
+ pageCacheFile = NULL;
+ }
+ if (pageCacheFile != NULL) {
+ // (D) is true.
+ r->filename = pageCacheFile;
+ r->canonical_filename = pageCacheFile;
+ if (!coreModuleWillBeRun) {
+ r->finfo.filetype = APR_NOFILE;
+ ap_set_content_type(r, "text/html");
+ ap_directory_walk(r);
+ ap_file_walk(r);
+ }
+ return false;
+ } else {
+ // (D) is not true.
+ note->forwardToBackend = true;
+ return true;
+ }
+ } catch (const FileSystemException &e) {
+ /* Something went wrong while accessing the directory in which
+ * r->filename lives. We already know that this URI belongs to
+ * a backend application, so this error probably means that the
+ * user set the wrong permissions for his 'public' folder. We
+ * don't let the handler hook run so that Apache can decide how
+ * to display the error.
+ */
+ return false;
+ }
+ }
+
+ /**
+ * Most of the high-level logic for forwarding a request to a backend application
+ * is contained in this method.
+ */
+ int handleRequest(request_rec *r) {
+ /* Check whether an error occured in prepareRequest() that should be reported
+ * to the browser.
+ */
+
+ // The union is needed in order to be compliant with
+ // C99/C++'s strict aliasing rules. http://tinyurl.com/g5hgh
+ union {
+ ErrorReport *errorReport;
+ void *pointer;
+ } u;
+
+ u.errorReport = 0;
+ apr_pool_userdata_get(&u.pointer, "Phusion Passenger: error report", r->pool);
+ if (u.errorReport != 0) {
+ return u.errorReport->report(r);
+ }
+
+ RequestNote *note = getRequestNote(r);
+ if (note == 0 || !note->forwardToBackend) {
+ return DECLINED;
+ } else if (r->handler != NULL && strcmp(r->handler, "redirect-handler") == 0) {
+ // mod_rewrite is at work.
+ return DECLINED;
+ }
+
+ TRACE_POINT();
+ DirConfig *config = note->config;
+ DirectoryMapper &mapper(note->mapper);
+
+ if (mapper.getPublicDirectory().empty()) {
+ return reportDocumentRootDeterminationError(r);
+ }
+
+ int httpStatus = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR);
+ if (httpStatus != OK) {
+ return httpStatus;
+ }
+
+ try {
+ this_thread::disable_interruption di;
+ this_thread::disable_syscall_interruption dsi;
+ apr_bucket_brigade *bb;
+ apr_bucket *b;
+ Application::SessionPtr session;
+ bool expectingUploadData;
+ shared_ptr uploadData;
+
+ expectingUploadData = ap_should_client_block(r);
+ if (expectingUploadData && atol(lookupHeader(r, "Content-Length"))
+ > UPLOAD_ACCELERATION_THRESHOLD) {
+ uploadData = receiveRequestBody(r);
+ }
+
+ UPDATE_TRACE_POINT();
+ try {
+ ServerConfig *sconfig = getServerConfig(r->server);
+ string appRoot(canonicalizePath(
+ config->getAppRoot(mapper.getPublicDirectory().c_str())
+ ));
+
+ session = getApplicationPool()->get(PoolOptions(
+ appRoot,
+ true,
+ sconfig->getDefaultUser(),
+ mapper.getEnvironment(),
+ config->getSpawnMethodString(),
+ mapper.getApplicationTypeString(),
+ config->frameworkSpawnerTimeout,
+ config->appSpawnerTimeout,
+ config->getMaxRequests(),
+ config->getMemoryLimit(),
+ config->usingGlobalQueue(),
+ config->getStatThrottleRate(),
+ config->getRestartDir()
+ ));
+ P_TRACE(3, "Forwarding " << r->uri << " to PID " << session->getPid());
+ } catch (const SpawnException &e) {
+ r->status = 500;
+ if (e.hasErrorPage()) {
+ ap_set_content_type(r, "text/html; charset=utf-8");
+ ap_rputs(e.getErrorPage().c_str(), r);
+ return OK;
+ } else {
+ throw;
+ }
+ } catch (const FileSystemException &e) {
+ /* The application root cannot be determined. This could
+ * happen if, for example, the user specified 'RailsBaseURI /foo'
+ * while there is no filesystem entry called "foo" in the virtual
+ * host's document root.
+ */
+ return ReportFileSystemError(e).report(r);
+ } catch (const BusyException &e) {
+ return reportBusyException(r);
+ }
+
+ UPDATE_TRACE_POINT();
+ session->setReaderTimeout(r->server->timeout / 1000);
+ session->setWriterTimeout(r->server->timeout / 1000);
+ sendHeaders(r, session, mapper.getBaseURI());
+ if (expectingUploadData) {
+ if (uploadData != NULL) {
+ sendRequestBody(r, session, uploadData);
+ uploadData.reset();
+ } else {
+ sendRequestBody(r, session);
+ }
+ }
+ session->shutdownWriter();
+
+ UPDATE_TRACE_POINT();
+ apr_file_t *readerPipe = NULL;
+ int reader = session->getStream();
+ pid_t backendPid = session->getPid();
+ apr_os_pipe_put(&readerPipe, &reader, r->pool);
+ apr_file_pipe_timeout_set(readerPipe, r->server->timeout);
+
+ bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc);
+ b = passenger_bucket_create(session, readerPipe, r->connection->bucket_alloc);
+ session.reset();
+ APR_BRIGADE_INSERT_TAIL(bb, b);
+
+ b = apr_bucket_eos_create(r->connection->bucket_alloc);
+ APR_BRIGADE_INSERT_TAIL(bb, b);
+
+ // I know the size because I read util_script.c's source. :-(
+ char backendData[MAX_STRING_LEN];
+ int result = ap_scan_script_header_err_brigade(r, bb, backendData);
+ if (result == OK) {
+ // The API documentation for ap_scan_script_err_brigade() says it
+ // returns HTTP_OK on success, but it actually returns OK.
+
+ // Manually set the Status header because
+ // ap_scan_script_header_err_brigade() filters it
+ // out. Some broken HTTP clients depend on the
+ // Status header for retrieving the HTTP status.
+ if (!r->status_line && *r->status_line == '\0') {
+ r->status_line = apr_psprintf(r->pool,
+ "%d Unknown Status",
+ r->status);
+ }
+ apr_table_setn(r->headers_out, "Status", r->status_line);
+
+ ap_pass_brigade(r->output_filters, bb);
+ return OK;
+ } else if (backendData[0] == '\0') {
+ P_ERROR("Backend process " << backendPid <<
+ " did not return a valid HTTP response. It returned no data.");
+ apr_table_setn(r->err_headers_out, "Status", "500 Internal Server Error");
+ return HTTP_INTERNAL_SERVER_ERROR;
+ } else {
+ P_ERROR("Backend process " << backendPid <<
+ " did not return a valid HTTP response. It returned: [" <<
+ backendData << "]");
+ apr_table_setn(r->err_headers_out, "Status", "500 Internal Server Error");
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ } catch (const thread_interrupted &e) {
+ P_TRACE(3, "A system call was interrupted during an HTTP request. Apache "
+ "is probably restarting or shutting down. Backtrace:\n" <<
+ e.backtrace());
+ return HTTP_INTERNAL_SERVER_ERROR;
+
+ } catch (const tracable_exception &e) {
+ P_ERROR("Unexpected error in mod_passenger: " <<
+ e.what() << "\n" << " Backtrace:\n" << e.backtrace());
+ return HTTP_INTERNAL_SERVER_ERROR;
+
+ } catch (const exception &e) {
+ P_ERROR("Unexpected error in mod_passenger: " <<
+ e.what() << "\n" << " Backtrace: not available");
+ return HTTP_INTERNAL_SERVER_ERROR;
+
+ } catch (...) {
+ P_ERROR("An unexpected, unknown error occured in mod_passenger.");
+ throw;
+ }
+ }
+
/**
* Convert an HTTP header name to a CGI environment name.
*/
@@ -391,32 +579,6 @@ class Hooks {
return lookupName(r->subprocess_env, name);
}
- // This code is a duplicate of what's in util_script.c. We can't use
- // r->unparsed_uri because it gets changed if there was a redirect.
- char *originalURI(request_rec *r) {
- char *first, *last;
-
- if (r->the_request == NULL) {
- return (char *) apr_pcalloc(r->pool, 1);
- }
-
- first = r->the_request; // use the request-line
-
- while (*first && !apr_isspace(*first)) {
- ++first; // skip over the method
- }
- while (apr_isspace(*first)) {
- ++first; // and the space(s)
- }
-
- last = first;
- while (*last && !apr_isspace(*last)) {
- ++last; // end at next whitespace
- }
-
- return apr_pstrmemdup(r->pool, first, last - first);
- }
-
void inline addHeader(apr_table_t *table, const char *name, const char *value) {
if (name != NULL && value != NULL) {
apr_table_addn(table, name, value);
@@ -441,7 +603,7 @@ class Hooks {
addHeader(headers, "REMOTE_PORT", apr_psprintf(r->pool, "%d", r->connection->remote_addr->port));
addHeader(headers, "REMOTE_USER", r->user);
addHeader(headers, "REQUEST_METHOD", r->method);
- addHeader(headers, "REQUEST_URI", originalURI(r));
+ addHeader(headers, "REQUEST_URI", r->unparsed_uri);
addHeader(headers, "QUERY_STRING", r->args ? r->args : "");
if (strcmp(baseURI, "/") != 0) {
addHeader(headers, "SCRIPT_NAME", baseURI);
@@ -518,21 +680,38 @@ class Hooks {
}
shared_ptr receiveRequestBody(request_rec *r) {
+ TRACE_POINT();
shared_ptr tempFile(new TempFile());
char buf[1024 * 32];
apr_off_t len;
+ size_t total_written = 0;
while ((len = ap_get_client_block(r, buf, sizeof(buf))) > 0) {
size_t written = 0;
do {
size_t ret = fwrite(buf, 1, len - written, tempFile->handle);
- if (ret == 0) {
- throw SystemException("An error occured while writing "
- "HTTP upload data to a temporary file",
- errno);
+ if (ret <= 0 || fflush(tempFile->handle) == EOF) {
+ int e = errno;
+ string message("An error occured while "
+ "buffering HTTP upload data to "
+ "a temporary file in ");
+ message.append(getTempDir());
+ if (e == ENOSPC) {
+ message.append(". Please make sure "
+ "that this directory has "
+ "enough disk space for "
+ "buffering file uploads, "
+ "or set the 'PassengerTempDir' "
+ "directive to a directory "
+ "that has enough disk space.");
+ throw RuntimeException(message);
+ } else {
+ throw SystemException(message, e);
+ }
}
written += ret;
} while (written < (size_t) len);
+ total_written += written;
}
if (len == -1) {
throw IOException("An error occurred while receiving HTTP upload data.");
@@ -544,8 +723,9 @@ class Hooks {
}
void sendRequestBody(request_rec *r, Application::SessionPtr &session, shared_ptr &uploadData) {
+ TRACE_POINT();
rewind(uploadData->handle);
- P_DEBUG("Content-Length = " << lookupHeader(r, "Content-Length"));
+ P_DEBUG("File upload: Content-Length = " << lookupHeader(r, "Content-Length"));
while (!feof(uploadData->handle)) {
char buf[1024 * 32];
size_t size;
@@ -573,6 +753,10 @@ class Hooks {
passenger_config_merge_all_servers(pconf, s);
ServerConfig *config = getServerConfig(s);
Passenger::setLogLevel(config->logLevel);
+ m_hasModRewrite = UNKNOWN;
+ m_hasModDir = UNKNOWN;
+ m_hasModAutoIndex = UNKNOWN;
+ mstat = cached_multi_file_stat_new(1024);
P_DEBUG("Initializing Phusion Passenger...");
ap_add_version_component(pconf, "Phusion_Passenger/" PASSENGER_VERSION);
@@ -580,6 +764,22 @@ class Hooks {
const char *ruby, *user;
string applicationPoolServerExe, spawnServer;
+ if (config->tempDir != NULL) {
+ setenv("TMPDIR", config->tempDir, 1);
+ } else {
+ unsetenv("TMPDIR");
+ }
+ /*
+ * As described in the comment in init_module, upon (re)starting
+ * Apache, the Hooks constructor is called twice. We unset
+ * PHUSION_PASSENGER_TMP before calling createPassengerTmpDir()
+ * because we want the temp directory's name to contain the PID
+ * of the process in which the Hooks constructor was called for
+ * the second time.
+ */
+ unsetenv("PHUSION_PASSENGER_TMP");
+ createPassengerTempDir();
+
ruby = (config->ruby != NULL) ? config->ruby : DEFAULT_RUBY_COMMAND;
if (config->userSwitching) {
user = "";
@@ -623,217 +823,204 @@ class Hooks {
pool->setMax(config->maxPoolSize);
pool->setMaxPerApp(config->maxInstancesPerApp);
pool->setMaxIdleTime(config->poolIdleTime);
- pool->setUseGlobalQueue(config->getUseGlobalQueue());
}
- int handleRequest(request_rec *r) {
+ ~Hooks() {
+ cached_multi_file_stat_free(mstat);
+ removeDirTree(getPassengerTempDir().c_str());
+ }
+
+ int prepareRequestWhenInHighPerformanceMode(request_rec *r) {
DirConfig *config = getDirConfig(r);
- DirectoryMapper mapper(r, config);
- if (mapper.getBaseURI() == NULL || r->filename == NULL || fileExists(r->filename)) {
+ if (config->isEnabled() && config->highPerformanceMode()) {
+ if (prepareRequest(r, config, r->filename, true)) {
+ return OK;
+ } else {
+ return DECLINED;
+ }
+ } else {
return DECLINED;
}
-
- try {
- if (mapper.getPublicDirectory().empty()) {
- return reportDocumentRootDeterminationError(r);
+ }
+
+ /**
+ * This is the hook method for the map_to_storage hook. Apache's final map_to_storage hook
+ * method (defined in core.c) will do the following:
+ *
+ * If r->filename doesn't exist, then it will change the filename to the
+ * following form:
+ *
+ * A/B
+ *
+ * A is top-most directory that exists. B is the first filename piece that
+ * normally follows A. For example, suppose that a website's DocumentRoot
+ * is /website, on server http://test.com/. Suppose that there's also a
+ * directory /website/images.
+ *
+ * If we access: then r->filename will be:
+ * http://test.com/foo/bar /website/foo
+ * http://test.com/foo/bar/baz /website/foo
+ * http://test.com/images/foo/bar /website/images/foo
+ *
+ * We obviously don't want this to happen because it'll interfere with our page
+ * cache file search code. So here we save the original value of r->filename so
+ * that we can use it later.
+ */
+ int saveOriginalFilename(request_rec *r) {
+ apr_table_set(r->notes, "Phusion Passenger: original filename", r->filename);
+ return DECLINED;
+ }
+
+ int prepareRequestWhenNotInHighPerformanceMode(request_rec *r) {
+ DirConfig *config = getDirConfig(r);
+ if (config->isEnabled()) {
+ if (config->highPerformanceMode()) {
+ /* Preparations have already been done in the map_to_storage hook.
+ * Prevent other modules' fixups hooks from being run.
+ */
+ return OK;
+ } else {
+ // core.c's map_to_storage hook will transform the filename, as
+ // described by saveOriginalFilename(). Here we restore the
+ // original filename.
+ const char *filename = apr_table_get(r->notes, "Phusion Passenger: original filename");
+ if (filename == NULL) {
+ return DECLINED;
+ } else {
+ prepareRequest(r, config, filename);
+ return DECLINED;
+ }
}
- } catch (const FileSystemException &e) {
- return reportFileSystemError(r, e);
+ } else {
+ return DECLINED;
}
-
- int httpStatus = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR);
- if (httpStatus != OK) {
- return httpStatus;
+ }
+
+ /**
+ * The default .htaccess provided by on Rails on Rails (that is, before version 2.1.0)
+ * has the following mod_rewrite rules in it:
+ *
+ * RewriteEngine on
+ * RewriteRule ^$ index.html [QSA]
+ * RewriteRule ^([^.]+)$ $1.html [QSA]
+ * RewriteCond %{REQUEST_FILENAME} !-f
+ * RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
+ *
+ * As a result, all requests that do not map to a filename will be redirected to
+ * dispatch.cgi (or dispatch.fcgi, if the user so specified). We don't want that
+ * to happen, so before mod_rewrite applies its rules, we save the current state.
+ * After mod_rewrite has applied its rules, undoRedirectionToDispatchCgi() will
+ * check whether mod_rewrite attempted to perform an internal redirection to
+ * dispatch.(f)cgi. If so, then it will revert the state to the way it was before
+ * mod_rewrite took place.
+ */
+ int saveStateBeforeRewriteRules(request_rec *r) {
+ RequestNote *note = getRequestNote(r);
+ if (note != 0 && hasModRewrite()) {
+ note->handlerBeforeModRewrite = r->handler;
+ note->filenameBeforeModRewrite = r->filename;
+ }
+ return DECLINED;
+ }
+
+ int undoRedirectionToDispatchCgi(request_rec *r) {
+ RequestNote *note = getRequestNote(r);
+ if (note == 0 || !hasModRewrite()) {
+ return DECLINED;
}
- try {
- this_thread::disable_interruption di;
- this_thread::disable_syscall_interruption dsi;
- apr_bucket_brigade *bb;
- apr_bucket *b;
- Application::SessionPtr session;
- bool expectingUploadData;
- shared_ptr uploadData;
-
- expectingUploadData = ap_should_client_block(r);
- if (expectingUploadData && atol(lookupHeader(r, "Content-Length"))
- > UPLOAD_ACCELERATION_THRESHOLD) {
- uploadData = receiveRequestBody(r);
- }
-
- try {
- const char *defaultUser, *environment, *spawnMethod;
- ServerConfig *sconfig;
-
- sconfig = getServerConfig(r->server);
- if (sconfig->defaultUser != NULL) {
- defaultUser = sconfig->defaultUser;
- } else {
- defaultUser = "nobody";
- }
- if (mapper.getApplicationType() == DirectoryMapper::RAILS) {
- if (config->railsEnv == NULL) {
- environment = DEFAULT_RAILS_ENV;
- } else {
- environment = config->railsEnv;
- }
- } else if (mapper.getApplicationType() == DirectoryMapper::RACK) {
- if (config->rackEnv == NULL) {
- environment = DEFAULT_RACK_ENV;
- } else {
- environment = config->rackEnv;
- }
- } else {
- environment = DEFAULT_WSGI_ENV;
- }
- if (config->spawnMethod == DirConfig::SM_CONSERVATIVE) {
- spawnMethod = "conservative";
- } else {
- spawnMethod = "smart";
- }
-
- session = getApplicationPool()->get(
- canonicalizePath(mapper.getPublicDirectory() + "/.."),
- true, defaultUser, environment, spawnMethod,
- mapper.getApplicationTypeString());
- P_TRACE(3, "Forwarding " << r->uri << " to PID " << session->getPid());
- } catch (const SpawnException &e) {
- if (e.hasErrorPage()) {
- r->status = 500;
- ap_set_content_type(r, "text/html; charset=utf-8");
- ap_rputs(e.getErrorPage().c_str(), r);
- return OK;
- } else {
- throw;
- }
- } catch (const BusyException &e) {
- return reportBusyException(r);
- }
- sendHeaders(r, session, mapper.getBaseURI());
- if (expectingUploadData) {
- if (uploadData != NULL) {
- sendRequestBody(r, session, uploadData);
- uploadData.reset();
- } else {
- sendRequestBody(r, session);
+ if (r->handler != NULL && strcmp(r->handler, "redirect-handler") == 0) {
+ // Check whether r->filename looks like "redirect:.../dispatch.(f)cgi"
+ size_t len = strlen(r->filename);
+ // 22 == strlen("redirect:/dispatch.cgi")
+ if (len >= 22 && memcmp(r->filename, "redirect:", 9) == 0
+ && (memcmp(r->filename + len - 13, "/dispatch.cgi", 13) == 0
+ || memcmp(r->filename + len - 14, "/dispatch.fcgi", 14) == 0)) {
+ if (note->filenameBeforeModRewrite != NULL) {
+ r->filename = note->filenameBeforeModRewrite;
+ r->canonical_filename = note->filenameBeforeModRewrite;
+ r->handler = note->handlerBeforeModRewrite;
}
}
- session->shutdownWriter();
-
- apr_file_t *readerPipe = NULL;
- int reader = session->getStream();
- apr_os_pipe_put(&readerPipe, &reader, r->pool);
-
- bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc);
- b = passenger_bucket_create(readerPipe, r->connection->bucket_alloc);
- APR_BRIGADE_INSERT_TAIL(bb, b);
-
- b = apr_bucket_eos_create(r->connection->bucket_alloc);
- APR_BRIGADE_INSERT_TAIL(bb, b);
-
- ap_scan_script_header_err_brigade(r, bb, NULL);
- ap_pass_brigade(r->output_filters, bb);
-
- Container *container = new Container();
- container->session = session;
- apr_pool_cleanup_register(r->pool, container, Container::cleanup, apr_pool_cleanup_null);
-
- return OK;
- } catch (const thread_interrupted &) {
- P_TRACE(3, "A system call was interrupted during an HTTP request. Apache "
- "is probably restarting or shutting down.");
- return HTTP_INTERNAL_SERVER_ERROR;
- } catch (const exception &e) {
- ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "*** Unexpected error in Passenger: %s", e.what());
- return HTTP_INTERNAL_SERVER_ERROR;
}
+ return DECLINED;
+ }
+
+ /**
+ * mod_dir does the following:
+ * If r->filename is a directory, and the URI doesn't end with a slash,
+ * then it will redirect the browser to an URI with a slash. For example,
+ * if you go to http://foo.com/images, then it will redirect you to
+ * http://foo.com/images/.
+ *
+ * This behavior is undesired. Suppose that there is an ImagesController,
+ * and there's also a 'public/images' folder used for storing page cache
+ * files. Then we don't want mod_dir to perform the redirection.
+ *
+ * So in startBlockingModDir(), we temporarily change some fields in the
+ * request structure in order to block mod_dir. In endBlockingModDir() we
+ * revert those fields to their old value.
+ */
+ int startBlockingModDir(request_rec *r) {
+ RequestNote *note = getRequestNote(r);
+ if (note != 0 && hasModDir()) {
+ note->oldFileType = r->finfo.filetype;
+ r->finfo.filetype = APR_NOFILE;
+ }
+ return DECLINED;
+ }
+
+ int endBlockingModDir(request_rec *r) {
+ RequestNote *note = getRequestNote(r);
+ if (note != 0 && hasModDir()) {
+ r->finfo.filetype = note->oldFileType;
+ }
+ return DECLINED;
+ }
+
+ /**
+ * mod_autoindex will try to display a directory index for URIs that map to a directory.
+ * This is undesired because of page caching semantics. Suppose that a Rails application
+ * has an ImagesController which has page caching enabled, and thus also a 'public/images'
+ * directory. When the visitor visits /images we'll want the request to be forwarded to
+ * the Rails application, instead of displaying a directory index.
+ *
+ * So in this hook method, we temporarily change some fields in the request structure
+ * in order to block mod_autoindex. In endBlockingModAutoIndex(), we restore the request
+ * structure to its former state.
+ */
+ int startBlockingModAutoIndex(request_rec *r) {
+ RequestNote *note = getRequestNote(r);
+ if (note != 0 && hasModAutoIndex()) {
+ note->handlerBeforeModAutoIndex = r->handler;
+ r->handler = "";
+ }
+ return DECLINED;
+ }
+
+ int endBlockingModAutoIndex(request_rec *r) {
+ RequestNote *note = getRequestNote(r);
+ if (note != 0 && hasModAutoIndex()) {
+ r->handler = note->handlerBeforeModAutoIndex;
+ }
+ return DECLINED;
}
- int
- mapToStorage(request_rec *r) {
+ int handleRequestWhenInHighPerformanceMode(request_rec *r) {
DirConfig *config = getDirConfig(r);
- DirectoryMapper mapper(r, config);
- bool forwardToApplication;
-
- try {
- if (mapper.getBaseURI() == NULL || fileExists(r->filename)) {
- /*
- * fileExists():
- * If the file already exists, serve it directly.
- * This is for static assets like .css and .js files.
- */
- forwardToApplication = false;
- } else if (r->method_number == M_GET) {
- char *html_file;
- size_t len;
-
- len = strlen(r->filename);
- if (len > 0 && r->filename[len - 1] == '/') {
- html_file = apr_pstrcat(r->pool, r->filename, "index.html", NULL);
- } else {
- html_file = apr_pstrcat(r->pool, r->filename, ".html", NULL);
- }
- if (fileExists(html_file)) {
- /* If a .html version of the URI exists, serve it directly.
- * We're essentially accelerating Rails page caching.
- */
- r->filename = html_file;
- r->canonical_filename = html_file;
- forwardToApplication = false;
- } else {
- forwardToApplication = true;
- }
- } else {
- /*
- * Non-GET requests are always forwarded to the application.
- * This important because of REST conventions, e.g.
- * 'POST /foo' maps to 'FooController.create',
- * while 'GET /foo' maps to 'FooController.index'.
- * We wouldn't want our page caching support to interfere
- * with that.
- */
- forwardToApplication = true;
- }
-
- if (forwardToApplication) {
- /* Apache's default map_to_storage process does strange
- * things with the filename. Suppose that the DocumentRoot
- * is /website, on server http://test.com/. If we access
- * http://test.com/foo/bar, and /website/foo/bar does not
- * exist, then Apache will change the filename to
- * /website/foo instead of the expected /website/bar.
- * We make sure that doesn't happen.
- *
- * Incidentally, this also disables mod_rewrite. That is a
- * good thing because the default Rails .htaccess file
- * interferes with Passenger anyway (it delegates requests
- * to the CGI script dispatch.cgi).
- */
- if (config->allowModRewrite != DirConfig::ENABLED
- && mapper.getApplicationType() == DirectoryMapper::RAILS) {
- /* Of course, we only do that if all of the following
- * are true:
- * - the config allows us to. Some people have complex
- * mod_rewrite rules that they don't want to abandon.
- * Those people will have to make sure that the Rails
- * app's .htaccess doesn't interfere.
- * - this is a Rails application.
- */
- return OK;
- } else if (strcmp(r->uri, mapper.getBaseURI()) == 0) {
- /* If the request URI is the application's base URI,
- * then we'll want to take over control. Otherwise,
- * Apache will show a directory listing. This fixes issue #11.
- */
- return OK;
- } else {
- return DECLINED;
- }
- } else {
- return DECLINED;
- }
- } catch (const FileSystemException &e) {
+ if (config->highPerformanceMode()) {
+ return handleRequest(r);
+ } else {
+ return DECLINED;
+ }
+ }
+
+ int handleRequestWhenNotInHighPerformanceMode(request_rec *r) {
+ DirConfig *config = getDirConfig(r);
+ if (config->highPerformanceMode()) {
return DECLINED;
+ } else {
+ return handleRequest(r);
}
}
};
@@ -900,9 +1087,10 @@ init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *
apr_pool_cleanup_null);
return OK;
- } catch (const thread_interrupted &) {
+ } catch (const thread_interrupted &e) {
P_TRACE(2, "A system call was interrupted during mod_passenger "
- "initialization. Apache might be restarting or shutting down.");
+ "initialization. Apache might be restarting or shutting "
+ "down. Backtrace:\n" << e.backtrace());
return DECLINED;
} catch (const thread_resource_error &e) {
@@ -911,7 +1099,14 @@ init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *
lim.rlim_cur = 0;
lim.rlim_max = 0;
+
+ /* Solaris does not define the RLIMIT_NPROC limit. Setting it to infinity... */
+#ifdef RLIMIT_NPROC
getrlimit(RLIMIT_NPROC, &lim);
+#else
+ lim.rlim_cur = lim.rlim_max = RLIM_INFINITY;
+#endif
+
#ifdef PTHREAD_THREADS_MAX
pthread_threads_max = toString(PTHREAD_THREADS_MAX);
#else
@@ -951,32 +1146,52 @@ init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *
}
}
-static int
-handle_request(request_rec *r) {
- if (hooks != NULL) {
- return hooks->handleRequest(r);
- } else {
- return DECLINED;
+#define DEFINE_REQUEST_HOOK(c_name, cpp_name) \
+ static int c_name(request_rec *r) { \
+ if (OXT_LIKELY(hooks != NULL)) { \
+ return hooks->cpp_name(r); \
+ } else { \
+ return DECLINED; \
+ } \
}
-}
-static int
-map_to_storage(request_rec *r) {
- if (hooks != NULL) {
- return hooks->mapToStorage(r);
- } else {
- return DECLINED;
- }
-}
+DEFINE_REQUEST_HOOK(prepare_request_when_in_high_performance_mode, prepareRequestWhenInHighPerformanceMode)
+DEFINE_REQUEST_HOOK(save_original_filename, saveOriginalFilename)
+DEFINE_REQUEST_HOOK(prepare_request_when_not_in_high_performance_mode, prepareRequestWhenNotInHighPerformanceMode)
+DEFINE_REQUEST_HOOK(save_state_before_rewrite_rules, saveStateBeforeRewriteRules)
+DEFINE_REQUEST_HOOK(undo_redirection_to_dispatch_cgi, undoRedirectionToDispatchCgi)
+DEFINE_REQUEST_HOOK(start_blocking_mod_dir, startBlockingModDir)
+DEFINE_REQUEST_HOOK(end_blocking_mod_dir, endBlockingModDir)
+DEFINE_REQUEST_HOOK(start_blocking_mod_autoindex, startBlockingModAutoIndex)
+DEFINE_REQUEST_HOOK(end_blocking_mod_autoindex, endBlockingModAutoIndex)
+DEFINE_REQUEST_HOOK(handle_request_when_in_high_performance_mode, handleRequestWhenInHighPerformanceMode)
+DEFINE_REQUEST_HOOK(handle_request_when_not_in_high_performance_mode, handleRequestWhenNotInHighPerformanceMode)
+
/**
* Apache hook registration function.
*/
void
passenger_register_hooks(apr_pool_t *p) {
+ static const char * const rewrite_module[] = { "mod_rewrite.c", NULL };
+ static const char * const dir_module[] = { "mod_dir.c", NULL };
+ static const char * const autoindex_module[] = { "mod_autoindex.c", NULL };
+
ap_hook_post_config(init_module, NULL, NULL, APR_HOOK_MIDDLE);
- ap_hook_map_to_storage(map_to_storage, NULL, NULL, APR_HOOK_FIRST);
- ap_hook_handler(handle_request, NULL, NULL, APR_HOOK_MIDDLE);
+
+ ap_hook_map_to_storage(prepare_request_when_in_high_performance_mode, NULL, NULL, APR_HOOK_FIRST);
+ ap_hook_map_to_storage(save_original_filename, NULL, NULL, APR_HOOK_LAST);
+
+ ap_hook_fixups(prepare_request_when_not_in_high_performance_mode, NULL, rewrite_module, APR_HOOK_FIRST);
+ ap_hook_fixups(save_state_before_rewrite_rules, NULL, rewrite_module, APR_HOOK_LAST);
+ ap_hook_fixups(undo_redirection_to_dispatch_cgi, rewrite_module, NULL, APR_HOOK_FIRST);
+ ap_hook_fixups(start_blocking_mod_dir, NULL, dir_module, APR_HOOK_MIDDLE);
+ ap_hook_fixups(end_blocking_mod_dir, dir_module, NULL, APR_HOOK_MIDDLE);
+
+ ap_hook_handler(handle_request_when_in_high_performance_mode, NULL, NULL, APR_HOOK_FIRST);
+ ap_hook_handler(start_blocking_mod_autoindex, NULL, autoindex_module, APR_HOOK_LAST);
+ ap_hook_handler(end_blocking_mod_autoindex, autoindex_module, NULL, APR_HOOK_FIRST);
+ ap_hook_handler(handle_request_when_not_in_high_performance_mode, NULL, NULL, APR_HOOK_LAST);
}
/**
diff --git a/ext/apache2/LICENSE-CNRI.TXT b/ext/apache2/LICENSE-CNRI.TXT
index ece10b0b..2778953a 100644
--- a/ext/apache2/LICENSE-CNRI.TXT
+++ b/ext/apache2/LICENSE-CNRI.TXT
@@ -2,6 +2,21 @@ A few functions in ext/apache2/Hooks.cpp are based on the source code of
mod_scgi version 1.9. Its license is included in this file.
Please note that these licensing terms *only* encompass those few
functions, and not Passenger as a whole.
+
+The functions which are based on mod_scgi's code are as follows:
+- Hooks::prepareRequest(). Although our version looks nothing like the
+ original, the idea of checking for the file's existance from the
+ map_to_storage/fixups hook is inspired by mod_scgi's code.
+- Hooks::handleRequest(). Although our version looks nothing like the original,
+ the idea of passing the backend process's socket file descriptor up to the
+ bucket brigade chain is inspired by mod_scgi's code.
+- Hooks::http2env(), Hooks::lookupName(), Hooks::lookupHeader(),
+ Hooks::lookupEnv(), Hooks::addHeader(): Copied from mod_scgi's functions that
+ are named similarly. Slightly modified to make the coding style consistent
+ with the rest of Phusion Passenger.
+- Hooks::sendHeaders(): Based for the most part on mod_scgi's send_headers()
+ function.
+
------------------------------------------------------------------------
CNRI OPEN SOURCE LICENSE AGREEMENT
diff --git a/ext/apache2/Logging.h b/ext/apache2/Logging.h
index 739b4868..8e90320e 100644
--- a/ext/apache2/Logging.h
+++ b/ext/apache2/Logging.h
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
#include
namespace Passenger {
@@ -39,24 +40,38 @@ void setLogLevel(unsigned int value);
void setDebugFile(const char *logFile = NULL);
/**
- * Write the given expression to the log stream.
+ * Write the given expression to the given stream.
+ *
+ * @param expr The expression to write.
+ * @param stream A pointer to an object that accepts the '<<' operator.
*/
-#define P_LOG(expr) \
+#define P_LOG_TO(expr, stream) \
do { \
- if (Passenger::_logStream != 0) { \
- time_t the_time = time(NULL); \
- struct tm *the_tm = localtime(&the_time); \
- char datetime_buf[60]; \
- struct timeval tv; \
- strftime(datetime_buf, sizeof(datetime_buf), "%x %H:%M:%S", the_tm); \
+ if (stream != 0) { \
+ time_t the_time; \
+ struct tm *the_tm; \
+ char datetime_buf[60]; \
+ struct timeval tv; \
+ std::stringstream sstream; \
+ \
+ the_time = time(NULL); \
+ the_tm = localtime(&the_time); \
+ strftime(datetime_buf, sizeof(datetime_buf), "%F %H:%M:%S", the_tm); \
gettimeofday(&tv, NULL); \
- *Passenger::_logStream << \
+ sstream << \
"[ pid=" << getpid() << " file=" << __FILE__ << ":" << __LINE__ << \
" time=" << datetime_buf << "." << (tv.tv_usec / 1000) << " ]:" << \
- "\n " << expr << std::endl; \
+ "\n " << expr << std::endl; \
+ *stream << sstream.str(); \
+ stream->flush(); \
} \
} while (false)
+/**
+ * Write the given expression to the log stream.
+ */
+#define P_LOG(expr) P_LOG_TO(expr, Passenger::_logStream)
+
/**
* Write the given expression, which represents a warning,
* to the log stream.
@@ -79,18 +94,7 @@ void setDebugFile(const char *logFile = NULL);
#define P_TRACE(level, expr) \
do { \
if (Passenger::_logLevel >= level) { \
- if (Passenger::_debugStream != 0) { \
- time_t the_time = time(NULL); \
- struct tm *the_tm = localtime(&the_time); \
- char datetime_buf[60]; \
- struct timeval tv; \
- strftime(datetime_buf, sizeof(datetime_buf), "%x %H:%M:%S", the_tm); \
- gettimeofday(&tv, NULL); \
- *Passenger::_debugStream << \
- "[ pid=" << getpid() << " file=" << __FILE__ << ":" << __LINE__ << \
- " time=" << datetime_buf << "." << (tv.tv_usec / 1000) << " ]:" << \
- "\n " << expr << std::endl; \
- } \
+ P_LOG_TO(expr, Passenger::_debugStream); \
} \
} while (false)
diff --git a/ext/apache2/MessageChannel.h b/ext/apache2/MessageChannel.h
index 740ef430..05e99069 100644
--- a/ext/apache2/MessageChannel.h
+++ b/ext/apache2/MessageChannel.h
@@ -20,6 +20,8 @@
#ifndef _PASSENGER_MESSAGE_CHANNEL_H_
#define _PASSENGER_MESSAGE_CHANNEL_H_
+#include
+
#include
#include
#include
@@ -31,14 +33,25 @@
#include
#include
#include
+#ifdef __OpenBSD__
+ // OpenBSD needs this for 'struct iovec'. Apparently it isn't
+ // always included by unistd.h and sys/types.h.
+ #include
+#endif
+#if !APR_HAVE_IOVEC
+ // We don't want apr_want.h to redefine 'struct iovec'.
+ // http://tinyurl.com/b6aatw
+ #undef APR_HAVE_IOVEC
+ #define APR_HAVE_IOVEC 1
+#endif
-#include "System.h"
#include "Exceptions.h"
#include "Utils.h"
namespace Passenger {
using namespace std;
+using namespace oxt;
/**
* Convenience class for I/O operations on file descriptors.
@@ -99,6 +112,11 @@ class MessageChannel {
private:
const static char DELIMITER = '\0';
int fd;
+
+ #ifdef __OpenBSD__
+ typedef u_int32_t uint32_t;
+ typedef u_int16_t uint16_t;
+ #endif
public:
/**
@@ -127,7 +145,7 @@ class MessageChannel {
*/
void close() {
if (fd != -1) {
- int ret = InterruptableCalls::close(fd);
+ int ret = syscalls::close(fd);
if (ret == -1) {
throw SystemException("Cannot close file descriptor", errno);
}
@@ -139,14 +157,18 @@ class MessageChannel {
* Send an array message, which consists of the given elements, over the underlying
* file descriptor.
*
- * @param args The message elements.
+ * @param args An object which contains the message elements. This object must
+ * support STL-style iteration, and each iterator must have an
+ * std::string as value. Use the StringArrayType and
+ * StringArrayConstIteratorType template parameters to specify the exact type names.
* @throws SystemException An error occured while writing the data to the file descriptor.
* @throws boost::thread_interrupted
* @pre None of the message elements may contain a NUL character ('\\0').
* @see read(), write(const char *, ...)
*/
- void write(const list &args) {
- list::const_iterator it;
+ template
+ void write(const StringArrayType &args) {
+ StringArrayConstIteratorType it;
string data;
uint16_t dataSize = 0;
@@ -164,6 +186,34 @@ class MessageChannel {
writeRaw(data);
}
+ /**
+ * Send an array message, which consists of the given elements, over the underlying
+ * file descriptor.
+ *
+ * @param args The message elements.
+ * @throws SystemException An error occured while writing the data to the file descriptor.
+ * @throws boost::thread_interrupted
+ * @pre None of the message elements may contain a NUL character ('\\0').
+ * @see read(), write(const char *, ...)
+ */
+ void write(const list &args) {
+ write, list::const_iterator>(args);
+ }
+
+ /**
+ * Send an array message, which consists of the given elements, over the underlying
+ * file descriptor.
+ *
+ * @param args The message elements.
+ * @throws SystemException An error occured while writing the data to the file descriptor.
+ * @throws boost::thread_interrupted
+ * @pre None of the message elements may contain a NUL character ('\\0').
+ * @see read(), write(const char *, ...)
+ */
+ void write(const vector &args) {
+ write, vector::const_iterator>(args);
+ }
+
/**
* Send an array message, which consists of the given strings, over the underlying
* file descriptor.
@@ -237,7 +287,7 @@ class MessageChannel {
ssize_t ret;
unsigned int written = 0;
do {
- ret = InterruptableCalls::write(fd, data + written, size - written);
+ ret = syscalls::write(fd, data + written, size - written);
if (ret == -1) {
throw SystemException("write() failed", errno);
} else {
@@ -273,7 +323,7 @@ class MessageChannel {
struct msghdr msg;
struct iovec vec;
char dummy[1];
- #ifdef __APPLE__
+ #if defined(__APPLE__) || defined(__SOLARIS__)
struct {
struct cmsghdr header;
int fd;
@@ -301,7 +351,7 @@ class MessageChannel {
control_header = CMSG_FIRSTHDR(&msg);
control_header->cmsg_level = SOL_SOCKET;
control_header->cmsg_type = SCM_RIGHTS;
- #ifdef __APPLE__
+ #if defined(__APPLE__) || defined(__SOLARIS__)
control_header->cmsg_len = sizeof(control_data);
control_data.fd = fileDescriptor;
#else
@@ -309,7 +359,7 @@ class MessageChannel {
memcpy(CMSG_DATA(control_header), &fileDescriptor, sizeof(int));
#endif
- ret = InterruptableCalls::sendmsg(fd, &msg, 0);
+ ret = syscalls::sendmsg(fd, &msg, 0);
if (ret == -1) {
throw SystemException("Cannot send file descriptor with sendmsg()", errno);
}
@@ -331,7 +381,7 @@ class MessageChannel {
unsigned int alreadyRead = 0;
do {
- ret = InterruptableCalls::read(fd, (char *) &size + alreadyRead, sizeof(size) - alreadyRead);
+ ret = syscalls::read(fd, (char *) &size + alreadyRead, sizeof(size) - alreadyRead);
if (ret == -1) {
throw SystemException("read() failed", errno);
} else if (ret == 0) {
@@ -346,7 +396,7 @@ class MessageChannel {
buffer.reserve(size);
while (buffer.size() < size) {
char tmp[1024 * 8];
- ret = InterruptableCalls::read(fd, tmp, min(size - buffer.size(), sizeof(tmp)));
+ ret = syscalls::read(fd, tmp, min(size - buffer.size(), sizeof(tmp)));
if (ret == -1) {
throw SystemException("read() failed", errno);
} else if (ret == 0) {
@@ -421,7 +471,7 @@ class MessageChannel {
unsigned int alreadyRead = 0;
while (alreadyRead < size) {
- ret = InterruptableCalls::read(fd, (char *) buf + alreadyRead, size - alreadyRead);
+ ret = syscalls::read(fd, (char *) buf + alreadyRead, size - alreadyRead);
if (ret == -1) {
throw SystemException("read() failed", errno);
} else if (ret == 0) {
@@ -449,7 +499,7 @@ class MessageChannel {
struct msghdr msg;
struct iovec vec;
char dummy[1];
- #ifdef __APPLE__
+ #if defined(__APPLE__) || defined(__SOLARIS__)
// File descriptor passing macros (CMSG_*) seem to be broken
// on 64-bit MacOS X. This structure works around the problem.
struct {
@@ -477,7 +527,7 @@ class MessageChannel {
msg.msg_controllen = sizeof(control_data);
msg.msg_flags = 0;
- ret = InterruptableCalls::recvmsg(fd, &msg, 0);
+ ret = syscalls::recvmsg(fd, &msg, 0);
if (ret == -1) {
throw SystemException("Cannot read file descriptor with recvmsg()", errno);
}
@@ -488,12 +538,71 @@ class MessageChannel {
|| control_header->cmsg_type != SCM_RIGHTS) {
throw IOException("No valid file descriptor received.");
}
- #ifdef __APPLE__
+ #if defined(__APPLE__) || defined(__SOLARIS__)
return control_data.fd;
#else
return *((int *) CMSG_DATA(control_header));
#endif
}
+
+ /**
+ * Set the timeout value for reading data from this channel.
+ * If no data can be read within the timeout period, then a
+ * SystemException will be thrown by one of the read methods,
+ * with error code EAGAIN or EWOULDBLOCK.
+ *
+ * @param msec The timeout, in milliseconds. If 0 is given,
+ * there will be no timeout.
+ * @throws SystemException Cannot set the timeout.
+ */
+ void setReadTimeout(unsigned int msec) {
+ // See the comment for setWriteTimeout().
+ struct timeval tv;
+ int ret;
+
+ tv.tv_sec = msec / 1000;
+ tv.tv_usec = msec % 1000 * 1000;
+ ret = syscalls::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
+ &tv, sizeof(tv));
+ #ifndef __SOLARIS__
+ // SO_RCVTIMEO is unimplemented and retuns an error on Solaris
+ // 9 and 10 SPARC. Seems to work okay without it.
+ if (ret == -1) {
+ throw SystemException("Cannot set read timeout for socket", errno);
+ }
+ #endif
+ }
+
+ /**
+ * Set the timeout value for writing data to this channel.
+ * If no data can be written within the timeout period, then a
+ * SystemException will be thrown, with error code EAGAIN or
+ * EWOULDBLOCK.
+ *
+ * @param msec The timeout, in milliseconds. If 0 is given,
+ * there will be no timeout.
+ * @throws SystemException Cannot set the timeout.
+ */
+ void setWriteTimeout(unsigned int msec) {
+ // People say that SO_RCVTIMEO/SO_SNDTIMEO are unreliable and
+ // not well-implemented on all platforms.
+ // http://www.developerweb.net/forum/archive/index.php/t-3439.html
+ // That's why we use APR's timeout facilities as well (see Hooks.cpp).
+ struct timeval tv;
+ int ret;
+
+ tv.tv_sec = msec / 1000;
+ tv.tv_usec = msec % 1000 * 1000;
+ ret = syscalls::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO,
+ &tv, sizeof(tv));
+ #ifndef __SOLARIS__
+ // SO_SNDTIMEO is unimplemented and returns an error on Solaris
+ // 9 and 10 SPARC. Seems to work okay without it.
+ if (ret == -1) {
+ throw SystemException("Cannot set read timeout for socket", errno);
+ }
+ #endif
+ }
};
} // namespace Passenger
diff --git a/ext/apache2/PoolOptions.h b/ext/apache2/PoolOptions.h
new file mode 100644
index 00000000..79f805fd
--- /dev/null
+++ b/ext/apache2/PoolOptions.h
@@ -0,0 +1,283 @@
+/*
+ * Phusion Passenger - http://www.modrails.com/
+ * Copyright (C) 2008 Phusion
+ *
+ * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#ifndef _PASSENGER_SPAWN_OPTIONS_H_
+#define _PASSENGER_SPAWN_OPTIONS_H_
+
+#include
+#include "Utils.h"
+
+namespace Passenger {
+
+using namespace std;
+
+/**
+ * This struct encapsulates information for ApplicationPool::get() and for
+ * SpawnManager::spawn(), such as which application is to be spawned.
+ *
+ * Notes on privilege lowering support
+ *
+ * If lowerPrivilege is true, then it will be attempt to
+ * switch the spawned application instance to the user who owns the
+ * application's config/environment.rb, and to the default
+ * group of that user.
+ *
+ * If that user doesn't exist on the system, or if that user is root,
+ * then it will be attempted to switch to the username given by
+ * lowestUser (and to the default group of that user).
+ * If lowestUser doesn't exist either, or if switching user failed
+ * (because the spawn server process does not have the privilege to do so),
+ * then the application will be spawned anyway, without reporting an error.
+ *
+ * It goes without saying that lowering privilege is only possible if
+ * the spawn server is running as root (and thus, by induction, that
+ * Phusion Passenger and Apache's control process are also running as root).
+ * Note that if Apache is listening on port 80, then its control process must
+ * be running as root. See "doc/Security of user switching.txt" for
+ * a detailed explanation.
+ */
+struct PoolOptions {
+ /**
+ * The root directory of the application to spawn. In case of a Ruby on Rails
+ * application, this is the folder that contains 'app/', 'public/', 'config/',
+ * etc. This must be a valid directory, but the path does not have to be absolute.
+ */
+ string appRoot;
+
+ /** Whether to lower the application's privileges. */
+ bool lowerPrivilege;
+
+ /**
+ * The user to fallback to if lowering privilege fails.
+ */
+ string lowestUser;
+
+ /**
+ * The RAILS_ENV/RACK_ENV environment that should be used. May not be an
+ * empty string.
+ */
+ string environment;
+
+ /**
+ * The spawn method to use. Either "smart" or "conservative". See the Ruby
+ * class SpawnManager for details.
+ */
+ string spawnMethod;
+
+ /** The application type. Either "rails", "rack" or "wsgi". */
+ string appType;
+
+ /**
+ * The idle timeout, in seconds, of Rails framework spawners.
+ * A timeout of 0 means that the framework spawner should never idle timeout. A timeout
+ * of -1 means that the default timeout value should be used.
+ *
+ * For more details about Rails framework spawners, please
+ * read the documentation on the Railz::FrameworkSpawner
+ * Ruby class.
+ */
+ long frameworkSpawnerTimeout;
+
+ /**
+ * The idle timeout, in seconds, of Rails application spawners.
+ * A timeout of 0 means that the application spawner should never idle timeout. A timeout
+ * of -1 means that the default timeout value should be used.
+ *
+ * For more details about Rails application spawners, please
+ * read the documentation on the Railz::ApplicationSpawner
+ * Ruby class.
+ */
+ long appSpawnerTimeout;
+
+ /**
+ * The maximum number of requests that the spawned application may process
+ * before exiting. A value of 0 means unlimited.
+ */
+ unsigned long maxRequests;
+
+ /**
+ * The maximum amount of memory (in MB) the spawned application may use.
+ * A value of 0 means unlimited.
+ */
+ unsigned long memoryLimit;
+
+ /**
+ * Whether to use a global queue instead of a per-backend process
+ * queue. This option is only used by ApplicationPool::get().
+ *
+ * If enabled, when all backend processes are active, get() will
+ * wait until there's at least one backend process that's idle, instead
+ * of queuing the request into a random process's private queue.
+ * This is especially useful if a website has one or more long-running
+ * requests.
+ */
+ bool useGlobalQueue;
+
+ /**
+ * A throttling rate for file stats. When set to a non-zero value N,
+ * restart.txt and other files which are usually stat()ted on every
+ * ApplicationPool::get() call will be stat()ed at most every N seconds.
+ */
+ unsigned long statThrottleRate;
+
+ /**
+ * The directory which contains restart.txt and always_restart.txt.
+ * An empty string means that the default directory should be used.
+ */
+ string restartDir;
+
+ /**
+ * Creates a new PoolOptions object with the default values filled in.
+ * One must still set appRoot manually, after having used this constructor.
+ */
+ PoolOptions() {
+ lowerPrivilege = true;
+ lowestUser = "nobody";
+ environment = "production";
+ spawnMethod = "smart";
+ appType = "rails";
+ frameworkSpawnerTimeout = -1;
+ appSpawnerTimeout = -1;
+ maxRequests = 0;
+ memoryLimit = 0;
+ useGlobalQueue = false;
+ statThrottleRate = 0;
+ }
+
+ /**
+ * Creates a new PoolOptions object with the given values.
+ */
+ PoolOptions(const string &appRoot,
+ bool lowerPrivilege = true,
+ const string &lowestUser = "nobody",
+ const string &environment = "production",
+ const string &spawnMethod = "smart",
+ const string &appType = "rails",
+ long frameworkSpawnerTimeout = -1,
+ long appSpawnerTimeout = -1,
+ unsigned long maxRequests = 0,
+ unsigned long memoryLimit = 0,
+ bool useGlobalQueue = false,
+ unsigned long statThrottleRate = 0,
+ const string &restartDir = ""
+ ) {
+ this->appRoot = appRoot;
+ this->lowerPrivilege = lowerPrivilege;
+ this->lowestUser = lowestUser;
+ this->environment = environment;
+ this->spawnMethod = spawnMethod;
+ this->appType = appType;
+ this->frameworkSpawnerTimeout = frameworkSpawnerTimeout;
+ this->appSpawnerTimeout = appSpawnerTimeout;
+ this->maxRequests = maxRequests;
+ this->memoryLimit = memoryLimit;
+ this->useGlobalQueue = useGlobalQueue;
+ this->statThrottleRate = statThrottleRate;
+ this->restartDir = restartDir;
+ }
+
+ /**
+ * Creates a new PoolOptions object from the given string vector.
+ * This vector contains information that's written to by toVector().
+ *
+ * For example:
+ * @code
+ * PoolOptions options(...);
+ * vector vec;
+ *
+ * vec.push_back("my");
+ * vec.push_back("data");
+ * options.toVector(vec); // PoolOptions information will start at index 2.
+ *
+ * PoolOptions copy(vec, 2);
+ * @endcode
+ *
+ * @param vec The vector containing spawn options information.
+ * @param startIndex The index in vec at which the information starts.
+ */
+ PoolOptions(const vector &vec, unsigned int startIndex = 0) {
+ appRoot = vec[startIndex + 1];
+ lowerPrivilege = vec[startIndex + 3] == "true";
+ lowestUser = vec[startIndex + 5];
+ environment = vec[startIndex + 7];
+ spawnMethod = vec[startIndex + 9];
+ appType = vec[startIndex + 11];
+ frameworkSpawnerTimeout = atol(vec[startIndex + 13]);
+ appSpawnerTimeout = atol(vec[startIndex + 15]);
+ maxRequests = atol(vec[startIndex + 17]);
+ memoryLimit = atol(vec[startIndex + 19]);
+ useGlobalQueue = vec[startIndex + 21] == "true";
+ statThrottleRate = atol(vec[startIndex + 23]);
+ restartDir = vec[startIndex + 25];
+ }
+
+ /**
+ * Append the information in this PoolOptions object to the given
+ * string vector. The resulting array could, for example, be used
+ * as a message to be sent to the spawn server.
+ */
+ void toVector(vector &vec) const {
+ if (vec.capacity() < vec.size() + 10) {
+ vec.reserve(vec.size() + 10);
+ }
+ appendKeyValue (vec, "app_root", appRoot);
+ appendKeyValue (vec, "lower_privilege", lowerPrivilege ? "true" : "false");
+ appendKeyValue (vec, "lowest_user", lowestUser);
+ appendKeyValue (vec, "environment", environment);
+ appendKeyValue (vec, "spawn_method", spawnMethod);
+ appendKeyValue (vec, "app_type", appType);
+ appendKeyValue2(vec, "framework_spawner_timeout", frameworkSpawnerTimeout);
+ appendKeyValue2(vec, "app_spawner_timeout", appSpawnerTimeout);
+ appendKeyValue3(vec, "max_requests", maxRequests);
+ appendKeyValue3(vec, "memory_limit", memoryLimit);
+ appendKeyValue (vec, "use_global_queue", useGlobalQueue ? "true" : "false");
+ appendKeyValue3(vec, "stat_throttle_rate", statThrottleRate);
+ appendKeyValue (vec, "restart_dir", restartDir);
+ }
+
+private:
+ static inline void
+ appendKeyValue(vector &vec, const char *key, const string &value) {
+ vec.push_back(key);
+ vec.push_back(const_cast(value));
+ }
+
+ static inline void
+ appendKeyValue(vector &vec, const char *key, const char *value) {
+ vec.push_back(key);
+ vec.push_back(value);
+ }
+
+ static inline void
+ appendKeyValue2(vector &vec, const char *key, long value) {
+ vec.push_back(key);
+ vec.push_back(toString(value));
+ }
+
+ static inline void
+ appendKeyValue3(vector &vec, const char *key, unsigned long value) {
+ vec.push_back(key);
+ vec.push_back(toString(value));
+ }
+};
+
+} // namespace Passenger
+
+#endif /* _PASSENGER_SPAWN_OPTIONS_H_ */
+
diff --git a/ext/apache2/SpawnManager.h b/ext/apache2/SpawnManager.h
index b48e43a4..71b4c1c5 100644
--- a/ext/apache2/SpawnManager.h
+++ b/ext/apache2/SpawnManager.h
@@ -24,6 +24,8 @@
#include
#include
#include
+#include
+#include
#include
#include
@@ -33,19 +35,21 @@
#include
#include
#include
+#include
#include
#include
#include "Application.h"
+#include "PoolOptions.h"
#include "MessageChannel.h"
#include "Exceptions.h"
#include "Logging.h"
-#include "System.h"
namespace Passenger {
using namespace std;
using namespace boost;
+using namespace oxt;
/**
* @brief Spawning of Ruby on Rails/Rack application instances.
@@ -89,7 +93,7 @@ class SpawnManager {
string rubyCommand;
string user;
- mutex lock;
+ boost::mutex lock;
MessageChannel channel;
pid_t pid;
@@ -103,30 +107,34 @@ class SpawnManager {
* @throws IOException The specified log file could not be opened.
*/
void restartServer() {
+ TRACE_POINT();
if (pid != 0) {
+ UPDATE_TRACE_POINT();
channel.close();
// Wait at most 5 seconds for the spawn server to exit.
// If that doesn't work, kill it, then wait at most 5 seconds
// for it to exit.
- time_t begin = InterruptableCalls::time(NULL);
+ time_t begin = syscalls::time(NULL);
bool done = false;
- while (!done && InterruptableCalls::time(NULL) - begin < 5) {
- if (InterruptableCalls::waitpid(pid, NULL, WNOHANG) > 0) {
+ while (!done && syscalls::time(NULL) - begin < 5) {
+ if (syscalls::waitpid(pid, NULL, WNOHANG) > 0) {
done = true;
} else {
- InterruptableCalls::usleep(100000);
+ syscalls::usleep(100000);
}
}
+ UPDATE_TRACE_POINT();
if (!done) {
+ UPDATE_TRACE_POINT();
P_TRACE(2, "Spawn server did not exit in time, killing it...");
- InterruptableCalls::kill(pid, SIGTERM);
- begin = InterruptableCalls::time(NULL);
- while (InterruptableCalls::time(NULL) - begin < 5) {
- if (InterruptableCalls::waitpid(pid, NULL, WNOHANG) > 0) {
+ syscalls::kill(pid, SIGTERM);
+ begin = syscalls::time(NULL);
+ while (syscalls::time(NULL) - begin < 5) {
+ if (syscalls::waitpid(pid, NULL, WNOHANG) > 0) {
break;
} else {
- InterruptableCalls::usleep(100000);
+ syscalls::usleep(100000);
}
}
P_TRACE(2, "Spawn server has exited.");
@@ -138,11 +146,11 @@ class SpawnManager {
FILE *logFileHandle = NULL;
serverNeedsRestart = true;
- if (InterruptableCalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) {
+ if (syscalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) {
throw SystemException("Cannot create a Unix socket", errno);
}
if (!logFile.empty()) {
- logFileHandle = InterruptableCalls::fopen(logFile.c_str(), "a");
+ logFileHandle = syscalls::fopen(logFile.c_str(), "a");
if (logFileHandle == NULL) {
string message("Cannot open log file '");
message.append(logFile);
@@ -151,7 +159,8 @@ class SpawnManager {
}
}
- pid = InterruptableCalls::fork();
+ UPDATE_TRACE_POINT();
+ pid = syscalls::fork();
if (pid == 0) {
if (!logFile.empty()) {
dup2(fileno(logFileHandle), STDERR_FILENO);
@@ -168,6 +177,14 @@ class SpawnManager {
if (!user.empty()) {
struct passwd *entry = getpwnam(user.c_str());
if (entry != NULL) {
+ if (initgroups(user.c_str(), entry->pw_gid) != 0) {
+ int e = errno;
+ fprintf(stderr, "*** Passenger: cannot set supplementary "
+ "groups for user %s: %s (%d)\n",
+ user.c_str(),
+ strerror(e),
+ e);
+ }
if (setgid(entry->pw_gid) != 0) {
int e = errno;
fprintf(stderr, "*** Passenger: cannot run spawn "
@@ -202,33 +219,35 @@ class SpawnManager {
// This argument is ignored by the spawn server. This works on some
// systems, such as Ubuntu Linux.
" ",
- NULL);
+ (char *) NULL);
int e = errno;
- fprintf(stderr, "*** Passenger ERROR: Could not start the spawn server: %s: %s (%d)\n",
+ fprintf(stderr, "*** Passenger ERROR (%s:%d):\n"
+ "Could not start the spawn server: %s: %s (%d)\n",
+ __FILE__, __LINE__,
rubyCommand.c_str(), strerror(e), e);
fflush(stderr);
_exit(1);
} else if (pid == -1) {
int e = errno;
- InterruptableCalls::close(fds[0]);
- InterruptableCalls::close(fds[1]);
+ syscalls::close(fds[0]);
+ syscalls::close(fds[1]);
if (logFileHandle != NULL) {
- InterruptableCalls::fclose(logFileHandle);
+ syscalls::fclose(logFileHandle);
}
pid = 0;
throw SystemException("Unable to fork a process", e);
} else {
- InterruptableCalls::close(fds[1]);
+ syscalls::close(fds[1]);
if (!logFile.empty()) {
- InterruptableCalls::fclose(logFileHandle);
+ syscalls::fclose(logFileHandle);
}
channel = MessageChannel(fds[0]);
serverNeedsRestart = false;
#ifdef TESTING_SPAWN_MANAGER
if (nextRestartShouldFail) {
- InterruptableCalls::kill(pid, SIGTERM);
- InterruptableCalls::usleep(500000);
+ syscalls::kill(pid, SIGTERM);
+ syscalls::usleep(500000);
}
#endif
}
@@ -237,41 +256,26 @@ class SpawnManager {
/**
* Send the spawn command to the spawn server.
*
- * @param appRoot The application root of the application to spawn.
- * @param lowerPrivilege Whether to lower the application's privileges.
- * @param lowestUser The user to fallback to if lowering privilege fails.
- * @param environment The RAILS_ENV/RACK_ENV environment that should be used.
- * @param spawnMethod The spawn method to use.
- * @param appType The application type.
+ * @param PoolOptions The spawn options to use.
* @return An Application smart pointer, representing the spawned application.
* @throws SpawnException Something went wrong.
*/
- ApplicationPtr sendSpawnCommand(
- const string &appRoot,
- bool lowerPrivilege,
- const string &lowestUser,
- const string &environment,
- const string &spawnMethod,
- const string &appType
- ) {
+ ApplicationPtr sendSpawnCommand(const PoolOptions &PoolOptions) {
+ TRACE_POINT();
vector args;
int ownerPipe;
try {
- channel.write("spawn_application",
- appRoot.c_str(),
- (lowerPrivilege) ? "true" : "false",
- lowestUser.c_str(),
- environment.c_str(),
- spawnMethod.c_str(),
- appType.c_str(),
- NULL);
+ args.push_back("spawn_application");
+ PoolOptions.toVector(args);
+ channel.write(args);
} catch (const SystemException &e) {
throw SpawnException(string("Could not write 'spawn_application' "
"command to the spawn server: ") + e.sys());
}
try {
+ UPDATE_TRACE_POINT();
// Read status.
if (!channel.read(args)) {
throw SpawnException("The spawn server has exited unexpectedly.");
@@ -299,6 +303,7 @@ class SpawnManager {
throw SpawnException(string("Could not read from the spawn server: ") + e.sys());
}
+ UPDATE_TRACE_POINT();
try {
ownerPipe = channel.readFileDescriptor();
} catch (const SystemException &e) {
@@ -312,14 +317,15 @@ class SpawnManager {
}
if (args.size() != 3) {
- InterruptableCalls::close(ownerPipe);
+ UPDATE_TRACE_POINT();
+ syscalls::close(ownerPipe);
throw SpawnException("The spawn server sent an invalid message.");
}
pid_t pid = atoi(args[0]);
- bool usingAbstractNamespace = args[2] == "true";
- if (!usingAbstractNamespace) {
+ UPDATE_TRACE_POINT();
+ if (args[2] == "unix") {
int ret;
do {
ret = chmod(args[1].c_str(), S_IRUSR | S_IWUSR);
@@ -328,18 +334,16 @@ class SpawnManager {
ret = chown(args[1].c_str(), getuid(), getgid());
} while (ret == -1 && errno == EINTR);
}
- return ApplicationPtr(new Application(appRoot, pid, args[1],
- usingAbstractNamespace, ownerPipe));
+ return ApplicationPtr(new Application(PoolOptions.appRoot,
+ pid, args[1], args[2], ownerPipe));
}
/**
* @throws boost::thread_interrupted
*/
ApplicationPtr
- handleSpawnException(const SpawnException &e, const string &appRoot,
- bool lowerPrivilege, const string &lowestUser,
- const string &environment, const string &spawnMethod,
- const string &appType) {
+ handleSpawnException(const SpawnException &e, const PoolOptions &PoolOptions) {
+ TRACE_POINT();
bool restarted;
try {
P_DEBUG("Spawn server died. Attempting to restart it...");
@@ -355,8 +359,7 @@ class SpawnManager {
restarted = false;
}
if (restarted) {
- return sendSpawnCommand(appRoot, lowerPrivilege, lowestUser,
- environment, spawnMethod, appType);
+ return sendSpawnCommand(PoolOptions);
} else {
throw SpawnException("The spawn server died unexpectedly, and restarting it failed.");
}
@@ -369,6 +372,7 @@ class SpawnManager {
* @throws SystemException Something went wrong.
*/
void sendReloadCommand(const string &appRoot) {
+ TRACE_POINT();
try {
channel.write("reload", appRoot.c_str(), NULL);
} catch (const SystemException &e) {
@@ -378,6 +382,7 @@ class SpawnManager {
}
void handleReloadException(const SystemException &e, const string &appRoot) {
+ TRACE_POINT();
bool restarted;
try {
P_DEBUG("Spawn server died. Attempting to restart it...");
@@ -434,6 +439,7 @@ class SpawnManager {
const string &logFile = "",
const string &rubyCommand = "ruby",
const string &user = "") {
+ TRACE_POINT();
this->spawnServerCommand = spawnServerCommand;
this->logFile = logFile;
this->rubyCommand = rubyCommand;
@@ -454,76 +460,45 @@ class SpawnManager {
}
~SpawnManager() throw() {
+ TRACE_POINT();
if (pid != 0) {
+ UPDATE_TRACE_POINT();
this_thread::disable_interruption di;
this_thread::disable_syscall_interruption dsi;
P_TRACE(2, "Shutting down spawn manager (PID " << pid << ").");
channel.close();
- InterruptableCalls::waitpid(pid, NULL, 0);
+ syscalls::waitpid(pid, NULL, 0);
P_TRACE(2, "Spawn manager exited.");
}
}
/**
- * Spawn a new instance of a Ruby on Rails or Rack application.
+ * Spawn a new instance of an application. Spawning details are to be passed
+ * via the PoolOptions parameter.
*
* If the spawn server died during the spawning process, then the server
* will be automatically restarted, and another spawn attempt will be made.
* If restarting the server fails, or if the second spawn attempt fails,
* then an exception will be thrown.
*
- * If lowerPrivilege is true, then it will be attempt to
- * switch the spawned application instance to the user who owns the
- * application's config/environment.rb, and to the default
- * group of that user.
- *
- * If that user doesn't exist on the system, or if that user is root,
- * then it will be attempted to switch to the username given by
- * lowestUser (and to the default group of that user).
- * If lowestUser doesn't exist either, or if switching user failed
- * (because the spawn server process does not have the privilege to do so),
- * then the application will be spawned anyway, without reporting an error.
- *
- * It goes without saying that lowering privilege is only possible if
- * the spawn server is running as root (and thus, by induction, that
- * Passenger and Apache's control process are also running as root).
- * Note that if Apache is listening on port 80, then its control process must
- * be running as root. See "doc/Security of user switching.txt" for
- * a detailed explanation.
- *
- * @param appRoot The application root of a RoR application, i.e. the folder that
- * contains 'app/', 'public/', 'config/', etc. This must be a valid directory,
- * but the path does not have to be absolute.
- * @param lowerPrivilege Whether to lower the application's privileges.
- * @param lowestUser The user to fallback to if lowering privilege fails.
- * @param environment The RAILS_ENV/RACK_ENV environment that should be used. May not be empty.
- * @param spawnMethod The spawn method to use. Either "smart" or "conservative".
- * See the Ruby class SpawnManager for details.
- * @param appType The application type. Either "rails" or "rack".
+ * @param PoolOptions An object containing the details for this spawn operation,
+ * such as which application to spawn. See PoolOptions for details.
* @return A smart pointer to an Application object, which represents the application
* instance that has been spawned. Use this object to communicate with the
* spawned application.
* @throws SpawnException Something went wrong.
* @throws boost::thread_interrupted
*/
- ApplicationPtr spawn(
- const string &appRoot,
- bool lowerPrivilege = true,
- const string &lowestUser = "nobody",
- const string &environment = "production",
- const string &spawnMethod = "smart",
- const string &appType = "rails"
- ) {
- mutex::scoped_lock l(lock);
+ ApplicationPtr spawn(const PoolOptions &PoolOptions) {
+ TRACE_POINT();
+ boost::mutex::scoped_lock l(lock);
try {
- return sendSpawnCommand(appRoot, lowerPrivilege, lowestUser,
- environment, spawnMethod, appType);
+ return sendSpawnCommand(PoolOptions);
} catch (const SpawnException &e) {
if (e.hasErrorPage()) {
throw;
} else {
- return handleSpawnException(e, appRoot, lowerPrivilege,
- lowestUser, environment, spawnMethod, appType);
+ return handleSpawnException(e, PoolOptions);
}
}
}
@@ -544,6 +519,7 @@ class SpawnManager {
* restart was attempted, but it failed.
*/
void reload(const string &appRoot) {
+ TRACE_POINT();
this_thread::disable_interruption di;
this_thread::disable_syscall_interruption dsi;
try {
diff --git a/ext/apache2/StandardApplicationPool.h b/ext/apache2/StandardApplicationPool.h
index e0c3f71e..08d1ee5f 100644
--- a/ext/apache2/StandardApplicationPool.h
+++ b/ext/apache2/StandardApplicationPool.h
@@ -27,6 +27,9 @@
#include
#include
+#include
+#include
+
#include
#include
#include