| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| #include "bin/vmstats_impl.h" |
| |
| #include "bin/file.h" |
| #include "bin/log.h" |
| #include "bin/platform.h" |
| #include "bin/resources.h" |
| #include "bin/socket.h" |
| #include "bin/thread.h" |
| #include "bin/utils.h" |
| #include "include/dart_debugger_api.h" |
| #include "platform/json.h" |
| |
| |
| namespace dart { |
| namespace bin { |
| |
| #define BUFSIZE 8192 |
| #define RETRY_PAUSE 100 // milliseconds |
| |
| static const char* INDEX_HTML = "index.html"; |
| static const char* VMSTATS_HTML = "vmstats.html"; |
| static const char* DEFAULT_HOST = "localhost"; |
| |
| // Global static pointer used to ensure a single instance of the class. |
| VmStats* VmStats::instance_ = NULL; |
| dart::Monitor* VmStats::instance_monitor_; |
| dart::Mutex* VmStatusService::mutex_; |
| |
| |
| void VmStats::Start(int port, const char* root_dir, bool verbose) { |
| if (instance_ != NULL) { |
| FATAL("VmStats already started."); |
| } |
| instance_ = new VmStats(verbose); |
| instance_monitor_ = new dart::Monitor(); |
| Initialize(); |
| VmStatusService::InitOnce(); |
| |
| if (port >= 0) { |
| StartServer(port, root_dir); |
| } |
| } |
| |
| |
| void VmStats::StartServer(int port, const char* root_dir) { |
| ASSERT(port >= 0); |
| ASSERT(instance_ != NULL); |
| ASSERT(instance_monitor_ != NULL); |
| Socket::Initialize(); |
| |
| if (root_dir != NULL) { |
| instance_->root_directory_ = root_dir; |
| } |
| |
| // TODO(tball): allow host to be specified. |
| char* host = const_cast<char*>(DEFAULT_HOST); |
| OSError* os_error; |
| SocketAddresses* addresses = Socket::LookupAddress(host, -1, &os_error); |
| if (addresses == NULL) { |
| Log::PrintErr("Failed IP lookup of VmStats host %s: %s\n", |
| host, os_error->message()); |
| return; |
| } |
| const intptr_t BACKLOG = 128; // Default value from HttpServer.dart |
| int64_t address = ServerSocket::CreateBindListen( |
| addresses->GetAt(0)->addr(), port, BACKLOG); |
| if (address < 0) { |
| Log::PrintErr("Failed binding VmStats socket: %s:%d\n", host, port); |
| return; |
| } |
| instance_->bind_address_ = address; |
| Log::Print("VmStats URL: http://%s:%"Pd"/\n", host, Socket::GetPort(address)); |
| |
| MonitorLocker ml(instance_monitor_); |
| instance_->running_ = true; |
| int err = dart::Thread::Start(WebServer, address); |
| if (err != 0) { |
| Log::PrintErr("Failed starting VmStats thread: %d\n", err); |
| Shutdown(); |
| } |
| } |
| |
| void VmStats::Stop() { |
| ASSERT(instance_ != NULL); |
| MonitorLocker ml(instance_monitor_); |
| instance_->running_ = false; |
| } |
| |
| |
| void VmStats::Shutdown() { |
| ASSERT(instance_ != NULL); |
| MonitorLocker ml(instance_monitor_); |
| Socket::Close(instance_->bind_address_); |
| delete instance_; |
| instance_ = NULL; |
| } |
| |
| |
| void VmStats::AddIsolate(IsolateData* isolate_data, |
| Dart_Isolate isolate) { |
| MonitorLocker ml(instance_monitor_); |
| instance_->isolate_table_[isolate_data] = isolate; |
| } |
| |
| |
| void VmStats::RemoveIsolate(IsolateData* isolate_data) { |
| MonitorLocker ml(instance_monitor_); |
| instance_->isolate_table_.erase(isolate_data); |
| } |
| |
| |
| static const char* ContentType(const char* url) { |
| const char* suffix = strrchr(url, '.'); |
| if (suffix != NULL) { |
| if (!strcmp(suffix, ".html")) { |
| return "text/html; charset=UTF-8"; |
| } |
| if (!strcmp(suffix, ".dart")) { |
| return "application/dart; charset=UTF-8"; |
| } |
| if (!strcmp(suffix, ".js")) { |
| return "application/javascript; charset=UTF-8"; |
| } |
| if (!strcmp(suffix, ".css")) { |
| return "text/css; charset=UTF-8"; |
| } |
| if (!strcmp(suffix, ".gif")) { |
| return "image/gif"; |
| } |
| if (!strcmp(suffix, ".png")) { |
| return "image/png"; |
| } |
| if (!strcmp(suffix, ".jpg") || !strcmp(suffix, ".jpeg")) { |
| return "image/jpeg"; |
| } |
| } |
| return "text/plain"; |
| } |
| |
| |
| // Return a malloc'd string from a format string and arguments. |
| intptr_t alloc_printf(char** result, const char* format, ...) { |
| va_list args; |
| va_start(args, format); |
| intptr_t len = vsnprintf(NULL, 0, format, args) + 1; |
| *result = reinterpret_cast<char*>(malloc(len)); |
| return vsnprintf(*result, len, format, args); |
| } |
| |
| |
| void writeResponse(intptr_t socket, const char* content_type, |
| const char* data, size_t length) { |
| char* header; |
| intptr_t len = alloc_printf(&header, |
| "HTTP/1.1 200 OK\nContent-Type: %s\nContent-Length: %"Pu"\n\n", |
| content_type, length); |
| Socket::Write(socket, header, len); |
| Socket::Write(socket, data, length); |
| Socket::Write(socket, "\n", 1); |
| free(header); |
| } |
| |
| |
| void writeErrorResponse(intptr_t socket, intptr_t error_num, const char* error, |
| const char* description) { |
| if (description != NULL) { |
| // Create body first, so its length is known when creating the header. |
| char* body; |
| intptr_t body_len = alloc_printf(&body, |
| "<html><head><title>%d %s</title></head>\n" |
| "<body>\n<h1>%s</h1>\n%s\n</body></html>\n", |
| error_num, error, error, description); |
| char* header; |
| intptr_t header_len = alloc_printf(&header, |
| "HTTP/1.1 %d %s\n" |
| "Content-Length: %d\n" |
| "Connection: close\n" |
| "Content-Type: text/html\n\n", |
| error_num, error, body_len); |
| Socket::Write(socket, header, header_len); |
| Socket::Write(socket, body, body_len); |
| free(header); |
| free(body); |
| } else { |
| char* response; |
| intptr_t len = |
| alloc_printf(&response, "HTTP/1.1 %d %s\n\n", error_num, error); |
| Socket::Write(socket, response, len); |
| free(response); |
| } |
| } |
| |
| |
| void VmStats::WebServer(uword bind_address) { |
| while (true) { |
| intptr_t socket = ServerSocket::Accept(bind_address); |
| if (socket == ServerSocket::kTemporaryFailure) { |
| // Not a real failure, woke up but no connection available. |
| |
| // Use MonitorLocker.Wait(), since it has finer granularity than sleep(). |
| dart::Monitor m; |
| MonitorLocker ml(&m); |
| ml.Wait(RETRY_PAUSE); |
| |
| continue; |
| } |
| if (socket < 0) { |
| // Stop() closed the socket. |
| return; |
| } |
| Socket::SetBlocking(socket); |
| |
| // TODO(tball): rewrite this to use STL, so as to eliminate the static |
| // buffer and support resource URLs that are longer than BUFSIZE. |
| |
| // Read request. |
| char buffer[BUFSIZE + 1]; |
| intptr_t len = Socket::Read(socket, buffer, BUFSIZE); |
| if (len <= 0) { |
| // Invalid HTTP request, ignore. |
| continue; |
| } |
| buffer[len] = '\0'; |
| |
| // Verify it's a GET request. |
| // TODO(tball): support POST requests. |
| if (strncmp("GET ", buffer, 4) != 0 && strncmp("get ", buffer, 4) != 0) { |
| Log::PrintErr("Unsupported HTTP request type"); |
| writeErrorResponse(socket, 403, "Forbidden", |
| "Unsupported HTTP request type"); |
| Socket::Close(socket); |
| continue; |
| } |
| |
| // Extract GET URL, and null-terminate URL in case request line has |
| // HTTP version. |
| for (int i = 4; i < len; i++) { |
| if (buffer[i] == ' ') { |
| buffer[i] = '\0'; |
| } |
| } |
| char* url = strdup(&buffer[4]); |
| |
| if (instance_->verbose_) { |
| Log::Print("vmstats: %s requested\n", url); |
| } |
| char* content = NULL; |
| |
| // Check for VmStats-specific URLs. |
| if (strcmp(url, "/isolates") == 0) { |
| content = instance_->IsolatesStatus(); |
| } else { |
| // Check plug-ins. |
| content = VmStatusService::GetVmStatus(url); |
| } |
| |
| if (content != NULL) { |
| writeResponse(socket, "application/json", content, strlen(content)); |
| free(content); |
| } else { |
| // No status content with this URL, return file or resource content. |
| dart::TextBuffer path(strlen(instance_->root_directory_) + strlen(url)); |
| path.AddString(instance_->root_directory_); |
| path.AddString(url); |
| |
| // Expand directory URLs. |
| if (strcmp(url, "/") == 0) { |
| path.AddString(VMSTATS_HTML); |
| } else if (url[strlen(url) - 1] == '/') { |
| path.AddString(INDEX_HTML); |
| } |
| |
| bool success = false; |
| char* text_buffer = NULL; |
| const char* content_type = ContentType(path.buf()); |
| if (File::Exists(path.buf())) { |
| File* f = File::Open(path.buf(), File::kRead); |
| if (f != NULL) { |
| intptr_t len = f->Length(); |
| text_buffer = reinterpret_cast<char*>(malloc(len)); |
| if (f->ReadFully(text_buffer, len)) { |
| writeResponse(socket, content_type, text_buffer, len); |
| success = true; |
| } |
| free(text_buffer); |
| delete f; |
| } |
| } else { |
| const char* resource; |
| intptr_t len = Resources::ResourceLookup(path.buf(), &resource); |
| if (len != Resources::kNoSuchInstance) { |
| ASSERT(len >= 0); |
| writeResponse(socket, content_type, resource, len); |
| success = true; |
| } |
| } |
| if (!success) { |
| char* description; |
| alloc_printf( |
| &description, "URL <a href=\"%s\">%s</a> not found.", url, url); |
| writeErrorResponse(socket, 404, "Not Found", description); |
| free(description); |
| } |
| } |
| Socket::Close(socket); |
| free(url); |
| } |
| |
| Shutdown(); |
| } |
| |
| |
| char* VmStats::IsolatesStatus() { |
| dart::TextBuffer text(64); |
| text.Printf("{\n\"isolates\": [\n"); |
| IsolateTable::iterator itr; |
| bool first = true; |
| for (itr = isolate_table_.begin(); itr != isolate_table_.end(); ++itr) { |
| Dart_Isolate isolate = itr->second; |
| static char request[512]; |
| snprintf(request, sizeof(request), |
| "/isolate/0x%"Px, |
| reinterpret_cast<intptr_t>(isolate)); |
| char* status = VmStatusService::GetVmStatus(request); |
| if (status != NULL) { |
| if (!first) { |
| text.AddString(",\n"); |
| } |
| text.AddString(status); |
| first = false; |
| free(status); |
| } |
| } |
| text.AddString("\n]\n}\n"); |
| return strdup(text.buf()); |
| } |
| |
| |
| // Advance the scanner to the value token of a specified name-value pair. |
| void SeekNamedValue(const char* name, dart::JSONScanner* scanner) { |
| while (!scanner->EOM()) { |
| scanner->Scan(); |
| if (scanner->IsStringLiteral(name)) { |
| scanner->Scan(); |
| ASSERT(scanner->CurrentToken() == dart::JSONScanner::TokenColon); |
| scanner->Scan(); |
| return; |
| } |
| } |
| } |
| |
| |
| // Windows doesn't have strndup(), so this is a simple, private version. |
| static char* StrNDup(const char* s, uword len) { |
| if (strlen(s) < len) { |
| len = strlen(s); |
| } |
| char* result = reinterpret_cast<char*>(malloc(len + 1)); |
| memmove(result, s, len); |
| result[len + 1] = '\0'; |
| return result; |
| } |
| |
| |
| void VmStats::DumpStackThread(uword unused) { |
| Log::Print("Isolate dump:\n"); |
| IsolateTable::iterator itr; |
| MonitorLocker ml(instance_monitor_); |
| for (itr = instance_->isolate_table_.begin(); |
| itr != instance_->isolate_table_.end(); ++itr) { |
| Dart_Isolate isolate = itr->second; |
| |
| // Print isolate name and details. |
| static char buffer[512]; |
| snprintf(buffer, sizeof(buffer), |
| "/isolate/0x%"Px, reinterpret_cast<intptr_t>(isolate)); |
| char* isolate_details = VmStatusService::GetVmStatus(buffer); |
| if (isolate_details != NULL) { |
| dart::JSONScanner scanner(isolate_details); |
| SeekNamedValue("name", &scanner); |
| char* name = StrNDup(scanner.TokenChars(), scanner.TokenLen()); |
| SeekNamedValue("port", &scanner); |
| char* port = StrNDup(scanner.TokenChars(), scanner.TokenLen()); |
| Log::Print("\"%s\" port=%s\n", name, port); |
| free(isolate_details); |
| free(port); |
| free(name); |
| } |
| |
| // Print stack trace. |
| snprintf(buffer, sizeof(buffer), |
| "/isolate/0x%"Px"/stacktrace", |
| reinterpret_cast<intptr_t>(isolate)); |
| char* trace = VmStatusService::GetVmStatus(buffer); |
| if (trace != NULL) { |
| dart::JSONScanner scanner(trace); |
| while (true) { |
| SeekNamedValue("url", &scanner); |
| if (scanner.CurrentToken() == dart::JSONScanner::TokenEOM) { |
| break; |
| } |
| char* url = StrNDup(scanner.TokenChars(), scanner.TokenLen()); |
| SeekNamedValue("line", &scanner); |
| char* line = StrNDup(scanner.TokenChars(), scanner.TokenLen()); |
| SeekNamedValue("function", &scanner); |
| char* function = StrNDup(scanner.TokenChars(), scanner.TokenLen()); |
| Log::Print(" at %s(%s:%s)\n", function, url, line); |
| free(url); |
| free(line); |
| free(function); |
| } |
| free(trace); |
| } |
| } |
| } |
| |
| |
| void VmStats::DumpStack() { |
| int err = dart::Thread::Start(DumpStackThread, 0); |
| if (err != 0) { |
| Log::PrintErr("Failed starting VmStats stackdump thread: %d\n", err); |
| Shutdown(); |
| } |
| } |
| |
| |
| // Global static pointer used to ensure a single instance of the class. |
| VmStatusService* VmStatusService::instance_ = NULL; |
| |
| |
| void VmStatusService::InitOnce() { |
| ASSERT(VmStatusService::instance_ == NULL); |
| VmStatusService::instance_ = new VmStatusService(); |
| VmStatusService::mutex_ = new dart::Mutex(); |
| |
| // Register built-in status plug-ins. RegisterPlugin is not used because |
| // this isn't called within an isolate, and because parameter checking |
| // isn't necessary. |
| instance_->RegisterPlugin(&Dart_GetVmStatus); |
| |
| // TODO(tball): dynamically load any additional plug-ins. |
| } |
| |
| |
| int VmStatusService::RegisterPlugin(Dart_VmStatusCallback callback) { |
| ASSERT(VmStatusService::instance_ != NULL); |
| ASSERT(VmStatusService::mutex_ != NULL); |
| MutexLocker ml(mutex_); |
| if (callback == NULL) { |
| return -1; |
| } |
| VmStatusPlugin* plugin = new VmStatusPlugin(callback); |
| VmStatusPlugin* list = instance_->registered_plugin_list_; |
| if (list == NULL) { |
| instance_->registered_plugin_list_ = plugin; |
| } else { |
| list->Append(plugin); |
| } |
| return 0; |
| } |
| |
| |
| char* VmStatusService::GetVmStatus(const char* request) { |
| ASSERT(VmStatusService::instance_ != NULL); |
| VmStatusPlugin* plugin = instance_->registered_plugin_list_; |
| while (plugin != NULL) { |
| char* result = (plugin->callback())(request); |
| if (result != NULL) { |
| return result; |
| } |
| plugin = plugin->next(); |
| } |
| return NULL; |
| } |
| |
| } // namespace bin |
| } // namespace dart |