/* * Phusion Passenger - https://www.phusionpassenger.com/ * Copyright (c) 2010-2018 Phusion Holding B.V. * * "Passenger", "Phusion Passenger" and "Union Station" are registered * trademarks of Phusion Holding B.V. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _PASSENGER_WATCHDOG_LAUNCHER_HPsg #define _PASSENGER_WATCHDOG_LAUNCHER_HPsg #include #include #include "JsonTools/CBindings.h" #ifdef __cplusplus extern "C" { #endif typedef enum { IM_APACHE, IM_NGINX, IM_STANDALONE } PsgIntegrationMode; typedef void PsgWatchdogLauncher; typedef void (*PsgAfterForkCallback)(void *, void *); PsgWatchdogLauncher *psg_watchdog_launcher_new(PsgIntegrationMode mode, char **error_message); int psg_watchdog_launcher_start(PsgWatchdogLauncher *launcher, const char *passengerRoot, PsgJsonValue *config, const PsgAfterForkCallback afterFork, void *callbackArgument, char **errorMessage); const char *psg_watchdog_launcher_get_core_address(PsgWatchdogLauncher *launcher, unsigned int *size); const char *psg_watchdog_launcher_get_core_password(PsgWatchdogLauncher *launcher, unsigned int *size); const char *psg_watchdog_launcher_get_instance_dir(PsgWatchdogLauncher *launcher, unsigned int *size); pid_t psg_watchdog_launcher_get_pid(PsgWatchdogLauncher *launcher); void psg_watchdog_launcher_detach(PsgWatchdogLauncher *launcher); void psg_watchdog_launcher_free(PsgWatchdogLauncher *launcher); #ifdef __cplusplus } /* extern "C" */ #endif #ifdef __cplusplus #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Passenger { using namespace std; using namespace boost; using namespace oxt; /** * Utility class for starting various the Passenger watchdog. */ class WatchdogLauncher { /** * Whether the starter process is Apache, Nginx or * Passenger Standalone. */ P_RO_PROPERTY(private, PsgIntegrationMode, IntegrationMode); /** * The watchdog's PID. Equals 0 if the watchdog hasn't been started yet * or if `detach()` is called. */ P_RO_PROPERTY(private, pid_t, Pid); // Note: the use of `CONST_REF` in the properties below is intentional. // The C getter functions return the string pointer directly. /** * The address on which the Passenger core listens for HTTP requests, * and the corresponding password. * * Only valid when `getPid() != 0`. */ P_RO_PROPERTY_CONST_REF(private, string, CoreAddress); P_RO_PROPERTY_CONST_REF(private, string, CorePassword); /** * The path to the instance directory that the Watchdog has created. * * Only valid when `getPid() != 0`. */ P_RO_PROPERTY_CONST_REF(private, string, InstanceDir); private: /** The watchdog's feedback file descriptor. */ FileDescriptor feedbackFd; /** * Safely dup2() the given file descriptor to 3 (FEEDBACK_FD). */ void installFeedbackFd(const FileDescriptor &fd) { if (fd != FEEDBACK_FD && syscalls::dup2(fd, FEEDBACK_FD) == -1) { int e = errno; try { writeArrayMessage(fd, "system error", "dup2() failed", toString(e).c_str(), NULL); _exit(1); } catch (...) { fprintf(stderr, "Passenger WatchdogLauncher: dup2() failed: %s (%d)\n", strerror(e), e); fflush(stderr); _exit(1); } } } /** * Call this if the watchdog seems to have crashed. This function will try * to determine whether the watchdog is still running, whether it crashed * with a signal, etc. If it has detected that the watchdog is no longer running * then it will set `pid` to -1. */ void inspectWatchdogCrashReason(pid_t &pid) { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; int ret, status; /* Upon noticing that something went wrong, the watchdog * or its subprocesses might still be writing out an error * report, so we wait a while before killing the watchdog. */ ret = timedWaitPid(pid, &status, 5000); if (ret == 0) { /* Looks like the watchdog didn't crash and is still running. */ throw RuntimeException( "Unable to start the " PROGRAM_NAME " watchdog: " "it froze during startup and reported an unknown error"); } else if (ret != -1 && WIFSIGNALED(status)) { /* Looks like a crash which caused a signal. */ pid = -1; throw RuntimeException( "Unable to start the " PROGRAM_NAME " watchdog: " "it seems to have been killed with signal " + getSignalName(WTERMSIG(status)) + " during startup"); } else if (ret == -1) { /* Looks like it exited for a different reason and has no exit code. */ pid = -1; throw RuntimeException( "Unable to start the " PROGRAM_NAME " watchdog: " "it seems to have crashed during startup for an unknown reason"); } else { /* Looks like it exited for a different reason, but has an exit code. */ pid = -1; throw RuntimeException( "Unable to start the " PROGRAM_NAME " watchdog: " "it seems to have crashed during startup for an unknown reason, " "with exit code " + toString(WEXITSTATUS(status))); } } void throwEnrichedWatchdogFailReason(const ResourceLocator &locator, const string &simpleReason) { if (mIntegrationMode == IM_STANDALONE) { throw RuntimeException("Unable to start " PROGRAM_NAME ": " + simpleReason + ". This probably means that your " SHORT_PROGRAM_NAME " installation is broken or incomplete. Please try reinstalling " SHORT_PROGRAM_NAME); } else { string passengerRootConfig; string docURL; if (mIntegrationMode == IM_APACHE) { passengerRootConfig = "PassengerRoot"; docURL = "https://www.phusionpassenger.com/library/config/apache/reference/#passengerroot"; } else { passengerRootConfig = "passenger_root"; docURL = "https://www.phusionpassenger.com/library/config/nginx/reference/#passenger_root"; } string message = "Unable to start " PROGRAM_NAME ": " + simpleReason + ". There may be different causes for this:\n\n" " - Your '" + passengerRootConfig + "' setting is set to the wrong value." " Please see " + docURL + " to learn how to fix the value.\n"; if (!locator.getBuildSystemDir().empty()) { message.append(" - The " AGENT_EXE " binary is not compiled." " Please run this command to compile it: " + locator.getBinDir() + "/passenger-config compile-agent\n"); } message.append(" - Your " SHORT_PROGRAM_NAME " installation is broken or incomplete." " Please reinstall " SHORT_PROGRAM_NAME "."); throw RuntimeException(message); } } static void killProcessGroupAndWait(pid_t *pid, unsigned long long timeout = 0) { if (*pid != -1 && (timeout == 0 || timedWaitPid(*pid, NULL, timeout) <= 0)) { boost::this_thread::disable_syscall_interruption dsi; syscalls::killpg(*pid, SIGKILL); syscalls::waitpid(*pid, NULL, 0); *pid = -1; } } /** * Behaves like `waitpid(pid, status, WNOHANG)`, but waits at most * `timeout` miliseconds for the process to exit. */ static int timedWaitPid(pid_t pid, int *status, unsigned long long timeout) { Timer timer; int ret; do { ret = syscalls::waitpid(pid, status, WNOHANG); if (ret > 0 || ret == -1) { return ret; } else { syscalls::usleep(10000); } } while (timer.elapsed() < timeout); return 0; // timed out } public: /** * Construct a WatchdogLauncher object. The watchdog won't be started * until you call `start()`. */ WatchdogLauncher(PsgIntegrationMode _integrationMode) : mIntegrationMode(_integrationMode), mPid(0) { } ~WatchdogLauncher() { if (mPid != 0) { boost::this_thread::disable_syscall_interruption dsi; /* Send a message down the feedback fd to tell the watchdog * that we're shutting down cleanly. Closing the fd without * sending anything indicates an unclean shutdown. */ syscalls::write(feedbackFd, "c", 1); feedbackFd.close(); syscalls::waitpid(mPid, NULL, 0); } } const char *getIntegrationModeString() const { switch (mIntegrationMode) { case IM_APACHE: return "apache"; case IM_NGINX: return "nginx"; case IM_STANDALONE: return "standalone"; default: return "unknown"; } } /** * Start the agents through the watchdog. * * @throws SystemException Something went wrong. * @throws IOException Something went wrong while communicating with one * of the agents during its initialization phase. * @throws RuntimeException Something went wrong. */ void start(const string &passengerRoot, const Json::Value &extraConfig = Json::Value(), const boost::function &afterFork = boost::function()) { TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; ResourceLocator locator(passengerRoot); string agentFilename; try { agentFilename = locator.findSupportBinary(AGENT_EXE); } catch (const Passenger::RuntimeException &e) { throwEnrichedWatchdogFailReason(locator, e.what()); } SocketPair fds; int e; pid_t pid; Json::Value::const_iterator it; Json::Value config; config["web_server_control_process_pid"] = getpid(); config["integration_mode"] = getIntegrationModeString(); config["passenger_root"] = passengerRoot; config["log_level"] = (int) LoggingKit::getLevel(); for (it = extraConfig.begin(); it != extraConfig.end(); it++) { config[it.name()] = *it; } fds = createUnixSocketPair(__FILE__, __LINE__); pid = syscalls::fork(); if (pid == 0) { // Child /* Become the session leader so that Apache can't kill the * watchdog with killpg() during shutdown, so that a * Ctrl-C only affects the web server, and so that * we can kill all of our subprocesses in a single killpg(). */ setsid(); /* We don't know how the web server or the environment affect * signal handlers and the signal mask, so reset this stuff * just in case. Also, we reset the signal handlers before * closing all file descriptors, in order to prevent bugs * like this: https://github.com/phusion/passenger/pull/97 */ resetSignalHandlersAndMask(); // Make sure the feedback fd is 3 and close all file descriptors // except stdin, stdout, stderr and 3. close(fds[0]); installFeedbackFd(fds[1]); setenv("PASSENGER_USE_FEEDBACK_FD", "true", 1); if (afterFork) { afterFork(); } closeAllFileDescriptors(FEEDBACK_FD); execl(agentFilename.c_str(), AGENT_EXE, "watchdog", // Some extra space to allow the child process to change its process title. " ", (char *) 0); e = errno; try { writeArrayMessage(FEEDBACK_FD, "exec error", toString(e).c_str(), NULL); _exit(1); } catch (...) { fprintf(stderr, "Passenger WatchdogLauncher: could not execute %s: %s (%d)\n", agentFilename.c_str(), strerror(e), e); fflush(stderr); _exit(1); } } else if (pid == -1) { // Error e = errno; throw SystemException("Cannot fork a new process", e); } else { // Parent UPDATE_TRACE_POINT(); FileDescriptor feedbackFd = fds[0]; vector args; bool result = false; ScopeGuard guard(boost::bind(&WatchdogLauncher::killProcessGroupAndWait, &pid, 0)); fds[1].close(); P_LOG_FILE_DESCRIPTOR_PURPOSE(feedbackFd, "WatchdogLauncher: feedback FD"); /****** Send arguments to watchdog through the feedback channel ******/ UPDATE_TRACE_POINT(); /* Here we don't care about EPIPE and ECONNRESET errors. The watchdog * could have sent an error message over the feedback fd without * reading the arguments. We'll notice that later. */ try { writeScalarMessage(feedbackFd, config.toStyledString()); } catch (const SystemException &e) { if (e.code() != EPIPE && e.code() != ECONNRESET) { inspectWatchdogCrashReason(pid); } } /****** Read agents information report ******/ boost::this_thread::restore_interruption ri(di); boost::this_thread::restore_syscall_interruption rsi(dsi); UPDATE_TRACE_POINT(); try { result = readArrayMessage(feedbackFd, args); } catch (const SystemException &ex) { if (ex.code() == ECONNRESET) { inspectWatchdogCrashReason(pid); } else { killProcessGroupAndWait(&pid, 5000); guard.clear(); throw SystemException("Unable to start the " PROGRAM_NAME " watchdog: " "unable to read its startup information report", ex.code()); } } if (!result) { UPDATE_TRACE_POINT(); inspectWatchdogCrashReason(pid); } if (args[0] == "Agents information") { UPDATE_TRACE_POINT(); if (args.size() != 1) { throw RuntimeException("Unable to start the " PROGRAM_NAME " watchdog: " "it belongs to an incompatible version of " SHORT_PROGRAM_NAME ". Please fully upgrade " SHORT_PROGRAM_NAME "."); } string jsonData; try { result = readScalarMessage(feedbackFd, jsonData); } catch (const SystemException &ex) { if (ex.code() == ECONNRESET) { inspectWatchdogCrashReason(pid); } else { killProcessGroupAndWait(&pid, 5000); guard.clear(); throw SystemException("Unable to start the " PROGRAM_NAME " watchdog: " "unable to read its startup information report", ex.code()); } } if (!result) { UPDATE_TRACE_POINT(); inspectWatchdogCrashReason(pid); } Json::Value doc; Json::Reader reader; if (!reader.parse(jsonData, doc)) { throw RuntimeException("Unable to start the " PROGRAM_NAME " watchdog: " "unable to parse its startup information report as valid JSON: " + reader.getFormattedErrorMessages() + "\n" "Raw data: \"" + cEscapeString(jsonData) + "\""); } mPid = pid; this->feedbackFd = feedbackFd; mCoreAddress = doc["core_address"].asString(); mCorePassword = doc["core_password"].asString(); mInstanceDir = doc["instance_dir"].asString(); guard.clear(); } else if (args[0] == "Watchdog startup error") { killProcessGroupAndWait(&pid, 5000); guard.clear(); throw RuntimeException("Unable to start the " PROGRAM_NAME " watchdog " "because it encountered the following error during startup: " + args[1]); } else if (args[0] == "system error") { killProcessGroupAndWait(&pid, 5000); guard.clear(); throw SystemException(args[1], atoi(args[2])); } else if (args[0] == "exec error") { e = atoi(args[1]); killProcessGroupAndWait(&pid, 5000); guard.clear(); if (e == ENOENT) { throwEnrichedWatchdogFailReason(locator, "Executable " + agentFilename + " not found."); } else { throw SystemException("Unable to start the " PROGRAM_NAME " watchdog (" + agentFilename + ")", e); } } else { UPDATE_TRACE_POINT(); killProcessGroupAndWait(&pid, 5000); guard.clear(); throw RuntimeException("The " PROGRAM_NAME " watchdog sent an unknown feedback message '" + args[0] + "'"); } } } /** * Close any file descriptors that this object has, and make it so that * the destructor doesn't try to shut down the watchdog. * * @post getPid() == 0 */ void detach() { feedbackFd.close(); mPid = 0; } }; } // namespace Passenger #endif /* __cplusplus */ #endif /* _PASSENGER_WATCHDOG_LAUNCHER_HPsg */