#include "examples/procmon/plot.h" #include "muduo/base/FileUtil.h" #include "muduo/base/Logging.h" #include "muduo/base/ProcessInfo.h" #include "muduo/net/EventLoop.h" #include "muduo/net/http/HttpRequest.h" #include "muduo/net/http/HttpResponse.h" #include "muduo/net/http/HttpServer.h" #include #include #include #include #include #include #include #include #define __STDC_FORMAT_MACROS #include using namespace muduo; using namespace muduo::net; // TODO: // - what if process exits? // // Represent parsed /proc/pid/stat struct StatData { void parse(const char* startAtState, int kbPerPage) { // istringstream is probably not the most efficient way to parse it, // see muduo-protorpc/examples/collect/ProcFs.cc for alternatives. std::istringstream iss(startAtState); // 0 1 2 3 4 5 6 7 8 9 11 13 15 // 3770 (cat) R 3718 3770 3718 34818 3770 4202496 214 0 0 0 0 0 0 0 20 // 16 18 19 20 21 22 23 24 25 // 0 1 0 298215 5750784 81 18446744073709551615 4194304 4242836 140736345340592 // 26 // 140736066274232 140575670169216 0 0 0 0 0 0 0 17 0 0 0 0 0 0 iss >> state; iss >> ppid >> pgrp >> session >> tty_nr >> tpgid >> flags; iss >> minflt >> cminflt >> majflt >> cmajflt; iss >> utime >> stime >> cutime >> cstime; iss >> priority >> nice >> num_threads >> itrealvalue >> starttime; long vsize, rss; iss >> vsize >> rss >> rsslim; vsizeKb = vsize / 1024; rssKb = rss * kbPerPage; } // int pid; char state; int ppid; int pgrp; int session; int tty_nr; int tpgid; int flags; long minflt; long cminflt; long majflt; long cmajflt; long utime; long stime; long cutime; long cstime; long priority; long nice; long num_threads; long itrealvalue; long starttime; long vsizeKb; long rssKb; long rsslim; }; static_assert(std::is_pod::value, "StatData should be POD."); class Procmon : noncopyable { public: Procmon(EventLoop* loop, pid_t pid, uint16_t port, const char* procname) : kClockTicksPerSecond_(muduo::ProcessInfo::clockTicksPerSecond()), kbPerPage_(muduo::ProcessInfo::pageSize() / 1024), kBootTime_(getBootTime()), pid_(pid), server_(loop, InetAddress(port), getName()), procname_(procname ? procname : ProcessInfo::procname(readProcFile("stat")).as_string()), hostname_(ProcessInfo::hostname()), cmdline_(getCmdLine()), ticks_(0), cpu_usage_(600 / kPeriod_), // 10 minutes cpu_chart_(640, 100, 600, kPeriod_), ram_chart_(640, 100, 7200, 30) { { // chdir to the same cwd of the process being monitored. string cwd = readLink("cwd"); if (::chdir(cwd.c_str())) { LOG_SYSERR << "Cannot chdir() to " << cwd; } } { char cwd[1024]; if (::getcwd(cwd, sizeof cwd)) { LOG_INFO << "Current dir: " << cwd; } } memZero(&lastStatData_, sizeof lastStatData_); server_.setHttpCallback(std::bind(&Procmon::onRequest, this, _1, _2)); } void start() { tick(); server_.getLoop()->runEvery(kPeriod_, std::bind(&Procmon::tick, this)); server_.start(); } private: string getName() const { char name[256]; snprintf(name, sizeof name, "procmon-%d", pid_); return name; } void onRequest(const HttpRequest& req, HttpResponse* resp) { resp->setStatusCode(HttpResponse::k200Ok); resp->setStatusMessage("OK"); resp->setContentType("text/plain"); resp->addHeader("Server", "Muduo-Procmon"); /* if (!processExists(pid_)) { resp->setStatusCode(HttpResponse::k404NotFound); resp->setStatusMessage("Not Found"); resp->setCloseConnection(true); return; } */ if (req.path() == "/") { resp->setContentType("text/html"); fillOverview(req.query()); resp->setBody(response_.retrieveAllAsString()); } else if (req.path() == "/cmdline") { resp->setBody(cmdline_); } else if (req.path() == "/cpu.png") { std::vector cpu_usage; for (size_t i = 0; i < cpu_usage_.size(); ++i) cpu_usage.push_back(cpu_usage_[i].cpuUsage(kPeriod_, kClockTicksPerSecond_)); string png = cpu_chart_.plotCpu(cpu_usage); resp->setContentType("image/png"); resp->setBody(png); } // FIXME: replace with a map else if (req.path() == "/environ") { resp->setBody(getEnviron()); } else if (req.path() == "/io") { resp->setBody(readProcFile("io")); } else if (req.path() == "/limits") { resp->setBody(readProcFile("limits")); } else if (req.path() == "/maps") { resp->setBody(readProcFile("maps")); } // numa_maps else if (req.path() == "/smaps") { resp->setBody(readProcFile("smaps")); } else if (req.path() == "/status") { resp->setBody(readProcFile("status")); } else if (req.path() == "/files") { listFiles(); resp->setBody(response_.retrieveAllAsString()); } else if (req.path() == "/threads") { listThreads(); resp->setBody(response_.retrieveAllAsString()); } else { resp->setStatusCode(HttpResponse::k404NotFound); resp->setStatusMessage("Not Found"); resp->setCloseConnection(true); } } void fillOverview(const string& query) { response_.retrieveAll(); Timestamp now = Timestamp::now(); appendResponse("%s on %s\n", procname_.c_str(), hostname_.c_str()); fillRefresh(query); appendResponse("\n"); string stat = readProcFile("stat"); if (stat.empty()) { appendResponse("

