/* * 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. */ // Include ev++.h early to avoid macro clash on EV_ERROR. #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(sun) && !defined(__sun) #define HAVE_FLOCK #endif #ifdef __linux__ #include #endif #include #include #include #ifdef HAVE_FLOCK #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace boost; using namespace oxt; using namespace Passenger; using namespace Passenger::Agent::Fundamentals; enum OomFileType { OOM_ADJ, OOM_SCORE_ADJ }; #define REQUEST_SOCKET_PASSWORD_SIZE 64 class InstanceDirToucher; class AgentWatcher; /***** Working objects *****/ namespace Passenger { namespace Watchdog { struct WorkingObjects { RandomGenerator randomGenerator; EventFd errorEvent; EventFd exitEvent; uid_t defaultUid; gid_t defaultGid; InstanceDirectoryPtr instanceDir; int startupReportFile; int lockFile; vector cleanupPidfiles; bool pidsCleanedUp; bool pidFileCleanedUp; string corePidFile; string fdPassingPassword; Json::Value extraConfigToPassToSubAgents; Json::Value controllerAddresses; Json::Value coreApiServerAddresses; Json::Value coreApiServerAuthorizations; Json::Value watchdogApiServerAddresses; Json::Value watchdogApiServerAuthorizations; int apiServerFds[SERVER_KIT_MAX_SERVER_ENDPOINTS]; BackgroundEventLoop *bgloop; ServerKit::Context *serverKitContext; ServerKit::Schema serverKitSchema; ApiServer::ApiServer *apiServer; WorkingObjects() : errorEvent(__FILE__, __LINE__, "WorkingObjects: errorEvent"), exitEvent(__FILE__, __LINE__, "WorkingObjects: exitEvent"), startupReportFile(-1), pidsCleanedUp(false), pidFileCleanedUp(false), extraConfigToPassToSubAgents(Json::objectValue), controllerAddresses(Json::arrayValue), coreApiServerAddresses(Json::arrayValue), coreApiServerAuthorizations(Json::arrayValue), watchdogApiServerAddresses(Json::arrayValue), watchdogApiServerAuthorizations(Json::arrayValue), bgloop(NULL), serverKitContext(NULL), apiServer(NULL) { for (unsigned int i = 0; i < SERVER_KIT_MAX_SERVER_ENDPOINTS; i++) { apiServerFds[i] = -1; } } }; typedef boost::shared_ptr WorkingObjectsPtr; } // namespace Watchdog } // namespace Passenger using namespace Passenger::Watchdog; static WrapperRegistry::Registry *watchdogWrapperRegistry; static Schema *watchdogSchema; static ConfigKit::Store *watchdogConfig; static WorkingObjects *workingObjects; static void cleanup(const WorkingObjectsPtr &wo); #include "AgentWatcher.cpp" #include "InstanceDirToucher.cpp" #include "CoreWatcher.cpp" /***** Functions *****/ #if !BOOST_OS_MACOS static FILE * openOomAdjFileGetType(const char *mode, OomFileType &type) { FILE *f = fopen("/proc/self/oom_score_adj", mode); if (f == NULL) { f = fopen("/proc/self/oom_adj", mode); if (f == NULL) { return NULL; } else { type = OOM_ADJ; return f; } } else { type = OOM_SCORE_ADJ; return f; } } static FILE * openOomAdjFileForcedType(const char *mode, OomFileType &type) { if (type == OOM_SCORE_ADJ) { return fopen("/proc/self/oom_score_adj", mode); } else { assert(type == OOM_ADJ); return fopen("/proc/self/oom_adj", mode); } } /** * Set the current process's OOM score to "never kill". */ static string setOomScoreNeverKill() { string oldScore; FILE *f; OomFileType type; f = openOomAdjFileGetType("r", type); if (f == NULL) { return ""; } // mark if this is a legacy score so we won't try to write it as OOM_SCORE_ADJ if (type == OOM_ADJ) { oldScore.append("l"); } char buf[1024]; size_t bytesRead; while (true) { bytesRead = fread(buf, 1, sizeof(buf), f); if (bytesRead == 0 && feof(f)) { break; } else if (bytesRead == 0 && ferror(f)) { fclose(f); return ""; } else { oldScore.append(buf, bytesRead); } } fclose(f); f = openOomAdjFileForcedType("w", type); if (f == NULL) { return ""; } if (type == OOM_SCORE_ADJ) { fprintf(f, "-1000\n"); } else { assert(type == OOM_ADJ); fprintf(f, "-17\n"); } fclose(f); return oldScore; } #endif static void terminationHandler(int signo) { ssize_t ret = write(workingObjects->exitEvent.writerFd(), "x", 1); (void) ret; // Don't care about the result. } /** * Wait until the starter process has exited or sent us an exit command, * or until one of the watcher threads encounter an error. If a thread * encountered an error then the error message will be printed. * * Returns whether this watchdog should exit gracefully, which is only the * case if the web server sent us an exit command and no thread encountered * an error. */ static bool waitForStarterProcessOrWatchers(const WorkingObjectsPtr &wo, vector &watchers) { TRACE_POINT(); fd_set fds; int max = -1, ret; char x; wo->bgloop->start("Main event loop", 0); struct sigaction action; action.sa_handler = terminationHandler; action.sa_flags = SA_RESTART; sigemptyset(&action.sa_mask); sigaction(SIGINT, &action, NULL); sigaction(SIGTERM, &action, NULL); FD_ZERO(&fds); if (feedbackFdAvailable()) { FD_SET(FEEDBACK_FD, &fds); max = std::max(max, FEEDBACK_FD); } FD_SET(wo->errorEvent.fd(), &fds); max = std::max(max, wo->errorEvent.fd()); FD_SET(wo->exitEvent.fd(), &fds); max = std::max(max, wo->exitEvent.fd()); UPDATE_TRACE_POINT(); ret = syscalls::select(max + 1, &fds, NULL, NULL, NULL); if (ret == -1) { int e = errno; P_ERROR("select() failed: " << strerror(e)); return false; } action.sa_handler = SIG_DFL; sigaction(SIGINT, &action, NULL); sigaction(SIGTERM, &action, NULL); P_DEBUG("Stopping API server"); wo->bgloop->stop(); for (unsigned int i = 0; i < SERVER_KIT_MAX_SERVER_ENDPOINTS; i++) { if (wo->apiServerFds[i] != -1) { syscalls::close(wo->apiServerFds[i]); } } if (FD_ISSET(wo->errorEvent.fd(), &fds)) { UPDATE_TRACE_POINT(); vector::const_iterator it; string message, backtrace, watcherName; for (it = watchers.begin(); it != watchers.end() && message.empty(); it++) { message = (*it)->getErrorMessage(); backtrace = (*it)->getErrorBacktrace(); watcherName = (*it)->name(); } if (!message.empty() && backtrace.empty()) { P_ERROR("Error in " << watcherName << " watcher:\n " << message); } else if (!message.empty() && !backtrace.empty()) { P_ERROR("Error in " << watcherName << " watcher:\n " << message << "\n" << backtrace); } return false; } else if (FD_ISSET(wo->exitEvent.fd(), &fds)) { return true; } else { UPDATE_TRACE_POINT(); assert(feedbackFdAvailable()); ret = syscalls::read(FEEDBACK_FD, &x, 1); return ret == 1 && x == 'c'; } } string relative(string filename){ string dir = filename.substr(filename.find_last_of('/')+1); return dir; } static vector readCleanupPids(const WorkingObjectsPtr &wo) { vector result; foreach (string filename, wo->cleanupPidfiles) { FILE *f = fopen(relative(filename).c_str(), "r"); if (f != NULL) { char buf[33]; size_t ret; ret = fread(buf, 1, 32, f); fclose(f); if (ret > 0) { buf[ret] = '\0'; result.push_back(atoi(buf)); } else { P_WARN("Cannot read cleanup PID file " << relative(filename).c_str() << " (" << filename << ")"); } } else { P_WARN("Cannot open cleanup PID file " << relative(filename).c_str() << " (" << filename << ")"); } } return result; } static void killCleanupPids(const vector &cleanupPids) { foreach (pid_t pid, cleanupPids) { P_DEBUG("Sending SIGTERM to cleanup PID " << pid); if(kill(pid, SIGTERM) == -1){ int e = errno; P_WARN("Failed to send SIGTERM to " << pid << ", error: " << e << " " << strerror(e)); } } } static void killCleanupPids(const WorkingObjectsPtr &wo) { if (!wo->pidsCleanedUp) { killCleanupPids(readCleanupPids(wo)); wo->pidsCleanedUp = true; } } static void deletePidFile(const WorkingObjectsPtr &wo) { Json::Value pidFile = watchdogConfig->get("watchdog_pid_file"); if (!pidFile.isNull() && !wo->pidFileCleanedUp && watchdogConfig->get("watchdog_pid_file_autodelete").asBool()) { syscalls::unlink(pidFile.asCString()); wo->pidFileCleanedUp = true; } } static void cleanupAgentsInBackground(const WorkingObjectsPtr &wo, vector &watchers, char *argv[]) { boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; pid_t pid; int e; pid = fork(); if (pid == 0) { // Child try { vector::const_iterator it; Timer timer(false); fd_set fds, fds2; int max, agentProcessesDone; unsigned long long deadline = 30000; // miliseconds #if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(sun) // Change process title. strcpy(argv[0], "PassengerWatchdog (cleaning up...)"); #endif P_DEBUG("Sending SIGTERM to all agent processes"); for (it = watchers.begin(); it != watchers.end(); it++) { (*it)->signalShutdown(); } max = 0; FD_ZERO(&fds); for (it = watchers.begin(); it != watchers.end(); it++) { FD_SET((*it)->getFeedbackFd(), &fds); if ((*it)->getFeedbackFd() > max) { max = (*it)->getFeedbackFd(); } } P_DEBUG("Waiting until all agent processes have exited..."); timer.start(); agentProcessesDone = 0; while (agentProcessesDone != -1 && agentProcessesDone < (int) watchers.size() && timer.elapsed() < deadline) { struct timeval timeout; #ifdef FD_COPY FD_COPY(&fds, &fds2); #else FD_ZERO(&fds2); for (it = watchers.begin(); it != watchers.end(); it++) { FD_SET((*it)->getFeedbackFd(), &fds2); } #endif timeout.tv_sec = 0; timeout.tv_usec = 10000; agentProcessesDone = syscalls::select(max + 1, &fds2, NULL, NULL, &timeout); if (agentProcessesDone > 0 && timer.elapsed() < deadline) { usleep(10000); } } if (agentProcessesDone == -1 || timer.elapsed() >= deadline) { // An error occurred or we've waited long enough. Kill all the // processes. P_WARN("Some " PROGRAM_NAME " agent processes did not exit " << "in time, forcefully shutting down all."); } else { P_DEBUG("All " PROGRAM_NAME " agent processes have exited. Forcing all subprocesses to shut down."); } P_DEBUG("Sending SIGKILL to all agent processes"); for (it = watchers.begin(); it != watchers.end(); it++) { (*it)->forceShutdown(); } cleanup(wo); _exit(0); } catch (const std::exception &e) { P_CRITICAL("An exception occurred during cleaning up: " << e.what()); _exit(1); } catch (...) { P_CRITICAL("An unknown exception occurred during cleaning up"); _exit(1); } } else if (pid == -1) { // Error e = errno; throw SystemException("fork() failed", e); } else { // Parent // Let child process handle cleanup. wo->instanceDir->detach(); } } static void forceAllAgentsShutdown(const WorkingObjectsPtr &wo, vector &watchers) { vector::iterator it; P_DEBUG("Sending SIGTERM to all agent processes"); for (it = watchers.begin(); it != watchers.end(); it++) { (*it)->signalShutdown(); } usleep(1000000); P_DEBUG("Sending SIGKILL to all agent processes"); for (it = watchers.begin(); it != watchers.end(); it++) { (*it)->forceShutdown(); } } static void runHookScriptAndThrowOnError(const char *name) { TRACE_POINT(); HookScriptOptions options; options.name = name; options.spec = watchdogConfig->get(string("hook_") + name).asString(); options.agentConfig = watchdogConfig->inspectEffectiveValues(); if (!runHookScripts(options)) { throw RuntimeException(string("Hook script ") + name + " failed"); } } static void usage() { printf("Usage: " AGENT_EXE " watchdog \n"); printf("Runs the " PROGRAM_NAME " watchdog.\n\n"); printf("The watchdog runs and supervises various " PROGRAM_NAME " agent processes,\n"); printf("at this moment only the core (there was also a process called UstRouter but\n"); printf("it no longer exists). Arguments marked with \"[A]\", e.g.\n"); printf("--passenger-root and --log-level, are automatically passed to all supervised\n"); printf("agents, unless you explicitly override them by passing extra arguments to a\n"); printf("supervised agent specifically. You can pass arguments to a supervised agent by\n"); printf("wrapping those arguments between --BC/--EC and --BU/--EU.\n"); printf("\n"); printf(" Example 1: pass some arguments to the core.\n\n"); printf(" " SHORT_PROGRAM_NAME " watchdog --passenger-root /opt/passenger \\\n"); printf(" --BC --listen tcp://127.0.0.1:4000 /webapps/foo\n"); printf("\n"); printf(" Example 2: pass some arguments to the core, and some others to the\n"); printf(" UstRouter. The watchdog itself and the core will use logging\n"); printf(" level 3, while the UstRouter will use logging level 1.\n\n"); printf(" " SHORT_PROGRAM_NAME " watchdog --passenger-root /opt/passenger \\\n"); printf(" --BC --listen tcp://127.0.0.1:4000 /webapps/foo --EC \\\n"); printf(" --BU --log-level 1 --EU \\\n"); printf(" --log-level 3\n"); printf("\n"); printf("Required options:\n"); printf(" --passenger-root PATH The location to the " PROGRAM_NAME " source\n"); printf(" directory [A]\n"); printf("\n"); printf("Argument passing options (optional):\n"); printf(" --BC, --begin-core-args Signals the beginning of arguments to pass to the\n"); printf(" Passenger core\n"); printf(" --EC, --end-core-args Signals the end of arguments to pass to the\n"); printf(" Passenger core\n"); printf(" --BU, --begin-ust-router-args\n"); printf(" Signals the beginning of arguments to pass to the\n"); printf(" UstRouter\n"); printf(" --EU, --end-ust-router-args\n"); printf(" Signals the end of arguments to pass to the\n"); printf(" UstRouter\n"); printf("\n"); printf("Other options (optional):\n"); printf(" --api-listen ADDRESS Listen on the given address for API commands.\n"); printf(" The address must be formatted as tcp://IP:PORT for\n"); printf(" TCP sockets, or unix:PATH for Unix domain sockets.\n"); printf(" You can specify this option multiple times (up to\n"); printf(" %u times) to listen on multiple addresses.\n", SERVER_KIT_MAX_SERVER_ENDPOINTS - 1); printf(" --authorize [LEVEL]:USERNAME:PASSWORDFILE\n"); printf(" Enables authentication on the API server, through\n"); printf(" the given API account. LEVEL indicates the\n"); printf(" privilege level (see below). PASSWORDFILE must\n"); printf(" point to a file containing the password\n"); printf("\n"); printf(" --instance-registry-dir Directory to register instance into.\n"); printf(" Default: %s\n", getSystemTempDir()); printf("\n"); printf(" --no-user-switching Disables user switching support [A]\n"); printf(" --default-user NAME Default user to start apps as, when user\n"); printf(" switching is enabled. Default: " DEFAULT_WEB_APP_USER "\n"); printf(" --default-group NAME Default group to start apps as, when user\n"); printf(" switching is disabled. Default: the default\n"); printf(" user's primary group\n"); printf("\n"); printf(" --daemonize Daemonize into the background\n"); printf(" --user NAME Lower privilege to the given user\n"); printf(" --pid-file PATH Store the watchdog's PID in the given file. The\n"); printf(" file is deleted on exit\n"); printf(" --no-delete-pid-file Do not delete PID file on exit\n"); printf(" --log-file PATH Log to the given file.\n"); printf(" --log-level LEVEL Logging level. [A] Default: %d\n", DEFAULT_LOG_LEVEL); printf(" --report-file PATH Upon successful initialization, report instance\n"); printf(" information to the given file, in JSON format\n"); printf(" --cleanup-pidfile PATH Upon shutdown, kill the process specified by\n"); printf(" the given PID file\n"); printf("\n"); printf(" --ctl NAME=VALUE Set custom internal option\n"); printf("\n"); printf(" -h, --help Show this help\n"); printf("\n"); printf("[A] = Automatically passed to supervised agents\n"); printf("\n"); printf("API account privilege levels (ordered from most to least privileges):\n"); printf(" readonly Read-only access\n"); printf(" full Full access (default)\n"); } static void parseOptions(int argc, const char *argv[], ConfigKit::Store &config) { OptionParser p(usage); Json::Value updates(Json::objectValue); int i = 2; while (i < argc) { if (p.isValueFlag(argc, i, argv[i], '\0', "--passenger-root")) { updates["passenger_root"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--BC") || p.isFlag(argv[i], '\0', "--begin-core-args")) { i++; while (i < argc) { if (p.isFlag(argv[i], '\0', "--EC") || p.isFlag(argv[i], '\0', "--end-core-args")) { i++; break; } else if (p.isFlag(argv[i], '\0', "--BU") || p.isFlag(argv[i], '\0', "--begin-ust-router-args")) { break; } else { Json::Value coreUpdates(Json::objectValue); if (!parseCoreOption(argc, argv, i, coreUpdates)) { fprintf(stderr, "ERROR: unrecognized core argument %s. Please " "type '%s core --help' for usage.\n", argv[i], argv[0]); exit(1); } Json::Value::iterator it, end = coreUpdates.end(); for (it = coreUpdates.begin(); it != end; it++) { string translatedName = watchdogSchema->core.translator. reverseTranslateOne(it.name()); updates[translatedName] = *it; } } } } else if (p.isFlag(argv[i], '\0', "--BU") || p.isFlag(argv[i], '\0', "--begin-ust-router-args")) { i++; while (i < argc) { if (p.isFlag(argv[i], '\0', "--EU") || p.isFlag(argv[i], '\0', "--end-ust-router-args")) { i++; break; } else if (p.isFlag(argv[i], '\0', "--BC") || p.isFlag(argv[i], '\0', "--begin-core-args")) { break; } else { fprintf(stderr, "ERROR: unrecognized UstRouter argument %s. Please " "type '%s ust-router --help' for usage.\n", argv[i], argv[0]); exit(1); } } } else if (p.isValueFlag(argc, i, argv[i], '\0', "--api-listen")) { if (getSocketAddressType(argv[i + 1]) != SAT_UNKNOWN) { Json::Value &addresses = updates["watchdog_api_server_addresses"]; if (addresses.size() == SERVER_KIT_MAX_SERVER_ENDPOINTS - 1) { fprintf(stderr, "ERROR: you may specify up to %u --api-listen addresses.\n", SERVER_KIT_MAX_SERVER_ENDPOINTS - 1); exit(1); } addresses.append(argv[i + 1]); i += 2; } else { fprintf(stderr, "ERROR: invalid address format for --api-listen. The address " "must be formatted as tcp://IP:PORT for TCP sockets, or unix:PATH " "for Unix domain sockets.\n"); exit(1); } } else if (p.isValueFlag(argc, i, argv[i], '\0', "--authorize")) { vector args; split(argv[i + 1], ':', args); if (args.size() < 2 || args.size() > 3) { fprintf(stderr, "ERROR: invalid format for --authorize. The syntax " "is \"[LEVEL:]USERNAME:PASSWORDFILE\".\n"); exit(1); } updates["watchdog_api_server_authorizations"].append(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--instance-registry-dir")) { updates["instance_registry_dir"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--no-user-switching")) { updates["user_switching"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--default-user")) { updates["default_user"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--default-group")) { updates["default_group"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--daemonize")) { updates["daemonize"] = true; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--user")) { updates["user"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--pid-file")) { updates["watchdog_pid_file"] = argv[i + 1]; i += 2; } else if (p.isFlag(argv[i], '\0', "--no-delete-pid-file")) { updates["watchdog_pid_file_autodelete"] = false; i++; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--log-level")) { updates["log_level"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--report-file")) { updates["startup_report_file"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--cleanup-pidfile")) { updates["pidfiles_to_delete_on_exit"].append(argv[i + 1]); i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--log-file")) { updates["log_target"] = argv[i + 1]; i += 2; } else if (p.isValueFlag(argc, i, argv[i], '\0', "--ctl")) { const char *value = strchr(argv[i + 1], '='); if (value == NULL) { fprintf(stderr, "ERROR: '%s' is not a valid --ctl parameter. " "It must be in the form of NAME=VALUE.\n", argv[i + 1]); exit(1); } string name(argv[i + 1], value - argv[i + 1]); value++; if (*value == '\0') { fprintf(stderr, "ERROR: '%s' is not a valid --ctl parameter. " "The value must be non-empty.\n", argv[i + 1]); exit(1); } updates[name] = autocastValueToJson(value); i += 2; } else if (p.isFlag(argv[i], 'h', "--help")) { usage(); exit(0); } else { fprintf(stderr, "ERROR: unrecognized argument %s. Please type " "'%s watchdog --help' for usage.\n", argv[i], argv[0]); exit(1); } } if (!updates.empty()) { vector errors; if (!config.update(updates, errors)) { P_BUG("Unable to set initial configuration: " << ConfigKit::toString(errors) << "\n" "Raw initial configuration: " << updates.toStyledString()); } } } static void initializeBareEssentials(int argc, char *argv[], WorkingObjectsPtr &wo) { /* * Some Apache installations (like on OS X) redirect stdout to /dev/null, * so that only stderr is redirected to the log file. We therefore * forcefully redirect stdout to stderr so that everything ends up in the * same place. */ dup2(2, 1); /* * Most operating systems overcommit memory. We *know* that this watchdog process * doesn't use much memory; on OS X it uses about 200 KB of private RSS. If the * watchdog is killed by the system Out-Of-Memory Killer or then it's all over: * the system administrator will have to restart the web server for Phusion * Passenger to be usable again. So here we disable Linux's OOM killer * for this watchdog. Note that the OOM score is inherited by child processes * so we need to restore it after each fork(). */ #if !BOOST_OS_MACOS string oldOomScore = setOomScoreNeverKill(); #endif watchdogWrapperRegistry = new WrapperRegistry::Registry(); watchdogWrapperRegistry->finalize(); watchdogSchema = new Schema(watchdogWrapperRegistry); watchdogConfig = new ConfigKit::Store(*watchdogSchema); initializeAgent(argc, &argv, SHORT_PROGRAM_NAME " watchdog", *watchdogConfig, watchdogSchema->core.schema.loggingKit.translator, parseOptions, NULL, 2); // Start all sub-agents with this environment variable. setenv("PASSENGER_USE_FEEDBACK_FD", "true", 1); wo = boost::make_shared(); workingObjects = wo.get(); #if !BOOST_OS_MACOS wo->extraConfigToPassToSubAgents["oom_score"] = oldOomScore; #endif } static void maybeSetsid() { /* 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(). * * WatchdogLauncher.h already calls setsid() before exec()ing * the Watchdog, but Flying Passenger does not. */ if (watchdogConfig->get("setsid").asBool()) { setsid(); } } static void redirectStdinToNull() { int fd = open("/dev/null", O_RDONLY); if (fd != -1) { dup2(fd, 0); close(fd); } } static void maybeDaemonize() { pid_t pid; int e; if (watchdogConfig->get("daemonize").asBool()) { pid = fork(); if (pid == 0) { setsid(); redirectStdinToNull(); } else if (pid == -1) { e = errno; throw SystemException("Cannot fork", e); } else { _exit(0); } } } static void createPidFile() { TRACE_POINT(); Json::Value pidFile = watchdogConfig->get("watchdog_pid_file"); if (!pidFile.isNull()) { char pidStr[32]; snprintf(pidStr, sizeof(pidStr), "%lld", (long long) getpid()); int fd = syscalls::open(pidFile.asCString(), O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { int e = errno; throw FileSystemException("Cannot create PID file " + pidFile.asString(), e, pidFile.asString()); } UPDATE_TRACE_POINT(); FdGuard guard(fd, __FILE__, __LINE__); writeExact(fd, pidStr, strlen(pidStr)); } } static void openStartupReportFile(const WorkingObjectsPtr &wo) { TRACE_POINT(); Json::Value path = watchdogConfig->get("startup_report_file"); if (!path.isNull()) { int fd = syscalls::open(path.asCString(), O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd == -1) { int e = errno; throw FileSystemException("Cannot open report file " + path.asString(), e, path.asString()); } P_LOG_FILE_DESCRIPTOR_OPEN4(fd, __FILE__, __LINE__, "WorkingObjects: startupReportFile"); wo->startupReportFile = fd; } } static void chdirToTmpDir() { const Json::Value pidfiles = watchdogConfig->get("pidfiles_to_delete_on_exit"); if (!pidfiles.empty()) { string str = pidfiles[0].asString(); string dir = str.substr(0,str.find_last_of('/')); if (dir != "" && chdir(dir.c_str()) == -1) { throw RuntimeException("Cannot change working directory to " + dir); } } } static void lowerPrivilege() { TRACE_POINT(); string userName = watchdogConfig->get("user").asString(); if (geteuid() == 0 && !userName.empty()) { OsUser osUser; if (!lookupSystemUserByName(userName, osUser)) { throw NonExistentUserException("Operating system user '" + userName + "' does not exist"); } gid_t gid = osUser.pwd.pw_gid; string groupName = lookupSystemGroupnameByGid(gid); if (initgroups(userName.c_str(), gid) != 0) { int e = errno; throw SystemException("Unable to lower " SHORT_PROGRAM_NAME " watchdog's privilege " "to that of user '" + userName + "' and group '" + groupName + "': cannot set supplementary groups", e); } if (setgid(gid) != 0) { int e = errno; throw SystemException("Unable to lower " SHORT_PROGRAM_NAME " watchdog's privilege " "to that of user '" + userName + "' and group '" + groupName + "': cannot set group ID to " + toString(gid), e); } if (setuid(osUser.pwd.pw_uid) != 0) { int e = errno; throw SystemException("Unable to lower " SHORT_PROGRAM_NAME " watchdog's privilege " "to that of user '" + userName + "' and group '" + groupName + "': cannot set user ID to " + toString(osUser.pwd.pw_uid), e); } #ifdef __linux__ // When we change the uid, /proc/self/pid contents don't change owner, // causing us to lose access to our own /proc/self/pid files. // This prctl call changes those files' ownership. // References: // https://stackoverflow.com/questions/8337846/files-ownergroup-doesnt-change-at-location-proc-pid-after-setuid // http://man7.org/linux/man-pages/man5/proc.5.html (search for "dumpable") prctl(PR_SET_DUMPABLE, 1); #endif setenv("USER", osUser.pwd.pw_name, 1); setenv("HOME", osUser.pwd.pw_dir, 1); setenv("UID", toString(gid).c_str(), 1); } } static void lookupDefaultUidGid(uid_t &uid, gid_t &gid) { const string defaultUser = watchdogConfig->get("default_user").asString(); const string defaultGroup = watchdogConfig->get("default_group").asString(); OsUser osUser; if (!lookupSystemUserByName(defaultUser, osUser)) { throw NonExistentUserException("Default user '" + defaultUser + "' does not exist"); } uid = osUser.pwd.pw_uid; OsGroup osGroup; if (!lookupSystemGroupByName(defaultGroup, osGroup)) { throw NonExistentGroupException("Default group '" + defaultGroup + "' does not exist"); } gid = osGroup.grp.gr_gid; } static void warnIfInstanceDirVulnerable(const string &root) { TRACE_POINT(); if (geteuid() != 0) { return; // Passenger is not root, so no escalation. } vector errors, checkErrors; if (isPathProbablySecureForRootUse(root, errors, checkErrors)) { if (!checkErrors.empty()) { string message = "WARNING: unable to perform privilege escalation vulnerability detection:\n"; foreach (string line, checkErrors) { message.append("\n - " + line); } P_WARN(message); } } else { string message = "WARNING: potential privilege escalation vulnerability detected. " \ PROGRAM_NAME " is running as root, and part(s) of the " SHORT_PROGRAM_NAME " instance directory (" + root + ") can be changed by non-root user(s):\n"; foreach (string line, errors) { message.append("\n - " + line); } foreach (string line, checkErrors) { message.append("\n - " + line); } message.append("\n\nPlease either fix up the permissions for the insecure paths, or use" \ " a different location for the instance dir that can only be modified by root."); P_WARN(message); } } static void initializeWorkingObjects(const WorkingObjectsPtr &wo, InstanceDirToucherPtr &instanceDirToucher, uid_t uidBeforeLoweringPrivilege) { TRACE_POINT(); Json::Value doc; Json::Value::iterator it, end; UPDATE_TRACE_POINT(); lookupDefaultUidGid(wo->defaultUid, wo->defaultGid); doc = watchdogConfig->get("pidfiles_to_delete_on_exit"); for (it = doc.begin(); it != doc.end(); it++) { wo->cleanupPidfiles.push_back(it->asString()); } UPDATE_TRACE_POINT(); InstanceDirectory::CreationOptions instanceOptions; instanceOptions.userSwitching = watchdogConfig->get("user_switching").asBool(); instanceOptions.originalUid = uidBeforeLoweringPrivilege; instanceOptions.defaultUid = wo->defaultUid; instanceOptions.defaultGid = wo->defaultGid; instanceOptions.properties["name"] = wo->randomGenerator.generateAsciiString(8); instanceOptions.properties["integration_mode"] = watchdogConfig->get("integration_mode").asString(); instanceOptions.properties["server_software"] = watchdogConfig->get("server_software").asString(); if (watchdogConfig->get("integration_mode").asString() == "standalone") { instanceOptions.properties["standalone_engine"] = watchdogConfig->get("standalone_engine").asString(); } // check if path is safe warnIfInstanceDirVulnerable(watchdogConfig->get("instance_registry_dir").asString()); wo->instanceDir = boost::make_shared(instanceOptions, watchdogConfig->get("instance_registry_dir").asString()); wo->extraConfigToPassToSubAgents["instance_dir"] = wo->instanceDir->getPath(); instanceDirToucher = boost::make_shared(wo); UPDATE_TRACE_POINT(); string lockFilePath = wo->instanceDir->getPath() + "/lock"; wo->lockFile = syscalls::open(lockFilePath.c_str(), O_RDONLY); if (wo->lockFile == -1) { int e = errno; throw FileSystemException("Cannot open " + lockFilePath + " for reading", e, lockFilePath); } P_LOG_FILE_DESCRIPTOR_OPEN4(wo->lockFile, __FILE__, __LINE__, "WorkingObjects: lock file"); createFile(wo->instanceDir->getPath() + "/watchdog.pid", toString(getpid())); UPDATE_TRACE_POINT(); string readOnlyAdminPassword = wo->randomGenerator.generateAsciiString(24); string fullAdminPassword = wo->randomGenerator.generateAsciiString(24); if (geteuid() == 0 && !watchdogConfig->get("user_switching").asBool()) { createFile(wo->instanceDir->getPath() + "/read_only_admin_password.txt", readOnlyAdminPassword, S_IRUSR, wo->defaultUid, wo->defaultGid); createFile(wo->instanceDir->getPath() + "/full_admin_password.txt", fullAdminPassword, S_IRUSR, wo->defaultUid, wo->defaultGid); } else { createFile(wo->instanceDir->getPath() + "/read_only_admin_password.txt", readOnlyAdminPassword, S_IRUSR | S_IWUSR); createFile(wo->instanceDir->getPath() + "/full_admin_password.txt", fullAdminPassword, S_IRUSR | S_IWUSR); } if (watchdogConfig->get("core_pid_file").isNull()) { wo->corePidFile = wo->instanceDir->getPath() + "/core.pid"; } else { wo->corePidFile = watchdogConfig->get("core_pid_file").asString(); } wo->fdPassingPassword = wo->randomGenerator.generateAsciiString(24); UPDATE_TRACE_POINT(); wo->controllerAddresses.append("unix:" + wo->instanceDir->getPath() + "/agents.s/core"); doc = watchdogConfig->get("controller_addresses"); for (it = doc.begin(); it != doc.end(); it++) { wo->controllerAddresses.append(*it); } wo->coreApiServerAddresses.append("unix:" + wo->instanceDir->getPath() + "/agents.s/core_api"); doc = watchdogConfig->get("core_api_server_addresses"); for (it = doc.begin(); it != doc.end(); it++) { wo->coreApiServerAddresses.append(*it); } UPDATE_TRACE_POINT(); wo->coreApiServerAuthorizations.append( "readonly:ro_admin:" + wo->instanceDir->getPath() + "/read_only_admin_password.txt"); wo->coreApiServerAuthorizations.append( "full:admin:" + wo->instanceDir->getPath() + "/full_admin_password.txt"); doc = watchdogConfig->get("core_api_server_authorizations"); for (it = doc.begin(); it != doc.end(); it++) { wo->coreApiServerAuthorizations.append(*it); } } static void initializeAgentWatchers(const WorkingObjectsPtr &wo, vector &watchers) { TRACE_POINT(); watchers.push_back(boost::make_shared(wo)); } static void makeFileWorldReadableAndWritable(const string &path) { int ret; do { ret = chmod(path.c_str(), parseModeString("u=rw,g=rw,o=rw")); } while (ret == -1 && errno == EINTR); } static void initializeApiServer(const WorkingObjectsPtr &wo) { TRACE_POINT(); Json::Value doc; Json::Value::iterator it; string description; UPDATE_TRACE_POINT(); wo->watchdogApiServerAuthorizations.append( "readonly:ro_admin:" + wo->instanceDir->getPath() + "/read_only_admin_password.txt"); wo->watchdogApiServerAuthorizations.append( "full:admin:" + wo->instanceDir->getPath() + "/full_admin_password.txt"); doc = watchdogConfig->get("watchdog_api_server_authorizations"); for (it = doc.begin(); it != doc.end(); it++) { wo->watchdogApiServerAuthorizations.append(*it); } UPDATE_TRACE_POINT(); wo->watchdogApiServerAddresses.append( "unix:" + wo->instanceDir->getPath() + "/agents.s/watchdog_api"); doc = watchdogConfig->get("watchdog_api_server_addresses"); for (it = doc.begin(); it != doc.end(); it++) { wo->watchdogApiServerAddresses.append(*it); } UPDATE_TRACE_POINT(); for (unsigned int i = 0; i < wo->watchdogApiServerAddresses.size(); i++) { string address = wo->watchdogApiServerAddresses[i].asString(); P_DEBUG("API server will listen on " << address); wo->apiServerFds[i] = createServer(address, 0, true, __FILE__, __LINE__); if (getSocketAddressType(address) == SAT_UNIX) { makeFileWorldReadableAndWritable(parseUnixSocketAddress(address)); } } UPDATE_TRACE_POINT(); Json::Value contextConfig = watchdogConfig->inspectEffectiveValues(); wo->bgloop = new BackgroundEventLoop(true, true); wo->serverKitContext = new ServerKit::Context( watchdogSchema->apiServerKit.schema, contextConfig, watchdogSchema->apiServerKit.translator); wo->serverKitContext->libev = wo->bgloop->safe; wo->serverKitContext->libuv = wo->bgloop->libuv_loop; wo->serverKitContext->initialize(); UPDATE_TRACE_POINT(); Json::Value apiServerConfig = watchdogConfig->inspectEffectiveValues(); apiServerConfig["fd_passing_password"] = wo->fdPassingPassword; apiServerConfig["authorizations"] = wo->watchdogApiServerAuthorizations; wo->apiServer = new ApiServer::ApiServer( wo->serverKitContext, watchdogSchema->apiServer.schema, apiServerConfig, watchdogSchema->apiServer.translator); wo->apiServer->exitEvent = &wo->exitEvent; wo->apiServer->initialize(); for (unsigned int i = 0; i < wo->watchdogApiServerAddresses.size(); i++) { wo->apiServer->listen(wo->apiServerFds[i]); } } static void startAgents(const WorkingObjectsPtr &wo, vector &watchers) { TRACE_POINT(); foreach (AgentWatcherPtr watcher, watchers) { P_DEBUG("Starting agent: " << watcher->name()); try { watcher->start(); } catch (const std::exception &e) { if (feedbackFdAvailable()) { writeArrayMessage(FEEDBACK_FD, "Watchdog startup error", e.what(), NULL); } else { const oxt::tracable_exception *e2 = dynamic_cast(&e); if (e2 != NULL) { P_CRITICAL("ERROR: " << e2->what() << "\n" << e2->backtrace()); } else { P_CRITICAL("ERROR: " << e.what()); } } forceAllAgentsShutdown(wo, watchers); cleanup(wo); exit(1); } // Allow other exceptions to propagate and crash the watchdog. } } static void beginWatchingAgents(const WorkingObjectsPtr &wo, vector &watchers) { foreach (AgentWatcherPtr watcher, watchers) { try { watcher->beginWatching(); } catch (const std::exception &e) { writeArrayMessage(FEEDBACK_FD, "Watchdog startup error", e.what(), NULL); forceAllAgentsShutdown(wo, watchers); cleanup(wo); exit(1); } // Allow other exceptions to propagate and crash the watchdog. } } static void reportStartupResult(const WorkingObjectsPtr &wo, const vector &watchers) { TRACE_POINT(); Json::Value report; report["instance_dir"] = wo->instanceDir->getPath(); foreach (AgentWatcherPtr watcher, watchers) { watcher->reportAgentStartupResult(report); } if (feedbackFdAvailable()) { writeArrayMessage(FEEDBACK_FD, "Agents information", NULL); writeScalarMessage(FEEDBACK_FD, report.toStyledString()); } if (wo->startupReportFile != -1) { string str = report.toStyledString(); writeExact(wo->startupReportFile, str.data(), str.size()); close(wo->startupReportFile); P_LOG_FILE_DESCRIPTOR_CLOSE(wo->startupReportFile); wo->startupReportFile = -1; } } static void finalizeInstanceDir(const WorkingObjectsPtr &wo) { TRACE_POINT(); #ifdef HAVE_FLOCK if (flock(wo->lockFile, LOCK_EX) == -1) { int e = errno; throw SystemException("Cannot obtain exclusive lock on the " "instance directory lock file", e); } #endif wo->instanceDir->finalizeCreation(); } static void cleanup(const WorkingObjectsPtr &wo) { TRACE_POINT(); // We need to call destroy() explicitly because of circular references. if (wo->instanceDir != NULL && wo->instanceDir->isOwner()) { wo->instanceDir->destroy(); wo->instanceDir.reset(); } killCleanupPids(wo); deletePidFile(wo); } int watchdogMain(int argc, char *argv[]) { WorkingObjectsPtr wo; initializeBareEssentials(argc, argv, wo); P_NOTICE("Starting " SHORT_PROGRAM_NAME " watchdog..."); InstanceDirToucherPtr instanceDirToucher; vector watchers; uid_t uidBeforeLoweringPrivilege = geteuid(); try { TRACE_POINT(); maybeSetsid(); maybeDaemonize(); createPidFile(); openStartupReportFile(wo); chdirToTmpDir(); lowerPrivilege(); initializeWorkingObjects(wo, instanceDirToucher, uidBeforeLoweringPrivilege); initializeAgentWatchers(wo, watchers); initializeApiServer(wo); UPDATE_TRACE_POINT(); runHookScriptAndThrowOnError("before_watchdog_initialization"); } catch (const std::exception &e) { if (feedbackFdAvailable()) { writeArrayMessage(FEEDBACK_FD, "Watchdog startup error", e.what(), NULL); } else { const oxt::tracable_exception *e2 = dynamic_cast(&e); if (e2 != NULL) { P_CRITICAL("ERROR: " << e2->what() << "\n" << e2->backtrace()); } else { P_CRITICAL("ERROR: " << e.what()); } } if (wo != NULL) { cleanup(wo); } return 1; } // Allow other exceptions to propagate and crash the watchdog. try { TRACE_POINT(); startAgents(wo, watchers); beginWatchingAgents(wo, watchers); reportStartupResult(wo, watchers); finalizeInstanceDir(wo); P_INFO("All " PROGRAM_NAME " agents started!"); UPDATE_TRACE_POINT(); runHookScriptAndThrowOnError("after_watchdog_initialization"); UPDATE_TRACE_POINT(); boost::this_thread::disable_interruption di; boost::this_thread::disable_syscall_interruption dsi; bool shouldExitGracefully = waitForStarterProcessOrWatchers(wo, watchers); if (shouldExitGracefully) { /* Fork a child process which cleans up all the agent processes in * the background and exit this watchdog process so that we don't block * the web server. */ P_DEBUG("Web server exited gracefully; gracefully shutting down all agents..."); } else { P_DEBUG("Web server did not exit gracefully, forcing shutdown of all agents..."); } UPDATE_TRACE_POINT(); runHookScriptAndThrowOnError("before_watchdog_shutdown"); UPDATE_TRACE_POINT(); AgentWatcher::stopWatching(watchers); if (shouldExitGracefully) { UPDATE_TRACE_POINT(); cleanupAgentsInBackground(wo, watchers, argv); // Child process will call cleanup() } else { UPDATE_TRACE_POINT(); forceAllAgentsShutdown(wo, watchers); cleanup(wo); } UPDATE_TRACE_POINT(); runHookScriptAndThrowOnError("after_watchdog_shutdown"); return shouldExitGracefully ? 0 : 1; } catch (const tracable_exception &e) { P_CRITICAL("ERROR: " << e.what() << "\n" << e.backtrace()); cleanup(wo); return 1; } }