hphp/runtime/server/admin-request-handler.cpp (1,339 lines of code) (raw):
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/server/admin-request-handler.h"
#include "hphp/runtime/base/apc-stats.h"
#include "hphp/runtime/base/datetime.h"
#include "hphp/runtime/base/file-util.h"
#include "hphp/runtime/base/hhprof.h"
#include "hphp/runtime/base/http-client.h"
#include "hphp/runtime/base/ini-setting.h"
#include "hphp/runtime/base/init-fini-node.h"
#include "hphp/runtime/base/memory-manager.h"
#include "hphp/runtime/base/preg.h"
#include "hphp/runtime/base/program-functions.h"
#include "hphp/runtime/base/rds.h"
#include "hphp/runtime/base/request-tracing.h"
#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/base/stat-cache.h"
#include "hphp/runtime/base/timestamp.h"
#include "hphp/runtime/base/unit-cache.h"
#include "hphp/runtime/vm/debug/debug.h"
#include "hphp/runtime/vm/jit/cg-meta.h"
#include "hphp/runtime/vm/jit/fixup.h"
#include "hphp/runtime/vm/jit/mcgen.h"
#include "hphp/runtime/vm/jit/prof-data-serialize.h"
#include "hphp/runtime/vm/jit/prof-data.h"
#include "hphp/runtime/vm/jit/relocation.h"
#include "hphp/runtime/vm/jit/tc-record.h"
#include "hphp/runtime/vm/jit/tc.h"
#include "hphp/runtime/vm/named-entity.h"
#include "hphp/runtime/vm/treadmill.h"
#include "hphp/runtime/vm/type-profile.h"
#include "hphp/runtime/ext/apc/ext_apc.h"
#include "hphp/runtime/ext/json/ext_json.h"
#include "hphp/runtime/ext/xenon/ext_xenon.h"
#include "hphp/runtime/server/http-request-handler.h"
#include "hphp/runtime/server/http-server.h"
#include "hphp/runtime/server/memory-stats.h"
#include "hphp/runtime/server/pagelet-server.h"
#include "hphp/runtime/server/rpc-request-handler.h"
#include "hphp/runtime/server/server-stats.h"
#include "hphp/util/alloc.h"
#include "hphp/util/build-info.h"
#include "hphp/util/hphp-config.h"
#include "hphp/util/hugetlb.h"
#include "hphp/util/logger.h"
#include "hphp/util/managed-arena.h"
#include "hphp/util/mutex.h"
#include "hphp/util/numa.h"
#include "hphp/util/process.h"
#include "hphp/util/service-data.h"
#include "hphp/util/stacktrace-profiler.h"
#include "hphp/util/timer.h"
#ifdef ENABLE_EXTENSION_MYSQL
#include "hphp/runtime/ext/mysql/mysql_stats.h"
#endif
#include <folly/Conv.h>
#include <folly/File.h>
#include <folly/FileUtil.h>
#include <folly/Random.h>
#include <folly/portability/Unistd.h>
#include <cstdio>
#include <fstream>
#include <string>
#include <sstream>
#include <iomanip>
#if defined(FACEBOOK) || defined(HAVE_LIBSODIUM)
#include <sodium.h>
#ifdef crypto_pwhash_STRBYTES
#define HAVE_CRYPTO_PWHASH_STR
#endif
#endif
#ifdef GOOGLE_CPU_PROFILER
#include <gperftools/profiler.h>
#include "hphp/runtime/base/file-util.h"
#endif
namespace HPHP {
using std::endl;
using std::string;
///////////////////////////////////////////////////////////////////////////////
AdminCommandExt* AdminCommandExt::s_head{nullptr};
THREAD_LOCAL(AccessLog::ThreadData, AdminRequestHandler::s_accessLogThreadData);
AccessLog AdminRequestHandler::s_accessLog(
&(AdminRequestHandler::getAccessLogThreadData));
AdminRequestHandler::AdminRequestHandler(int timeout) :
RequestHandler(timeout) {
}
// Helper machinery for jemalloc-stats-print command.
#ifdef USE_JEMALLOC
struct malloc_write {
char *s;
size_t slen;
size_t smax;
bool oom;
};
void malloc_write_init(malloc_write *mw) {
mw->s = nullptr;
mw->slen = 0;
mw->smax = 0;
mw->oom = false;
}
void malloc_write_fini(malloc_write *mw) {
if (mw->s != nullptr) {
free(mw->s);
malloc_write_init(mw);
}
}
static void malloc_write_cb(void *cbopaque, const char *s) {
malloc_write* mw = (malloc_write*)cbopaque;
size_t slen = strlen(s);
if (mw->oom || slen == 0) {
return;
}
if (mw->slen + slen+1 >= mw->smax) {
assertx(mw->slen + slen > 0);
char* ts = (char*)realloc(mw->s, (mw->slen + slen) << 1);
if (ts == nullptr) {
mw->oom = true;
return;
}
mw->s = ts;
mw->smax = (mw->slen + slen) << 1;
}
memcpy(&mw->s[mw->slen], s, slen+1);
mw->slen += slen;
}
#endif
void WarnIfNotOK(Transport* transport) {
auto code = static_cast<Transport::StatusCode>(transport->getResponseCode());
if (code != Transport::StatusCode::OK) {
Logger::Warning("Non-OK response from admin server: %d %s",
static_cast<int>(code),
transport->getResponseInfo().c_str());
}
}
#ifdef HPHP_TRACE
namespace {
/*
* Task to trace a total of 'count' requests whose URL contain 'url', using the
* module:level,... specificaion 'spec' (see HPHP::Trace).
*
* To ensure an unbroken trace output stream, the task is held locally by the
* thread currently tracing its request. If filtering on URL, threads may pass
* the task around to find matches faster. Concurrent tasks are not supported.
*/
struct TraceTask {
const std::string spec;
const std::string url;
int64_t count;
};
std::atomic<TraceTask*> s_traceTask{nullptr}; // Task up for grabs.
__thread TraceTask* tl_traceTask{nullptr};
InitFiniNode s_traceRequestStart([]() {
if (!tl_traceTask && !s_traceTask.load(std::memory_order_acquire)) return;
if (!tl_traceTask) {
// Try grab the task.
tl_traceTask = s_traceTask.exchange(nullptr, std::memory_order_acq_rel);
}
if (!tl_traceTask) return; // We lost the race; nothing to do.
if (tl_traceTask->count == 0) {
// Task already complete.
Trace::trace("Trace complete at %d\n", (int)time(nullptr));
Trace::setTraceThread("");
delete tl_traceTask;
tl_traceTask = nullptr;
return;
}
const string url = g_context->getRequestUrl();
if (url.find(tl_traceTask->url) == string::npos) {
// URL mismatch; hand task back (and discard any unlikely colliding task).
delete s_traceTask.exchange(tl_traceTask, std::memory_order_acq_rel);
tl_traceTask = nullptr;
Trace::setTraceThread("");
} else {
// Work on task.
--tl_traceTask->count;
const auto spec = tl_traceTask->spec;
Trace::setTraceThread(spec);
Trace::trace("Trace for %s at %d using spec %s\n",
url.c_str(), (int)time(nullptr), spec.c_str());
}
}, InitFiniNode::When::RequestStart, "trace");
} // namespace
#endif // HPHP_TRACE
void AdminRequestHandler::logToAccessLog(Transport* transport) {
GetAccessLog().onNewRequest();
GetAccessLog().log(transport, nullptr);
WarnIfNotOK(transport);
}
void AdminRequestHandler::setupRequest(Transport* transport) {
auto const cmd = transport->getCommand();
if (strncmp(cmd.c_str(), "dump-apc", 8) == 0) {
hphp_session_init(Treadmill::SessionKind::AdminPort);
} else {
g_context.getCheck();
}
GetAccessLog().onNewRequest();
}
void AdminRequestHandler::teardownRequest(Transport* transport) noexcept {
SCOPE_EXIT {
auto const cmd = transport->getCommand();
if (strncmp(cmd.c_str(), "dump-apc", 8) == 0) {
hphp_context_exit();
hphp_session_exit();
} else {
hphp_memory_cleanup();
}
};
GetAccessLog().log(transport, nullptr);
WarnIfNotOK(transport);
}
namespace {
// When this struct is destroyed, it will close the file.
struct DumpFile {
std::string path;
folly::File file;
};
Optional<DumpFile> dump_file(const char* name) {
auto const path = folly::sformat("{}/{}", RO::AdminDumpPath, name);
// mkdir -p the directory prefix of `path`
if (FileUtil::mkdir(path) != 0) return std::nullopt;
// If remove fails because of a permissions issue, then we won't be
// able to open the file for exclusive write below.
remove(path.c_str());
// Create the file, failing if it already exists. Doing so ensures
// that we have write access to the file and that no other user does.
auto const fd = open(path.c_str(), O_CREAT|O_EXCL|O_RDWR, 0666);
if (fd < 0) return std::nullopt;
return DumpFile{path, folly::File(fd, /*owns=*/true)};
}
}
void AdminRequestHandler::handleRequest(Transport *transport) {
transport->addHeader("Content-Type", "text/plain");
std::string cmd = transport->getCommand();
do {
if (cmd == "" || cmd == "help") {
string usage =
"/stop: stop the web server\n"
" instance-id optional, if specified, instance ID has to match\n"
"/oom-kill: abort all requests whose memory usage exceed\n"
" Server.RequestMemoryOOMKillBytes\n"
"/free-mem: ask allocator to release unused memory to system\n"
"/prepare-to-stop: ask the server to prepare for stopping\n"
"/flush-profile: flush profiling counters (in -fprofile-gen builds)\n"
"/flush-logs: trigger batching log-writers to flush all content\n"
"/translate: translate hex encoded stacktrace in 'stack' param\n"
" stack required, stack trace to translate\n"
" build-id optional, if specified, build ID has to match\n"
" bare optional, whether to display frame ordinates\n"
"/build-id: returns build id that's passed in from command line"
"\n"
"/instance-id: instance id that's passed in from command line\n"
"/compiler-id: returns the compiler id that built this app\n"
"/config-id: returns the config id passed in from command line\n"
"/repo-schema: return the repo schema id used by this app\n"
"/ini-get-all: dump all settings as JSON\n"
"/check-load: how many threads are actively handling requests\n"
"/check-queued: how many http requests are queued waiting to be\n"
" handled\n"
"/check-health: return json containing basic load/usage stats\n"
"/check-ev: how many http requests are active by libevent\n"
"/check-pl-load: how many pagelet threads are actively handling\n"
" requests\n"
"/check-pl-queued: how many pagelet requests are queued waiting to\n"
" be handled\n"
"/check-sql: report SQL table statistics\n"
"/check-sat how many satellite threads are actively handling\n"
" requests and queued waiting to be handled\n"
"/status.xml: show server status in XML\n"
"/status.json: show server status in JSON\n"
"/status.html: show server status in HTML\n"
"/rqtrace-stats: show aggregate request trace stats in JSON\n"
"/memory.xml: show memory status in XML\n"
"/memory.json: show memory status in JSON\n"
"/memory.html: show memory status in HTML\n"
"/statcache-clear: clear the stat cache entries\n"
"/stats-on: main switch: enable server stats\n"
"/stats-off: main switch: disable server stats\n"
"/stats-clear: clear all server stats\n"
"/stats-web: turn on/off server page stats (CPU and gen time)\n"
"/stats-mem: turn on/off memory statistics\n"
"/stats-sql: turn on/off SQL statistics\n"
"/stats-mutex: turn on/off mutex statistics\n"
" sampling optional, default 1000\n"
"/stats.keys: list all available keys\n"
" from optional, <timestamp>, or <-n> second ago\n"
" to optional, <timestamp>, or <-n> second ago\n"
"/stats.kvp: show server stats in key-value pairs\n"
" from optional, <timestamp>, or <-n> second ago\n"
" to optional, <timestamp>, or <-n> second ago\n"
" agg optional, aggragation: *, url, code\n"
" keys optional, <key>,<key/hit>,<key/sec>,<:regex:>\n"
" url optional, only stats of this page or URL\n"
" code optional, only stats of pages returning this code\n"
"/xenon-snap: generate a Xenon snapshot, which is logged later\n"
"/hugepage: show stats about hugepage usage\n"
"/jit-des-info: show information about deserialized profile data\n"
"/static-strings: get number of static strings\n"
"/static-strings-rds: ... that correspond to defined constants\n"
"/dump-static-strings: dump static strings to /tmp/static_strings\n"
"/random-static-strings: return randomly selected static strings\n"
" count number of strings to return, default 1\n"
"/dump-apc: dump all current value in APC to /tmp/apc_dump\n"
"/dump-apc-prefix: dump a key prefix contents from APC to\n"
" /tmp/apc_dump_prefix\n"
" prefix required, the prefix to dump\n"
" count optional, the number of keys to dump, default 1\n"
"/dump-apc-info: show basic APC stats\n"
"/dump-apc-meta: dump meta information for all objects in APC to\n"
" /tmp/apc_dump_meta\n"
"/random-apc: dump the key and size of a random APC entry\n"
" count number of entries to return\n"
"/treadmill: dump treadmill information\n"
"/pcre-cache-size: get pcre cache map size\n"
"/dump-pcre-cache: dump cached pcre's to /tmp/pcre_cache\n"
"/dump-array-info: dump array tracer info to /tmp/array_tracer_dump\n"
"/invalidate-units: remove specified files from the unit cache\n"
" path absolute path of files to invalidate\n"
"/start-stacktrace-profiler: set enable_stacktrace_profiler to true\n"
"/relocate: relocate translations\n"
" random optional, default false, relocate random subset\n"
" all optional, default false, relocate all translations\n"
" time optional, default 20 (seconds)\n"
#ifdef HPHP_TRACE
"/trace-request: write trace for next request(s) to "
+ RuntimeOption::getTraceOutputFile() + "\n"
" spec module:level,... spec; see hphp/util/trace.h\n"
" count optional, total requests to trace (default: 1)\n"
" url optional, trace only if URL contains \'url\'\n"
#endif
#ifdef GOOGLE_CPU_PROFILER
"/prof-cpu-on: turn on CPU profiler\n"
"/prof-cpu-off: turn off CPU profiler\n"
#endif
#ifdef EXECUTION_PROFILER
"/prof-exe: returns sampled execution profile\n"
#endif
"/vm-tcspace: show space used by translator caches\n"
"/vm-tcaddr: show addresses of translation cache sections\n"
"/vm-dump-tc: dump translation cache to /tmp/tc_dump_a and\n"
" /tmp/tc_dump_astub\n"
"/vm-namedentities:show size of the NamedEntityTable\n"
"/proxy: set up request proxy\n"
" origin URL to proxy requests to\n"
" percentage percentage of requests to proxy\n"
"/load-factor: get or set load factor\n"
" set optional, set new load factor (default 1.0,\n"
" valid range [-1.0, 10.0])\n"
"/queue-discount: get/set how much we discount the queue-length \n"
" set optional, set discount value (default 0,\n"
" valid range [0, 10000])\n"
"/warmup-status: Describes state of JIT warmup.\n"
" Returns empty string if warmed up.\n"
;
#ifdef USE_TCMALLOC
if (MallocExtensionInstance) {
usage.append(
"/tcmalloc-stats: get internal tcmalloc stats\n"
"/tcmalloc-set-tc: set max mem tcmalloc thread-cache can use\n"
);
}
#endif
#ifdef USE_JEMALLOC
if (mallctl) {
usage.append(
"/jemalloc-stats: get internal jemalloc stats\n"
"/jemalloc-stats-print:\n"
" get comprehensive jemalloc stats in\n"
" human-readable form\n"
"/jemalloc-prof-activate:\n"
" activate heap profiling\n"
"/jemalloc-prof-deactivate:\n"
" deactivate heap profiling\n"
"/jemalloc-prof-dump:\n"
" dump heap profile\n"
" file optional, filesystem path\n"
"/jemalloc-prof-request:\n"
" dump thread-local heap profile in\n"
" the next request that runs\n"
" file optional, filesystem path\n"
);
#ifdef ENABLE_HHPROF
usage.append(
"/hhprof/start: start profiling\n"
" requestType \"all\" or \"next\"*\n"
" url profile next matching url for \"next\"\n"
" lgSample lg sample rate\n"
" profileType \"current\"* or \"cumulative\"\n"
"/hhprof/status: configuration and current dump status\n"
"/hhprof/stop: stop profiling\n"
"/pprof/cmdline: program command line\n"
"/pprof/heap: heap dump\n"
"/pprof/symbol: symbol lookup\n"
);
#endif // ENABLE_HHPROF
}
#endif // USE_JEMALLOC
AdminCommandExt::iterate([&](AdminCommandExt* ace) {
usage.append(ace->usage());
return false;
});
transport->sendString(usage);
break;
}
bool needs_password = (cmd != "build-id") && (cmd != "compiler-id") &&
(cmd != "instance-id") && (cmd != "flush-logs") &&
(cmd != "warmup-status") && (cmd != "config-id")
#if defined(ENABLE_HHPROF) && defined(USE_JEMALLOC)
&& (mallctl == nullptr || (
(cmd != "hhprof/start")
&& (cmd != "hhprof/status")
&& (cmd != "hhprof/stop")
&& (cmd != "pprof/cmdline")
&& (cmd != "pprof/heap")
&& (cmd != "pprof/symbol")))
#endif
;
// When configured, we allow read-only stats to be read without a password.
if (needs_password && !RuntimeOption::AdminServerStatsNeedPassword) {
if ((strncmp(cmd.c_str(), "memory.", 7) == 0) ||
(strncmp(cmd.c_str(), "stats.", 6) == 0) ||
(strncmp(cmd.c_str(), "check-", 6) == 0) ||
(strncmp(cmd.c_str(), "static-strings", 14) == 0) ||
cmd == "hugepage" || cmd == "pcre-cache-size" ||
cmd == "vm-tcspace" || cmd == "vm-tcaddr" ||
cmd == "vm-namedentities" || cmd == "jemalloc-stats") {
needs_password = false;
}
}
if (needs_password && !RuntimeOption::HashedAdminPasswords.empty()) {
bool matched = false;
#ifdef HAVE_CRYPTO_PWHASH_STR
const auto password = transport->getParam("auth");
for (const std::string& hash : RuntimeOption::HashedAdminPasswords) {
if (crypto_pwhash_str_verify(hash.data(),
password.data(),
password.size()) == 0) {
matched = true;
break;
}
}
#endif
if (!matched) {
transport->sendString("Unauthorized", 401);
break;
}
} else if (needs_password && !RuntimeOption::AdminPasswords.empty()) {
std::set<std::string>::const_iterator iter =
RuntimeOption::AdminPasswords.find(transport->getParam("auth"));
if (iter == RuntimeOption::AdminPasswords.end()) {
transport->sendString("Unauthorized", 401);
break;
}
} else {
if (needs_password && !RuntimeOption::AdminPassword.empty() &&
RuntimeOption::AdminPassword != transport->getParam("auth")) {
transport->sendString("Unauthorized", 401);
break;
}
}
if (cmd == "stop") {
string instanceId = transport->getParam("instance-id");
if (!instanceId.empty() && instanceId != RuntimeOption::InstanceId) {
transport->sendString("Instance ID doesn't match.", 500);
break;
}
transport->sendString("OK\n");
Logger::Info("Got admin port stop request from %s",
transport->getRemoteHost());
HttpServer::Server->stop();
break;
}
if (cmd == "oom-kill") {
Logger::Info("Invoking OOM killer upon admin port request from %s",
transport->getRemoteHost());
auto const server = HttpServer::Server->getPageServer();
RequestInfo::InvokeOOMKiller(server->getActiveWorker());
transport->sendString("OOM killer invoked");
break;
}
if (cmd == "free-mem") {
const auto before = Process::GetMemUsageMb();
std::string errStr;
if (purge_all(&errStr)) {
const auto after = Process::GetMemUsageMb();
transport->sendString(
folly::sformat("Purged {} -> {} MB RSS", before, after).c_str());
} else {
transport->sendString(errStr.c_str(), 500);
}
break;
}
if (cmd == "prepare-to-stop") {
Logger::Info("Got admin port prepare-to-stop request from %s",
transport->getRemoteHost());
MemInfo info, newInfo;
Process::GetMemoryInfo(info);
HttpServer::PrepareToStop();
// We may consider purge_all() here, too. But since requests
// are still coming in, it may not be very useful, and has some
// performance penalties.
// TODO: evaluate the effect of sync() and uncomment if
// desirable. It is blocking and can take some time, so do it
// in a separate thread.
// std::thread t{sync};
// t.detach();
transport->sendString("OK\n");
Process::GetMemoryInfo(newInfo);
Logger::FInfo("free/cached/buffer {}/{}/{} -> {}/{}/{}",
info.freeMb, info.cachedMb, info.buffersMb,
newInfo.freeMb, newInfo.cachedMb, newInfo.buffersMb);
break;
}
if (cmd == "flush-profile") {
HttpServer::ProfileFlush();
// send the response *after* flushing, so the caller knows the
// data has been updated.
transport->sendString("OK\n");
break;
}
if (cmd == "flush-logs") {
transport->sendString("OK\n");
Logger::FlushAll();
HttpRequestHandler::GetAccessLog().flushAllWriters();
AdminRequestHandler::GetAccessLog().flushAllWriters();
RPCRequestHandler::GetAccessLog().flushAllWriters();
break;
}
if (cmd == "set-log-level") {
string result("OK\n");
string level = transport->getParam("level");
if (level == "None") {
Logger::LogLevel = Logger::LogNone;
} else if (level == "Error") {
Logger::LogLevel = Logger::LogError;
} else if (level == "Warning") {
Logger::LogLevel = Logger::LogWarning;
} else if (level == "Info") {
Logger::LogLevel = Logger::LogInfo;
} else if (level == "Verbose") {
Logger::LogLevel = Logger::LogVerbose;
} else {
result = "Failed to set log level\n";
}
transport->sendString(result);
break;
}
#ifdef HPHP_TRACE
if (cmd == "trace-request") {
Trace::ensureInit(RuntimeOption::getTraceOutputFile());
// Just discard any existing task.
delete s_traceTask.exchange(
new TraceTask{transport->getParam("spec"),
transport->getParam("url"),
std::max(transport->getInt64Param("count"), 1ll)},
std::memory_order_acq_rel);
transport->sendString("OK\n");
break;
}
#endif
if (cmd == "build-id") {
transport->sendString(RuntimeOption::BuildId, 200);
break;
}
if (cmd == "instance-id") {
transport->sendString(RuntimeOption::InstanceId, 200);
break;
}
if (cmd == "compiler-id") {
transport->sendString(compilerId().begin(), 200);
break;
}
if (cmd == "config-id") {
transport->sendString(std::to_string(RuntimeOption::ConfigId), 200);
break;
}
if (cmd == "repo-schema") {
transport->sendString(repoSchemaId().begin(), 200);
break;
}
if (cmd == "translate") {
string buildId = transport->getParam("build-id");
if (!buildId.empty() && buildId != RuntimeOption::BuildId) {
transport->sendString("Build ID doesn't match.", 500);
break;
}
string translated = translate_stack(transport->getParam("stack").c_str(),
transport->getParam("bare").empty());
transport->sendString(translated);
break;
}
if (strncmp(cmd.c_str(), "check", 5) == 0 &&
handleCheckRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "status", 6) == 0 &&
handleStatusRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(),"memory", 6) == 0 &&
handleMemoryRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "stats", 5) == 0 &&
handleStatsRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "prof", 4) == 0 &&
handleProfileRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "dump-apc", 8) == 0 &&
handleDumpCacheRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "invalidate-units", 16) == 0 &&
handleInvalidateUnitRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "xenon-snap", 10) == 0) {
static int64_t s_lastSampleTime = 0;
auto const current = TimeStamp::Current();
if (current > s_lastSampleTime) {
s_lastSampleTime = current;
Xenon::getInstance().surpriseAll();
}
transport->sendString("a Xenon sample will be collected\n", 200);
break;
}
if (strncmp(cmd.c_str(), "hugepage", 9) == 0) {
#if USE_JEMALLOC_EXTENT_HOOKS
std::string msg =
folly::sformat("{} 1G huge pages active\n", num_1g_pages());
if (auto a = alloc::lowArena()) {
msg += a->reportStats();
}
if (auto a = alloc::highArena()) {
msg += a->reportStats();
}
transport->sendString(msg, 200);
#else
transport->sendString("", 200);
#endif
break;
}
if (strncmp(cmd.c_str(), "jit-des-info", 13) == 0) {
if (isJitSerializing() || !jit::ProfData::wasDeserialized()) {
transport->sendString("", 200);
break;
}
auto msg = folly::sformat("{}:{}",
jit::ProfData::buildHost()->slice(),
jit::ProfData::buildTime());
transport->sendString(msg, 200);
}
if (strncmp(cmd.c_str(), "static-strings", 14) == 0 &&
handleStaticStringsRequest(cmd, transport)) {
break;
}
if (strncmp(cmd.c_str(), "dump-static-strings", 19) == 0) {
if (auto file = dump_file("static_strings")) {
handleDumpStaticStringsRequest(file->file);
transport->sendString(folly::sformat("dumped to {}\n", file->path));
} else {
transport->sendString("Unable to mkdir or file already exists.\n");
}
break;
}
if (strncmp(cmd.c_str(), "random-static-strings", 21) == 0) {
handleRandomStaticStringsRequest(cmd, transport);
break;
}
if (strncmp(cmd.c_str(), "vm-", 3) == 0 &&
handleVMRequest(cmd, transport)) {
break;
}
if (cmd == "proxy") {
handleProxyRequest(cmd, transport);
break;
}
if (cmd == "statcache-clear") {
StatCache::clearCache();
break;
}
if (cmd == "pcre-cache-size") {
std::ostringstream size;
size << preg_pcre_cache_size() << endl;
transport->sendString(size.str());
break;
}
if (cmd == "dump-pcre-cache") {
if (auto file = dump_file("pcre_cache")) {
pcre_dump_cache(file->file);
transport->sendString(folly::sformat("dumped to {}\n", file->path));
} else {
transport->sendString("Unable to mkdir or file already exists.\n");
}
break;
}
if (cmd == "start-stacktrace-profiler") {
enable_stacktrace_profiler = true;
transport->sendString("OK\n");
break;
}
if (cmd == "warmup-status") {
transport->sendString(jit::tc::warmupStatusString());
break;
}
if (strncmp(cmd.c_str(), "random-apc", 10) == 0 &&
handleRandomApcRequest(cmd, transport)) {
break;
}
if (cmd == "treadmill") {
transport->sendString(Treadmill::dumpTreadmillInfo());
break;
}
if (cmd == "load-factor") {
auto const factorStr = transport->getParam("set");
if (factorStr.empty()) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(3)
<< HttpServer::LoadFactor.load();
transport->sendString(oss.str());
break;
}
double factor = 1.0;
if (sscanf(factorStr.c_str(), "%lf", &factor) < 1 ||
factor > 10 || factor < -1) {
transport->sendString("Invalid load factor spec: " + factorStr, 400);
break;
}
HttpServer::LoadFactor.store(factor, std::memory_order_relaxed);
transport->sendString(folly::sformat("Load factor updated to {}\n",
factor));
Logger::Info("Load factor updated to %lf", factor);
break;
}
if (cmd == "queue-discount") {
auto const discountStr = transport->getParam("set");
if (discountStr.empty()) {
transport->sendString(folly::to<string>(
HttpServer::QueueDiscount.load(std::memory_order_relaxed)));
break;
}
int queue_discount = 0;
if (sscanf(discountStr.c_str(), "%d", &queue_discount) < 1 ||
queue_discount > 10000 || queue_discount < 0) {
transport->sendString("Invalid queue discount spec: " +
discountStr, 400);
break;
}
HttpServer::QueueDiscount.store(queue_discount,
std::memory_order_relaxed);
transport->sendString(folly::sformat("Queue Discount updated to {}\n",
queue_discount));
Logger::Info("Queue Discount updated to %d", queue_discount);
break;
}
if (cmd == "ini-get-all") {
auto out = IniSetting::GetAllAsJSON();
transport->sendString(out.c_str());
break;
}
if (cmd == "numa-info") {
std::ostringstream out;
#ifdef HAVE_NUMA
out << "use_numa: " << use_numa << endl;
out << "numa_num_nodes: " << numa_num_nodes << endl;
out << "numa_node_mask: " << numa_node_mask << endl;
out << "numa_node_set: " << numa_node_set << endl;
#else
out << "HAVE_NUMA not defined" << endl;
#endif
transport->sendString(out.str());
break;
}
#ifdef USE_JEMALLOC
assertx(mallctlnametomib && mallctlbymib);
if (cmd == "jemalloc-stats") {
// jemalloc stats update is periodically triggered in the
// host-health-monitor thread.
uint32_t error = 0;
auto call_mallctl = [&](const char* statName) {
size_t value = 0;
if (mallctlRead<size_t, true>(statName, &value) != 0) {
error = 1;
}
return value;
};
size_t allocated = call_mallctl("stats.allocated");
size_t active = call_mallctl("stats.active");
size_t mapped = call_mallctl("stats.mapped");
auto const low_mapped = alloc::getLowMapped();
std::ostringstream stats;
stats << "<jemalloc-stats>" << endl;
stats << " <allocated>" << allocated << "</allocated>" << endl;
stats << " <active>" << active << "</active>" << endl;
stats << " <mapped>" << mapped << "</mapped>" << endl;
stats << " <low_mapped>" << low_mapped << "</low_mapped>" << endl;
stats << "</jemalloc-stats>" << endl;
transport->sendString(stats.str());
break;
}
if (cmd == "jemalloc-stats-print") {
malloc_write mwo;
malloc_write_init(&mwo);
malloc_stats_print(malloc_write_cb, (void *)&mwo, "");
if (mwo.oom) {
malloc_write_fini(&mwo);
transport->sendString("OOM\n");
break;
}
transport->sendString(mwo.s == nullptr ? "" : mwo.s);
malloc_write_fini(&mwo);
break;
}
if (cmd == "jemalloc-prof-activate") {
if (jemalloc_pprof_enable()) {
transport->sendString("Error in mallctl(\"prof.active\", true)\n");
} else {
transport->sendString("OK\n");
}
break;
}
if (cmd == "jemalloc-prof-deactivate") {
if (jemalloc_pprof_disable()) {
transport->sendString("Error in mallctl(\"prof.active\", false)\n");
} else {
transport->sendString("OK\n");
}
break;
}
if (cmd == "jemalloc-prof-dump") {
string f = transport->getParam("file");
if (jemalloc_pprof_dump(f, true)) {
transport->sendString("Error in mallctl(\"prof.dump\", " + f + ")\n");
} else {
transport->sendString("OK\n");
}
break;
}
if (cmd == "jemalloc-prof-request") {
auto f = transport->getParam("file");
bool success = MemoryManager::triggerProfiling(f);
if (success) {
transport->sendString("OK\n");
} else {
transport->sendString("Request profiling already triggered\n");
}
break;
}
#ifdef ENABLE_HHPROF
if (cmd == "hhprof/start") {
HHProf::HandleHHProfStart(transport);
break;
}
if (cmd == "hhprof/status") {
HHProf::HandleHHProfStatus(transport);
break;
}
if (cmd == "hhprof/stop") {
HHProf::HandleHHProfStop(transport);
break;
}
if (cmd == "pprof/cmdline") {
HHProf::HandlePProfCmdline(transport);
break;
}
if (cmd == "pprof/heap") {
HHProf::HandlePProfHeap(transport);
break;
}
if (cmd == "pprof/symbol") {
HHProf::HandlePProfSymbol(transport);
break;
}
#endif // ENABLE_HHPROF
#endif // USE_JEMALLOC
if (cmd == "rqtrace-stats") {
std::stringstream out;
bool first = true;
out << "{" << endl;
auto appendStat =
[&](folly::StringPiece name, folly::StringPiece kind, int64_t value) {
out << folly::format(
"{} \"{}_{}\":{}\n", first ? "" : ",", name, kind, value);
first = false;
};
rqtrace::visit_process_stats(
[&] (const StringData* name, rqtrace::EventStats stats) {
appendStat(name->data(), "duration", stats.total_duration);
appendStat(name->data(), "count", stats.total_count);
}
);
out << "}" << endl;
transport->sendString(out.str());
break;
}
if (AdminCommandExt::iterate([&](AdminCommandExt* ace) {
return ace->handleRequest(transport);
})) {
break;
}
transport->sendString("Unknown command: " + cmd + "\n", 404);
} while (0);
transport->onSendEnd();
}
void AdminRequestHandler::abortRequest(Transport *transport) {
g_context.getCheck();
transport->sendString("Service Unavailable", 503);
transport->onSendEnd();
}
///////////////////////////////////////////////////////////////////////////////
// stats commands
static bool toggle_switch(Transport *transport, bool &setting) {
setting = !setting;
transport->sendString(setting ? "On\n" : "Off\n");
return true;
}
static bool send_report(Transport *transport) {
std::string keys = transport->getParam("keys");
std::string prefix = transport->getParam("prefix");
std::string out = ServerStats::Report(keys, prefix);
transport->replaceHeader("Content-Type", "text/plain");
transport->sendString(out);
return true;
}
static bool send_status(Transport *transport, Writer::Format format,
const char *mime) {
transport->replaceHeader("Content-Type", mime);
transport->sendString(ServerStats::ReportStatus(format));
return true;
}
bool AdminRequestHandler::handleCheckRequest(const std::string &cmd,
Transport *transport) {
if (cmd == "check-load") {
int count = HttpServer::Server->getPageServer()->getActiveWorker();
transport->sendString(folly::to<std::string>(count));
return true;
}
if (cmd == "check-ev") {
int count =
HttpServer::Server->getPageServer()->getLibEventConnectionCount();
transport->sendString(folly::to<string>(count));
return true;
}
if (cmd == "check-queued") {
int count = HttpServer::Server->getPageServer()->getQueuedJobs();
transport->sendString(folly::to<string>(count));
return true;
}
if (cmd == "check-health") {
std::stringstream out;
bool first = true;
out << "{" << endl;
auto const appendStat = [&](folly::StringPiece name, int64_t value) {
out << folly::format("{} \"{}\":{}\n", first ? "" : ",", name, value);
first = false;
};
appendStat("hhbc-roarena-capac", hhbc_arena_capacity());
appendStat("hhbc-size", g_hhbc_size->getValue());
appendStat("rds", rds::usedBytes());
appendStat("rds-local", rds::usedLocalBytes());
appendStat("rds-persistent", rds::usedPersistentBytes());
appendStat("catch-traces", jit::numCatchTraces());
appendStat("fixups", jit::FixupMap::size());
appendStat("units", numLoadedUnits());
appendStat("funcs", Func::maxFuncIdNum());
appendStat("named-entities", NamedEntity::tableSize());
for (auto& pair : NamedEntity::tableStats()) {
appendStat(folly::sformat("named-entities-{}", pair.first), pair.second);
}
appendStat("static-strings", makeStaticStringCount());
appendStat("request-count", requestCount());
appendStat("jit-des", jit::ProfData::triedDeserialization());
appendStat("jit-des-succ", jit::ProfData::wasDeserialized());
/*
* We're only using globalProfData() here because admin requests don't call
* requestInitProfData(). Normal request threads should always use
* profData() which provides much better guarantees about the lifetime of
* the returned object.
*/
if (auto profData = jit::globalProfData()) {
appendStat("prof-funcs", profData->profilingFuncs());
appendStat("prof-bc", profData->profilingBCSize());
appendStat("opt-funcs", profData->optimizedFuncs());
}
if (RuntimeOption::EvalEnableReusableTC) {
auto const memInfos = jit::tc::getTCMemoryUsage();
for (auto const& info : memInfos) {
appendStat(folly::format("tc-{}-allocs", info.name).str(), info.allocs);
appendStat(folly::format("tc-{}-frees", info.name).str(), info.frees);
appendStat(folly::format("tc-{}-free-size", info.name).str(),
info.free_size);
appendStat(folly::format("tc-{}-free-blocks", info.name).str(),
info.free_blocks);
}
appendStat("tc-recorded-funcs", jit::tc::recordedFuncs());
appendStat("tc-smashed-calls", jit::tc::smashedCalls());
appendStat("tc-smashed-branches", jit::tc::smashedBranches());
}
out << "}" << endl;
transport->sendString(out.str());
return true;
}
if (cmd == "check-pl-load") {
int count = PageletServer::GetActiveWorker();
transport->sendString(folly::to<string>(count));
return true;
}
if (cmd == "check-pl-queued") {
int count = PageletServer::GetQueuedJobs();
transport->sendString(folly::to<string>(count));
return true;
}
if (cmd == "check-sat") {
std::vector<std::pair<std::string, int>> stats;
HttpServer::Server->getSatelliteStats(&stats);
std::stringstream out;
bool first = true;
out << "{" << endl;
auto appendStat = [&](const std::string &name, int value) {
out << (!first ? "," : "") << " \"" << name << "\":" << value << endl;
first = false;
};
for (auto i : stats) {
appendStat(i.first, i.second);
}
out << "}" << endl;
transport->sendString(out.str());
return true;
}
if (cmd == "check-sql") {
string stats = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
stats += "<SQL>\n";
#ifdef ENABLE_EXTENSION_MYSQL
stats += MySqlStats::ReportStats();
#endif
stats += "</SQL>\n";
transport->sendString(stats);
return true;
}
return false;
}
bool AdminRequestHandler::handleStatusRequest(const std::string &cmd,
Transport *transport) {
if (cmd == "status.xml") {
return send_status(transport, Writer::Format::XML, "application/xml");
}
if (cmd == "status.json") {
return send_status(transport, Writer::Format::JSON,
"application/json");
}
if (cmd == "status.html" || cmd == "status.htm") {
return send_status(transport, Writer::Format::HTML, "text/html");
}
return false;
}
bool AdminRequestHandler::handleInvalidateUnitRequest(const std::string &cmd,
Transport *transport) {
if (RuntimeOption::RepoAuthoritative) {
transport->sendString("Cannot invalidate units in repo authoritative mode\n", 400);
return true;
}
std::string invalidated;
std::vector<std::string> paths;
// Support GET for convenience, and POST for large requests.
transport->getArrayParam("path", paths, Transport::Method::AUTO);
bool first = true;
for (auto const& path : paths) {
String translatedPath = File::TranslatePathKeepRelative(path);
invalidateUnit(translatedPath.get());
if (first) {
first = false;
} else {
invalidated += " ";
}
invalidated += translatedPath.c_str();
}
auto msg = folly::sformat("Invalidated {} path(s): {}\n",
paths.size(), invalidated);
transport->sendString(msg);
return true;
}
bool AdminRequestHandler::handleMemoryRequest(const std::string &cmd,
Transport *transport){
std::string out;
if (cmd == "memory.xml") {
MemoryStats::ReportMemory(out, Writer::Format::XML);
transport->replaceHeader("Content-Type","application/xml");
transport->sendString(out);
return true;
}
if (cmd == "memory.json") {
MemoryStats::ReportMemory(out, Writer::Format::JSON);
transport->replaceHeader("Content-Type","application/json");
transport->sendString(out);
return true;
}
if (cmd == "memory.html" || cmd == "memory.htm") {
MemoryStats::ReportMemory(out, Writer::Format::HTML);
transport->replaceHeader("Content-Type","text/html");
transport->sendString(out);
return true;
}
return false;
}
bool AdminRequestHandler::handleStatsRequest(const std::string &cmd,
Transport *transport) {
if (cmd == "stats-on") {
RuntimeOption::EnableStats = true;
transport->sendString("OK\n");
return true;
}
if (cmd == "stats-off") {
RuntimeOption::EnableStats = false;
transport->sendString("OK\n");
return true;
}
if (cmd == "stats-clear") {
ServerStats::Clear();
transport->sendString("OK\n");
return true;
}
if (cmd == "stats-web") {
return toggle_switch(transport, RuntimeOption::EnableWebStats);
}
if (cmd == "stats-mem") {
toggle_switch(transport, RuntimeOption::EnableMemoryStats);
return true;
}
if (cmd == "stats-sql") {
return toggle_switch(transport, RuntimeOption::EnableSQLStats);
}
if (cmd == "stats-mutex") {
int sampling = transport->getIntParam("sampling");
if (sampling > 0) {
LockProfiler::s_profile_sampling = sampling;
}
return toggle_switch(transport, LockProfiler::s_profile);
}
if (cmd == "stats.keys") {
transport->sendString(ServerStats::GetKeys());
return true;
}
if (cmd == "stats.kvp") {
return send_report(transport);
}
if (cmd == "stats.xsl") {
string xsl;
if (!RuntimeOption::StatsXSLProxy.empty()) {
StringBuffer response;
if (HttpClient().get(RuntimeOption::StatsXSLProxy.c_str(), response) ==
200) {
xsl = response.data();
if (!xsl.empty()) {
transport->replaceHeader("Content-Type", "application/xml");
transport->sendString(xsl);
return true;
}
}
}
transport->sendString("Not Found\n", 404);
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
// profile commands
bool AdminRequestHandler::handleProfileRequest(const std::string &cmd,
Transport *transport) {
if (cmd == "prof-exe") {
std::map<RequestInfo::Executing, int> counts;
RequestInfo::GetExecutionSamples(counts);
string res = "[ ";
for (std::map<RequestInfo::Executing, int>::const_iterator iter =
counts.begin(); iter != counts.end(); ++iter) {
res += folly::to<string>(iter->first) + ", " +
folly::to<string>(iter->second) + ", ";
}
res += "-1 ]";
transport->sendString(res);
return true;
}
#ifdef GOOGLE_CPU_PROFILER
if (handleCPUProfilerRequest(cmd, transport)) {
return true;
}
#endif
return false;
}
#ifdef GOOGLE_CPU_PROFILER
bool AdminRequestHandler::handleCPUProfilerRequest(const std::string &cmd,
Transport *transport) {
string file = RuntimeOption::ProfilerOutputDir + "/" +
Process::HostName + "/hphp.prof";
if (cmd == "prof-cpu-on") {
if (FileUtil::mkdir(file)) {
ProfilerStart(file.c_str());
transport->sendString("OK\n");
} else {
transport->sendString("Unable to mkdir for profile data.\n");
}
return true;
}
if (cmd == "prof-cpu-off") {
ProfilerStop();
ProfilerFlush();
transport->sendString("OK\n");
return true;
}
return false;
}
#endif
bool AdminRequestHandler::handleStaticStringsRequest(const std::string& cmd,
Transport* transport) {
if (cmd == "static-strings") {
std::ostringstream result;
result << makeStaticStringCount();
transport->sendString(result.str());
return true;
} else if (cmd == "static-strings-rds") {
std::ostringstream result;
result << countStaticStringConstants();
transport->sendString(result.str());
return true;
}
return false;
}
std::string formatStaticString(StringData* str) {
return folly::sformat(
"----\n{} bytes\n{}\n", str->size(), str->toCppString());
}
bool AdminRequestHandler::handleDumpStaticStringsRequest(folly::File& file) {
auto const& list = lookupDefinedStaticStrings();
for (auto item : list) {
auto const line = formatStaticString(item);
folly::writeFull(file.fd(), line.data(), line.size());
if (RuntimeOption::EvalPerfDataMap) {
auto const len = std::min<size_t>(item->size(), 255);
std::string str(item->data(), len);
// Only print the first line (up to 255 characters). Since we want '\0' in
// the list of characters to avoid, we need to use the version of
// `find_first_of()' with explicit length.
auto cutOffPos = str.find_first_of("\r\n", 0, 3);
if (cutOffPos != std::string::npos) str.erase(cutOffPos);
Debug::DebugInfo::recordDataMap(item->mutableData(),
item->mutableData() + item->size(),
"Str-" + str);
}
}
return true;
}
bool AdminRequestHandler::handleRandomStaticStringsRequest(
const std::string& /*cmd*/, Transport* transport) {
size_t count = 1;
auto countParam = transport->getParam("count");
if (countParam != "") {
try {
count = folly::to<size_t>(countParam);
} catch (...) {
// do the default on invalid input
}
}
std::string output;
auto list = lookupDefinedStaticStrings();
if (count < list.size()) {
for (size_t i = 0; i < count; i++) {
size_t j = folly::Random::rand64(i, list.size());
std::swap(list[i], list[j]);
}
list.resize(count);
}
for (auto item : list) {
output += formatStaticString(item);
}
transport->sendString(output);
return true;
}
bool AdminRequestHandler::handleVMRequest(const std::string &cmd,
Transport *transport) {
if (cmd == "vm-tcspace") {
transport->sendString(jit::tc::getTCSpace());
return true;
}
if (cmd == "vm-tcaddr") {
transport->sendString(jit::tc::getTCAddrs());
return true;
}
if (cmd == "vm-namedentities") {
std::ostringstream result;
result << NamedEntity::tableSize();
transport->sendString(result.str());
return true;
}
if (cmd == "vm-dump-tc") {
if (jit::tc::dump()) {
transport->sendString("Done");
} else {
transport->sendString("Error dumping the translation cache");
}
return true;
}
return false;
}
void AdminRequestHandler::handleProxyRequest(const std::string& /*cmd*/,
Transport* transport) {
try {
auto const percentStr = transport->getParam("percentage");
auto const percent = percentStr.empty() ? 0 : folly::to<int>(percentStr);
if (percent < 0 || percent > 100) {
throw std::range_error("must be in [0, 100]");
}
setProxyOriginPercentage(transport->getParam("origin"), percent);
transport->sendString("Origin and percentage updated");
} catch (const std::range_error& re) {
transport->sendString(folly::sformat("Invalid percentage: {}", re.what()));
}
}
///////////////////////////////////////////////////////////////////////////////
// Dump cache content
bool AdminRequestHandler::handleDumpCacheRequest(const std::string &cmd,
Transport *transport) {
if (cmd == "dump-apc") {
if (!apcExtension::Enable) {
transport->sendString("No APC\n");
return true;
}
string keyOnlyParam = transport->getParam("keyonly");
bool keyOnly = false;
if (keyOnlyParam == "true" || keyOnlyParam == "1") {
keyOnly = true;
}
apc_dump("/tmp/apc_dump", keyOnly, false);
transport->sendString("Done");
return true;
}
if (cmd == "dump-apc-prefix") {
if (!apcExtension::Enable) {
transport->sendString("No APC\n");
return true;
}
auto const prefix = transport->getParam("prefix");
if (prefix.empty()) {
transport->sendString("No prefix provided\n");
return true;
}
auto const countStr = transport->getParam("count");
uint32_t count = 1;
try {
count = countStr.empty() ? count : folly::to<uint32_t>(countStr);
} catch (...) {
// use default if invalid count
}
apc_dump_prefix("/tmp/apc_dump_prefix", prefix, count);
transport->sendString("Done");
return true;
}
if (cmd == "dump-apc-info") {
if (!apcExtension::Enable) {
transport->sendString("No APC\n");
return true;
}
transport->sendString(APCStats::getAPCStats().getStatsInfo());
return true;
}
if (cmd == "dump-apc-meta") {
if (!apcExtension::Enable) {
transport->sendString("No APC\n");
return true;
}
apc_dump("/tmp/apc_dump_meta", false, true);
transport->sendString("Done");
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
bool AdminRequestHandler::handleRandomApcRequest(const std::string& /*cmd*/,
Transport* transport) {
std::ostringstream out;
uint32_t keyCount = 1;
std::string count = transport->getParam("count");
if (count != "") {
try {
keyCount = folly::to<int64_t>(count);
} catch (...) {
// set keyCount to 1 if an invalid string is passed
keyCount = 1;
}
}
apc_get_random_entries(out, keyCount);
transport->sendString(out.str());
return true;
}
}