PID %d doesn't exist.

", pid_); return; } int pid = atoi(stat.c_str()); assert(pid == pid_); StringPiece procname = ProcessInfo::procname(stat); appendResponse("

%s on %s

\n", procname.as_string().c_str(), hostname_.c_str()); response_.append("

Refresh 1s "); response_.append("2s "); response_.append("5s "); response_.append("15s "); response_.append("60s\n"); response_.append("

Command line\n"); response_.append("Environment variables\n"); response_.append("Threads\n"); appendResponse("

Page generated at %s (UTC)", now.toFormattedString().c_str()); response_.append("

"); StatData statData; // how about use lastStatData_ ? memZero(&statData, sizeof statData); statData.parse(procname.end()+1, kbPerPage_); // end is ')' appendTableRow("PID", pid); Timestamp started(getStartTime(statData.starttime)); // FIXME: cache it; appendTableRow("Started at", started.toFormattedString(false /*showMicroseconds*/) + " (UTC)"); appendTableRowFloat("Uptime (s)", timeDifference(now, started)); // FIXME: format as days+H:M:S appendTableRow("Executable", readLink("exe")); appendTableRow("Current dir", readLink("cwd")); appendTableRow("State", getState(statData.state)); appendTableRowFloat("User time (s)", getSeconds(statData.utime)); appendTableRowFloat("System time (s)", getSeconds(statData.stime)); appendTableRow("VmSize (KiB)", statData.vsizeKb); appendTableRow("VmRSS (KiB)", statData.rssKb); appendTableRow("Threads", statData.num_threads); appendTableRow("CPU usage", ""); appendTableRow("Priority", statData.priority); appendTableRow("Nice", statData.nice); appendTableRow("Minor page faults", statData.minflt); appendTableRow("Major page faults", statData.majflt); // TODO: user response_.append("
"); response_.append(""); } void fillRefresh(const string& query) { size_t p = query.find("refresh="); if (p != string::npos) { int seconds = atoi(query.c_str()+p+8); if (seconds > 0) { appendResponse("\n", seconds); } } } static int dirFilter(const struct dirent* d) { return (d->d_name[0] != '.'); } static char getDirType(char d_type) { switch (d_type) { case DT_REG: return '-'; case DT_DIR: return 'd'; case DT_LNK: return 'l'; default: return '?'; } } void listFiles() { struct dirent** namelist = NULL; int result = ::scandir(".", &namelist, dirFilter, alphasort); for (int i = 0; i < result; ++i) { struct stat stat; if (::lstat(namelist[i]->d_name, &stat) == 0) { Timestamp mtime(stat.st_mtime * Timestamp::kMicroSecondsPerSecond); int64_t size = stat.st_size; appendResponse("%c %9" PRId64 " %s %s", getDirType(namelist[i]->d_type), size, mtime.toFormattedString(/*showMicroseconds=*/false).c_str(), namelist[i]->d_name); if (namelist[i]->d_type == DT_LNK) { char link[1024]; ssize_t len = ::readlink(namelist[i]->d_name, link, sizeof link - 1); if (len > 0) { link[len] = '\0'; appendResponse(" -> %s", link); } } appendResponse("\n"); } ::free(namelist[i]); } ::free(namelist); } void listThreads() { response_.retrieveAll(); // FIXME: implement this } string readProcFile(const char* basename) { char filename[256]; snprintf(filename, sizeof filename, "/proc/%d/%s", pid_, basename); string content; FileUtil::readFile(filename, 1024*1024, &content); return content; } string readLink(const char* basename) { char filename[256]; snprintf(filename, sizeof filename, "/proc/%d/%s", pid_, basename); char link[1024]; ssize_t len = ::readlink(filename, link, sizeof link); string result; if (len > 0) { result.assign(link, len); } return result; } int appendResponse(const char* fmt, ...) __attribute__ ((format (printf, 2, 3))); void appendTableRow(const char* name, long value) { appendResponse("%s%ld\n", name, value); } void appendTableRowFloat(const char* name, double value) { appendResponse("%s%.2f\n", name, value); } void appendTableRow(const char* name, StringArg value) { appendResponse("%s%s\n", name, value.c_str()); } string getCmdLine() { return boost::replace_all_copy(readProcFile("cmdline"), string(1, '\0'), "\n\t"); } string getEnviron() { return boost::replace_all_copy(readProcFile("environ"), string(1, '\0'), "\n"); } Timestamp getStartTime(long starttime) { return Timestamp(Timestamp::kMicroSecondsPerSecond * kBootTime_ + Timestamp::kMicroSecondsPerSecond * starttime / kClockTicksPerSecond_); } double getSeconds(long ticks) { return static_cast(ticks) / kClockTicksPerSecond_; } void tick() { string stat = readProcFile("stat"); // FIXME: catch file descriptor if (stat.empty()) return; StringPiece procname = ProcessInfo::procname(stat); StatData statData; memZero(&statData, sizeof statData); statData.parse(procname.end()+1, kbPerPage_); // end is ')' if (ticks_ > 0) { CpuTime time; time.userTime_ = std::max(0, static_cast(statData.utime - lastStatData_.utime)); time.sysTime_ = std::max(0, static_cast(statData.stime - lastStatData_.stime)); cpu_usage_.push_back(time); } lastStatData_ = statData; ++ticks_; } // // static member functions // static const char* getState(char state) { // One character from the string "RSDZTW" where R is running, S is sleeping in // an interruptible wait, D is waiting in uninterruptible disk sleep, Z is zombie, // T is traced or stopped (on a signal), and W is paging. switch (state) { case 'R': return "Running"; case 'S': return "Sleeping"; case 'D': return "Disk sleep"; case 'Z': return "Zombie"; default: return "Unknown"; } } static long getLong(const string& status, const char* key) { long result = 0; size_t pos = status.find(key); if (pos != string::npos) { result = ::atol(status.c_str() + pos + strlen(key)); } return result; } static long getBootTime() { string stat; FileUtil::readFile("/proc/stat", 65536, &stat); return getLong(stat, "btime "); } struct CpuTime { int userTime_; int sysTime_; double cpuUsage(double kPeriod, double kClockTicksPerSecond) const { return (userTime_ + sysTime_) / (kClockTicksPerSecond * kPeriod); } }; const static int kPeriod_ = 2.0; const int kClockTicksPerSecond_; const int kbPerPage_; const long kBootTime_; // in Unix-time const pid_t pid_; HttpServer server_; const string procname_; const string hostname_; const string cmdline_; int ticks_; StatData lastStatData_; boost::circular_buffer cpu_usage_; Plot cpu_chart_; Plot ram_chart_; // scratch variables Buffer response_; }; // define outline for __attribute__ int Procmon::appendResponse(const char* fmt, ...) { char buf[1024]; va_list args; va_start(args, fmt); int ret = vsnprintf(buf, sizeof buf, fmt, args); va_end(args); response_.append(buf); return ret; } bool processExists(pid_t pid) { char filename[256]; snprintf(filename, sizeof filename, "/proc/%d/stat", pid); return ::access(filename, R_OK) == 0; } int main(int argc, char* argv[]) { if (argc < 3) { printf("Usage: %s pid port [name]\n", argv[0]); return 0; } int pid = atoi(argv[1]); if (!processExists(pid)) { printf("Process %d doesn't exist.\n", pid); return 1; } EventLoop loop; uint16_t port = static_cast(atoi(argv[2])); Procmon procmon(&loop, pid, port, argc > 3 ? argv[3] : NULL); procmon.start(); loop.loop(); }