/** * File: webService.cpp * * Author: richard; adapted for Victor by Paul Terry 01/08/2018 * Created: 4/17/2017 * * Description: Provides interface to civetweb, an embedded web server * * * Copyright: Anki, Inc. 2017-2018 * **/ #include "webService.h" #if !defined(ANKI_NO_WEBSERVER_ENABLED) #define ANKI_NO_WEBSERVER_ENABLED 0 #endif #if !ANKI_NO_WEBSERVER_ENABLED #if USE_DAS #include "DAS/DAS.h" #endif #include "coretech/common/engine/utils/data/dataPlatform.h" #include "util/logging/logging.h" #include "util/console/consoleSystem.h" #include "util/console/consoleChannel.h" #include "util/cpuProfiler/cpuProfiler.h" #include "util/dispatchQueue/dispatchQueue.h" #include "util/global/globalDefinitions.h" #include "util/helpers/ankiDefines.h" #include "util/helpers/templateHelpers.h" #include "util/string/stringUtils.h" #include "osState/osState.h" #if defined(ANKI_PLATFORM_ANDROID) #include #endif #include #include #include #include #include #include #define LOG_CHANNEL "WebService" using namespace Anki::Vector; namespace { #ifndef SIMULATOR bool s_WaitingForProcessStatus = false; std::vector s_ProcessStatuses; std::mutex s_ProcessStatusMutex; std::condition_variable s_ProcessStatusCondition; #endif std::atomic_bool s_ShuttingDown{false}; } // Used websockets codes, see websocket RFC pg 29 // http://tools.ietf.org/html/rfc6455#section-5.2 enum { WebSocketsTypeText = 0x1, WebSocketsTypeCloseConnection = 0x8 }; // 256KB to accommodate output of animation names static const size_t kBigBufferSize = 256*1024; class ExternalOnlyConsoleChannel : public Anki::Util::IConsoleChannel { public: ExternalOnlyConsoleChannel(char* outText, uint32_t outTextLength) : _outText(outText) , _outTextLength(outTextLength) , _outTextPos(0) { assert((_outText!=nullptr) && (_outTextLength > 0)); _tempBuffer = new char[kTempBufferSize]; } virtual ~ExternalOnlyConsoleChannel() { if (_outText != nullptr) { // insure out buffer is null terminated if (_outTextPos < _outTextLength) { _outText[_outTextPos] = 0; } else { _outText[_outTextLength - 1] = 0; } } Anki::Util::SafeDeleteArray( _tempBuffer ); } bool IsOpen() override { return true; } int WriteData(uint8_t *buffer, int len) override { assert(0); // not implemented (and doesn't seem to ever be called?) return len; } int WriteLogv(const char *format, va_list args) override { // Print to a temporary buffer first so we can use that for any required logs const int printRetVal = vsnprintf(_tempBuffer, kTempBufferSize, format, args); if (printRetVal > 0) { if ((_outText != nullptr) && (_outTextLength > _outTextPos)) { const uint32_t remainingRoom = _outTextLength - _outTextPos; // new line is implicit in all log calls const int outPrintRetVal = snprintf(&_outText[_outTextPos], remainingRoom, "%s\n", _tempBuffer); // note outPrintRetVal can be >remainingRoom (snprintf returns size required, not size used) // so this can make _outTextPos > _outTextLength, but this is ok as we only use it when < _outTextLength _outTextPos += (outPrintRetVal > 0) ? outPrintRetVal : 0; } } return printRetVal; } int WriteLog(const char *format, ...) override { va_list ap; va_start(ap, format); int result = WriteLogv(format, ap); va_end(ap); return result; } bool Flush() override { // already flushed return true; } void SetTTYLoggingEnabled(bool newVal) override { } bool IsTTYLoggingEnabled() const override { return true; } const char* GetChannelName() const override { return nullptr; } void SetChannelName(const char* newName) override {} private: static const size_t kTempBufferSize = kBigBufferSize; char* _tempBuffer; char* _outText; uint32_t _outTextLength; uint32_t _outTextPos; }; static int LogMessage(const struct mg_connection *conn, const char *message) { LOG_INFO("WebService.LogMessage", "%s", message); return 1; } static int LogHandler(struct mg_connection *conn, void *cbdata) { #if USE_DAS // Stop rolling over logs so they are viewable // (otherwise, they get uploaded and then deleted pretty quickly) DASDisableNetwork(DASDisableNetworkReason_LogRollover); #endif // pretend we didn't handle it and pass onto the default handler return 0; } void ExecCommand(const std::vector& args) { LOG_INFO("WebService.ExecCommand", "Called with cmd: %s (and %i arguments)", args[0].c_str(), (int)(args.size() - 1)); pid_t pID = fork(); if (pID == 0) // child { char* argv_child[args.size() + 1]; for (size_t i = 0; i < args.size(); i++) { argv_child[i] = (char *) malloc(args[i].size() + 1); strcpy(argv_child[i], args[i].c_str()); } argv_child[args.size()] = nullptr; execv(argv_child[0], argv_child); // We'll only get here if execv fails for (size_t i = 0 ; i < args.size() + 1 ; ++i) { free(argv_child[i]); } exit(0); } else if (pID < 0) // fail { LOG_INFO("Webservice.ExecCommand.FailedFork", "Failed fork!"); } else // parent { // We don't wait for child to complete or do anything special } } static int ProcessRequest(struct mg_connection *conn, WebService::WebService::RequestType requestType, const std::string& param1, const std::string& param2, const std::string& param3 = "", bool waitAndSendResponse = true, WebService::WebService::ExternalCallback extCallback = nullptr, void* cbdata = nullptr) { if (s_ShuttingDown) { return 1; } WebService::WebService::Request* requestPtr = new WebService::WebService::Request(requestType, param1, param2, param3, extCallback, cbdata); struct mg_context *ctx = mg_get_context(conn); WebService::WebService* that = static_cast(mg_get_user_data(ctx)); that->AddRequest(requestPtr); if (waitAndSendResponse) { // Wait until the main thread processes the request using namespace std::chrono; static const long long kTimeoutDuration_s = 10; { std::unique_lock lk{requestPtr->_readyMutex}; requestPtr->_readyCondition.wait_for(lk, seconds(kTimeoutDuration_s), [requestPtr]{ return requestPtr->_resultReady; }); } if (!requestPtr->_resultReady) { std::lock_guard lock(that->_requestMutex); requestPtr->_result = "Timed out after " + std::to_string(kTimeoutDuration_s) + " seconds"; } mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); mg_printf(conn, "%s\n", requestPtr->_result.c_str()); // Now mark the request as done so the main thread can delete it. // if you pass !waitAndSendResponse, you need to manually set this flag requestPtr->_done = true; } return 1; } static int ConsoleVarsUI(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string category = ((info->query_string) ? info->query_string : ""); std::string standalone = "standalone"; if (category == "embedded") { category = ""; standalone = ""; } const int returnCode = ProcessRequest(conn, WebService::WebService::RequestType::RT_ConsoleVarsUI, category, standalone); return returnCode; } static int ConsoleVarSet(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string query; if (info->content_length > 0) { char buf[info->content_length+1]; mg_read(conn, buf, sizeof(buf)); buf[info->content_length] = 0; query = buf; } else if (info->query_string) { query = info->query_string; } const int returnCode = ProcessRequest(conn, WebService::WebService::RequestType::RT_ConsoleVarSet, query, ""); return returnCode; } static int ConsoleVarGet(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string key; if (info->query_string) { if (!strncmp(info->query_string, "key=", 4)) { key = std::string(info->query_string+4); } } const int returnCode = ProcessRequest(conn, WebService::WebService::RequestType::RT_ConsoleVarGet, key, ""); return returnCode; } static int ConsoleVarList(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string key; if (info->query_string) { if (!strncmp(info->query_string, "key=", 4)) { key = std::string(info->query_string + 4); } } const int returnCode = ProcessRequest(conn, WebService::WebService::RequestType::RT_ConsoleVarList, key, ""); return returnCode; } static int ConsoleFuncList(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string key; if (info->query_string) { if (!strncmp(info->query_string, "key=", 4)) { key = std::string(info->query_string + 4); } } const int returnCode = ProcessRequest(conn, WebService::WebService::RequestType::RT_ConsoleFuncList, key, ""); return returnCode; } static int ConsoleFuncCall(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string func; std::string args; if (info->content_length > 0) { char buf[info->content_length+1]; mg_read(conn, buf, sizeof(buf)); buf[info->content_length] = 0; func = buf; } else if (info->query_string) { func = info->query_string; } int returnCode = 1; if (func.substr(0,5) == "func=") { size_t amp = func.find('&'); if (amp == std::string::npos) { func = func.substr(5); } else { args = func.substr(amp+6); // skip over "args=" func = func.substr(5, amp-5); // unescape '+' => ' ', except if it follows a '\', in which case keep the '+' and drop the '\' size_t plus = args.find("+"); while (plus != std::string::npos) { if( (plus > 0) && (args[plus-1] == '\\') ) { args.erase(plus-1, 1); --plus; } else { args = args.replace(plus, 1, " "); } plus = args.find("+", plus+1); } // unescape '\"' => '"' size_t quote = args.find("\\\""); while (quote != std::string::npos) { args = args.replace(quote, 2, "\""); quote = args.find("\\\"", quote+2); } } returnCode = ProcessRequest(conn, WebService::WebService::RequestType::RT_ConsoleFuncCall, func, args); } return returnCode; } static int ProcessRequestFromQueryString(struct mg_connection *conn, void *cbdata, WebService::WebService::RequestType type) { const mg_request_info* info = mg_get_request_info(conn); std::string request; if (info->content_length > 0) { char buf[info->content_length+1]; mg_read(conn, buf, sizeof(buf)); buf[info->content_length] = 0; request = buf; } else if (info->query_string) { request = info->query_string; } int returnCode = ProcessRequest(conn, type, request, ""); return returnCode; } static int TempAppToEngine(struct mg_connection *conn, void *cbdata) { return ProcessRequestFromQueryString( conn, cbdata, WebService::WebService::RequestType::RT_TempAppToEngine ); } static int TempEngineToApp(struct mg_connection *conn, void *cbdata) { return ProcessRequestFromQueryString( conn, cbdata, WebService::WebService::RequestType::RT_TempEngineToApp ); } WebService::WebService::Request::Request(RequestType rt, const std::string& param1, const std::string& param2, const std::string& param3, ExternalCallback extCallback, void* cbdata) { _requestType = rt; _param1 = param1; _param2 = param2; _param3 = param3; _externalCallback = extCallback; _cbdata = cbdata, _result = ""; _resultReady = false; _done = false; } WebService::WebService::Request::Request(RequestType rt, const std::string& param1, const std::string& param2) : Request(rt, param1, param2, "", nullptr, nullptr) { } static int dasinfo(struct mg_connection *conn, void *cbdata) { mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); // NOTE: For some reason, android builds of the webserver are not getting USE_DAS defined properly #if USE_DAS std::string dasString = "DAS: " + std::string(DASGetLogDir()) + " DASDisableNetworkReason:"; int disabled = DASGetNetworkingDisabled(); if (disabled & DASDisableNetworkReason_Simulator) { dasString += " Simulator"; } if (disabled & DASDisableNetworkReason_UserOptOut) { dasString += " UserOptOut"; } if (disabled & DASDisableNetworkReason_Shutdown) { dasString += " Shutdown"; } if (disabled & DASDisableNetworkReason_LogRollover) { dasString += " LogRollover"; } // if (disabled & DASDisableNetworkReason_Debug) { // dasString += " Debug"; // } #else std::string dasString = "DAS: #undefined for this platform"; #endif mg_printf(conn, "%s", dasString.c_str()); return 1; } static int GetInitialConfig(struct mg_connection *conn, void *cbdata) { mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); struct mg_context *ctx = mg_get_context(conn); WebService::WebService* that = static_cast(mg_get_user_data(ctx)); const std::string& title0 = that->GetConfig()["title0"].asString(); const std::string& title1 = that->GetConfig()["title1"].asString(); const std::string& startPage = that->GetConfig()["startPage"].asString(); #ifdef SIMULATOR const std::string& webotsSim = "true"; #else const std::string& webotsSim = "false"; #endif const std::string& allowPerfPage = that->GetConfig()["allowPerfPage"].asString(); const std::string& whichWebServer = std::to_string(that->GetConfig()["whichWebServer"].asInt()); const std::string& allowConsoleVarsPage = that->GetConfig()["allowConsoleVarsPage"].asString(); const std::string& allowPerfMetricPage = that->GetConfig()["allowPerfMetricPage"].asString(); const int tickBudget_ms = that->GetConfig()["tickBudget_ms"].asInt(); mg_printf(conn, "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%d\n", title0.c_str(), title1.c_str(), startPage.c_str(), webotsSim.c_str(), allowPerfPage.c_str(), whichWebServer.c_str(), allowConsoleVarsPage.c_str(), allowPerfMetricPage.c_str(), tickBudget_ms); return 1; } static int GetMainRobotInfo(struct mg_connection *conn, void *cbdata) { mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); const auto& osState = OSState::getInstance(); const std::string serialNo = osState->GetSerialNumberAsString(); const std::string ip = osState->GetIPAddress(); const std::string robotName = osState->GetRobotName(); const std::string osBuildVersion = osState->GetOSBuildVersion(); const std::string sha = osState->GetBuildSha(); const std::string MACAddress = osState->GetMACAddress(); const std::string SSID = osState->GetSSID(); const std::string buildConfig = #if defined(NDEBUG) "RELEASE"; #else "DEBUG"; #endif #ifdef SIMULATOR const std::string procVersion = "n/a (webots)"; const std::string procCmdLine = "n/a (webots)"; #else // This is a one-time read of info that won't change during the run, // so we don't keep any file streams open std::ifstream fs; fs.open("/proc/version", std::ifstream::in); std::string procVersion; std::getline(fs, procVersion); fs.close(); fs.open("/proc/cmdline", std::ifstream::in); std::string procCmdLine; std::getline(fs, procCmdLine); fs.close(); #endif mg_printf(conn, "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", serialNo.c_str(), ip.c_str(), buildConfig.c_str(), procVersion.c_str(), procCmdLine.c_str(), robotName.c_str(), osBuildVersion.c_str(), sha.c_str(), MACAddress.c_str(), SSID.c_str()); return 1; } static int GetPerfStats(struct mg_connection *conn, void *cbdata) { if (s_ShuttingDown) { return 1; } using namespace std::chrono; const auto startTime = steady_clock::now(); enum { kStat_CpuFreq, kStat_Temperature, kStat_Uptime, kStat_IdleTime, kStat_RealTimeClock, kStat_MemoryInfo1, kStat_MemoryInfo2, kStat_OverallCpu, kStat_Cpu0, kStat_Cpu1, kStat_Cpu2, kStat_Cpu3, kStat_UserDiskSpace, kNumStats }; bool active[kNumStats]; const mg_request_info* info = mg_get_request_info(conn); const std::string boolsString = info->query_string ? info->query_string : ""; int i = 0; for ( ; i < kNumStats && boolsString[i]; i++) { active[i] = (boolsString[i] == '1'); } // If the string wasn't long enough, make the rest of the flags false for ( ; i < kNumStats; i++) { active[i] = false; } const auto& osState = OSState::getInstance(); std::string stat_cpuFreq; if (active[kStat_CpuFreq]) { // Update cpu freq const uint32_t cpuFreq_kHz = osState->GetCPUFreq_kHz(); stat_cpuFreq = std::to_string(cpuFreq_kHz); } std::string stat_temperature; if (active[kStat_Temperature]) { // Update temperature reading (Celsius) const uint32_t cpuTemp_C = osState->GetTemperature_C(); stat_temperature = std::to_string(cpuTemp_C); } std::string stat_uptime; std::string stat_idleTime; if (active[kStat_Uptime] || active[kStat_IdleTime]) { // Up time/idle time float idleTime = 0.0f; const float uptime = osState->GetUptimeAndIdleTime(idleTime); std::stringstream ss; ss << std::fixed << std::setprecision(2) << uptime; stat_uptime = ss.str(); std::stringstream ss2; ss2 << std::fixed << std::setprecision(2) << idleTime; stat_idleTime = ss2.str(); } std::string stat_rtc; if (active[kStat_RealTimeClock]) { // Date/time on robot const auto now = system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; ss << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %X"); stat_rtc = ss.str(); } std::string stat_mem1; std::string stat_mem2; if (active[kStat_MemoryInfo1] || active[kStat_MemoryInfo2]) { OSState::MemoryInfo info; osState->GetMemoryInfo(info); if (active[kStat_MemoryInfo1]) { // Memory use 1 stat_mem1 = std::to_string(info.totalMem_kB) + "," + std::to_string(info.freeMem_kB); } if (active[kStat_MemoryInfo2]) { // Memory use 2 stat_mem2 = std::to_string(info.totalMem_kB) + "," + std::to_string(info.availMem_kB); } } std::vector stat_cpuStat; if (active[kStat_OverallCpu] || active[kStat_Cpu0] || active[kStat_Cpu1] || active[kStat_Cpu2] || active[kStat_Cpu3]) { // CPU time stats osState->GetCPUTimeStats(stat_cpuStat); } static constexpr size_t kNumCPUTimeStats = 5; if (stat_cpuStat.size() < kNumCPUTimeStats) { stat_cpuStat.resize(kNumCPUTimeStats); } std::string stat_userDiskSpace; if (active[kStat_UserDiskSpace]) { #ifdef ANKI_PLATFORM_VICOS OSState::DiskInfo info; const bool success = osState->GetDiskInfo("/data", info); if (success) { stat_userDiskSpace = std::to_string(info.total_kB) + "," + std::to_string(info.avail_kB); } else { stat_userDiskSpace = "1,0"; } #else stat_userDiskSpace = "1,0"; // Not really applicable to webots #endif } const auto now = steady_clock::now(); const auto elapsed_us = duration_cast(now - startTime).count(); LOG_INFO("WebService.Perf", "GetPerfStats took %lld microseconds to read", elapsed_us); mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); mg_printf(conn, "%s\n%s\n%s\n%s\n%s\n%s\n%s\n", stat_cpuFreq.c_str(), stat_temperature.c_str(), stat_uptime.c_str(), stat_idleTime.c_str(), stat_rtc.c_str(), stat_mem1.c_str(), stat_mem2.c_str()); mg_printf(conn, "%s\n%s\n%s\n%s\n%s\n%s\n", stat_cpuStat[0].c_str(), stat_cpuStat[1].c_str(), stat_cpuStat[2].c_str(), stat_cpuStat[3].c_str(), stat_cpuStat[4].c_str(), stat_userDiskSpace.c_str()); return 1; } #ifndef SIMULATOR static int SystemCtl(struct mg_connection *conn, void *cbdata) { using namespace std::chrono; const auto startTime = steady_clock::now(); const mg_request_info* info = mg_get_request_info(conn); const std::string query = info->query_string ? info->query_string : ""; if (query.substr(0, 5) == "proc=") { const size_t amp = query.find('&'); if (amp != std::string::npos) { const std::string action = query.substr(amp + 1); const std::string procName = query.substr(5, amp - 5); std::vector args; args.push_back("/bin/systemctl"); args.push_back(action); args.push_back(procName); ExecCommand(args); const auto now = steady_clock::now(); const auto elapsed_us = duration_cast(now - startTime).count(); LOG_INFO("WebService.Systemctl", "SystemCtl took %lld microseconds", elapsed_us); } } mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); mg_printf(conn, "\n"); return 1; } static int GetProcessStatus(struct mg_connection *conn, void *cbdata) { if (s_ShuttingDown) { return 1; } std::string resultsString; using namespace std::chrono; const auto startTime = steady_clock::now(); const mg_request_info* info = mg_get_request_info(conn); const std::string query = info->query_string ? info->query_string : ""; if (query.substr(0, 5) == "proc=") { struct mg_context* ctx = mg_get_context(conn); Anki::Vector::WebService::WebService* that = static_cast(mg_get_user_data(ctx)); std::string remainder = query.substr(5); std::vector args; args.push_back("/bin/sh"); args.push_back("/anki/bin/vic-getprocessstatus.sh"); args.push_back(that->GetConfig()["port"].asString()); // Loop and pull out all requested process names, separated by ampersands while (!remainder.empty()) { const size_t amp = remainder.find('&'); if (amp != std::string::npos) { args.push_back(remainder.substr(0, amp)); remainder = remainder.substr(amp + 1); } else { args.push_back(remainder); break; } } ExecCommand(args); static const long long kTimeoutDuration_s = 10; { std::unique_lock lk{s_ProcessStatusMutex}; s_WaitingForProcessStatus = true; s_ProcessStatusCondition.wait_for(lk, seconds(kTimeoutDuration_s), []{ return !s_WaitingForProcessStatus; }); if (s_WaitingForProcessStatus) { LOG_INFO("WebService.GetProcessStatus", "GetProcessStatus timed out after %lld seconds", kTimeoutDuration_s); } bool firstDone = false; for (const auto& result : s_ProcessStatuses) { if (firstDone) { resultsString += "\n"; } resultsString += result; firstDone = true; } } } mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); mg_printf(conn, "%s", resultsString.c_str()); const auto now = steady_clock::now(); const auto elapsed_us = duration_cast(now - startTime).count(); LOG_INFO("WebService.GetProcessStatus.Time", "GetProcessStatus took %lld microseconds", elapsed_us); return 1; } static int ProcessStatus(struct mg_connection *conn, void *cbdata) { const mg_request_info* info = mg_get_request_info(conn); std::string results = info->query_string ? info->query_string : ""; { std::unique_lock lk{s_ProcessStatusMutex}; s_ProcessStatuses.clear(); while (!results.empty()) { const size_t amp = results.find('&'); if (amp != std::string::npos) { s_ProcessStatuses.push_back(results.substr(0, amp)); results = results.substr(amp + 1); } else { s_ProcessStatuses.push_back(results); break; } } // Notify the requesting thread that the result is now ready s_WaitingForProcessStatus = false; } s_ProcessStatusCondition.notify_all(); mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: " "close\r\n\r\n"); mg_printf(conn, "\n"); return 1; } #endif // #ifndef SIMULATOR namespace Anki { namespace Vector { namespace WebService { WebService::WebService() : _ctx(nullptr) { } WebService::~WebService() { Stop(); } void WebService::Start(Anki::Util::Data::DataPlatform* platform, const Json::Value& config) { if (platform == nullptr) { return; } if (_ctx != nullptr) { return; } _config = config; _platform = platform; const std::string portNumString = _config["port"].asString(); const std::string webserverPath = platform->pathToResource(Util::Data::Scope::Resources, "webserver"); std::string rewrite; rewrite += "/persistent=" + platform->pathToResource(Util::Data::Scope::Persistent, ""); rewrite += ","; rewrite += "/resources=" + platform->pathToResource(Util::Data::Scope::Resources, ""); rewrite += ","; rewrite += "/cache=" + platform->pathToResource(Util::Data::Scope::Cache, ""); rewrite += ","; rewrite += "/currentgamelog=" + platform->pathToResource(Util::Data::Scope::CurrentGameLog, ""); #if USE_DAS rewrite += ","; rewrite += "/daslog=" + std::string(DASGetLogDir()); #endif //https://ankiinc.atlassian.net/browse/VIC-1554 // const std::string& passwordFile = platform->pathToResource(Util::Data::Scope::Resources, "webserver/htpasswd"); const char *options[] = { "document_root", webserverPath.c_str(), "listening_ports", portNumString.c_str(), "num_threads", "4", "url_rewrite_patterns", rewrite.c_str(), "static_file_max_age", "0", //https://ankiinc.atlassian.net/browse/VIC-1554 // "put_delete_auth_file", // passwordFile.c_str(), // "authentication_domain", // "vector.anki.com", "websocket_timeout_ms", "3600000", // 1 hour //https://ankiinc.atlassian.net/browse/VIC-1554 //#if defined(NDEBUG) // "global_auth_file", // passwordFile.c_str(), //#endif 0 }; struct mg_callbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.log_message = LogMessage; _ctx = mg_start(&callbacks, this, options); mg_set_websocket_handler(_ctx, "/socket", HandleWebSocketsConnect, HandleWebSocketsReady, HandleWebSocketsData, HandleWebSocketsClose, 0); mg_set_request_handler(_ctx, "/daslog", LogHandler, 0); mg_set_request_handler(_ctx, "/consolevars", ConsoleVarsUI, 0); mg_set_request_handler(_ctx, "/consolevarset", ConsoleVarSet, 0); mg_set_request_handler(_ctx, "/consolevarget", ConsoleVarGet, 0); mg_set_request_handler(_ctx, "/consolevarlist", ConsoleVarList, 0); mg_set_request_handler(_ctx, "/consolefunclist", ConsoleFuncList, 0); mg_set_request_handler(_ctx, "/consolefunccall", ConsoleFuncCall, 0); mg_set_request_handler(_ctx, "/dasinfo", dasinfo, 0); mg_set_request_handler(_ctx, "/getinitialconfig", GetInitialConfig, 0); mg_set_request_handler(_ctx, "/getmainrobotinfo", GetMainRobotInfo, 0); mg_set_request_handler(_ctx, "/getperfstats", GetPerfStats, 0); #ifndef SIMULATOR mg_set_request_handler(_ctx, "/systemctl", SystemCtl, 0); mg_set_request_handler(_ctx, "/getprocessstatus", GetProcessStatus, 0); mg_set_request_handler(_ctx, "/processstatus", ProcessStatus, 0); #endif // todo (VIC-1398): remove if( ANKI_DEV_CHEATS ) { mg_set_request_handler(_ctx, "/sendAppMessage", TempAppToEngine, 0); mg_set_request_handler(_ctx, "/getAppMessages", TempEngineToApp, 0); } const std::string& consoleVarsTemplate = platform->pathToResource(Util::Data::Scope::Resources, "webserver/consolevarsui.html"); _consoleVarsUIHTMLTemplate = Anki::Util::StringFromContentsOfFile(consoleVarsTemplate); _requests.clear(); _dispatchQueue = Util::Dispatch::Create("WebsocketSender"); } // This is called from the main thread void WebService::Update() { ANKI_CPU_PROFILE("WebService::Update"); std::lock_guard lock(_requestMutex); // First pass: Delete any completely-finished requests from the list (and delete the requests themselves) size_t destIndex = 0; for (size_t srcIndex = 0; srcIndex < _requests.size(); srcIndex++) { Request* requestPtr = _requests[srcIndex]; if (requestPtr->_done) { delete requestPtr; } else { if (srcIndex != destIndex) { _requests[destIndex] = _requests[srcIndex]; } destIndex++; } } _requests.resize(destIndex); Anki::Util::ConsoleSystem& consoleSystem = Anki::Util::ConsoleSystem::Instance(); // Second pass: Process any requests that haven't been processed yet for (const auto requestPtr : _requests) { if (!requestPtr->_resultReady) { switch (requestPtr->_requestType) { case RT_ConsoleVarsUI: { const bool standalone = (requestPtr->_param2 == "standalone"); GenerateConsoleVarsUI(requestPtr->_result, requestPtr->_param1, standalone); } break; case RT_ConsoleVarGet: { const std::string& key = requestPtr->_param1; if (!key.empty()) { const Anki::Util::IConsoleVariable* consoleVar = consoleSystem.FindVariable(key.c_str()); if (consoleVar) { requestPtr->_result = consoleVar->ToString() + "
"; } else { requestPtr->_result = "Variable not found
"; } } else { requestPtr->_result = "Key required (name of variable)
"; } } break; case RT_ConsoleVarSet: { std::string key = requestPtr->_param1; while (!key.empty()) { if (key.substr(0, 4) == "key=") { std::string value; std::string remainder; size_t amp = key.find('&'); if (amp != std::string::npos) { value = key.substr(amp+7); key = key.substr(4, amp-4); size_t amp = value.find('&'); if (amp != std::string::npos) { remainder = value.substr(amp+1); value = value.substr(0, amp); } } Anki::Util::IConsoleVariable* consoleVar = consoleSystem.FindVariable(key.c_str()); if (consoleVar) { if (consoleVar->ParseText(value.c_str() )) { // success LOG_INFO("WebService", "CONSOLE_VAR %s %s", key.c_str(), value.c_str()); requestPtr->_result += consoleVar->ToString() + "
"; } else { requestPtr->_result += "Error setting variable "+key+"="+value+"
"; } } else { requestPtr->_result += "Variable not found "+key+"
"; } key = remainder; } else { break; } } } break; case RT_ConsoleVarList: { const std::string& key = requestPtr->_param1; const auto keyLen = key.length(); const Anki::Util::ConsoleSystem::VariableDatabase& varDatabase = consoleSystem.GetVariableDatabase(); for (Anki::Util::ConsoleSystem::VariableDatabase::const_iterator it = varDatabase.begin(); it != varDatabase.end(); ++it) { std::string label = it->second->GetID(); if (keyLen == 0 || Anki::Util::StringCaseInsensitiveEquals(label.substr(0, keyLen), key)) { requestPtr->_result += label.c_str(); requestPtr->_result += "
\n"; } } } break; case RT_ConsoleFuncList: { const std::string& key = requestPtr->_param1; const auto keyLen = key.length(); const Anki::Util::ConsoleSystem::FunctionDatabase& funcDatabase = consoleSystem.GetFunctionDatabase(); for (Anki::Util::ConsoleSystem::FunctionDatabase::const_iterator it = funcDatabase.begin(); it != funcDatabase.end(); ++it) { std::string label = it->second->GetID(); if (keyLen == 0 || Anki::Util::StringCaseInsensitiveEquals(label.substr(0, keyLen), key)) { requestPtr->_result += label.c_str(); requestPtr->_result += "
\n"; } } } break; case RT_ConsoleFuncCall: { const std::string& func = requestPtr->_param1; const std::string& args = requestPtr->_param2; Anki::Util::IConsoleFunction* consoleFunc = consoleSystem.FindFunction(func.c_str()); if (consoleFunc) { char outText[kBigBufferSize + 1] = {0}; uint32_t outTextLength = sizeof(outText); ExternalOnlyConsoleChannel consoleChannel(outText, outTextLength); bool success = consoleSystem.ParseConsoleFunctionCall(consoleFunc, args.c_str(), consoleChannel); if (success) { LOG_INFO("WebService.FuncCallSuccess", "CONSOLE_FUNC %s %s success", func.c_str(), args.c_str()); requestPtr->_result += outText; } else { LOG_INFO("WebService.FuncCallFailure", "CONSOLE_FUNC %s %s failed %s", func.c_str(), args.c_str(), outText); requestPtr->_result += outText; } } else { LOG_INFO("WebService.FuncCallNotFound", "CONSOLE_FUNC %s %s not found", func.c_str(), args.c_str()); } } break; case RT_External: { // Call out to the external update handler DEV_ASSERT(requestPtr->_externalCallback != nullptr, "Expecting valid externalCallback pointer"); const int returnCode = requestPtr->_externalCallback(requestPtr); if (returnCode == 0) { LOG_INFO("WebService.Update", "External callback failed"); } } break; case RT_TempAppToEngine: { _appToEngineOnData.emit( requestPtr->_param1 ); } break; case RT_TempEngineToApp: { requestPtr->_result = _appToEngineRequestData.emit(); } break; case RT_WebsocketOnSubscribe: case RT_WebsocketOnData: { const auto& moduleName = requestPtr->_param1; const auto& idxStr = requestPtr->_param2; const size_t idx = std::stoi( idxStr ); auto sendToClient = [idx, moduleName, this](const Json::Value& toSend){ std::lock_guard lock(s_wsConnectionsMutex); if( (idx < _webSocketConnections.size()) && (_webSocketConnections[idx].subscribedModules.count( moduleName ) > 0) ) { Json::Value payload; payload["module"] = moduleName; payload["data"] = toSend; SendToWebSocket( _webSocketConnections[idx].conn, payload ); } }; if( requestPtr->_requestType == RT_WebsocketOnSubscribe ) { auto signalIt = _webVizSubscribedSignals.find( moduleName ); if( signalIt != _webVizSubscribedSignals.end() ) { signalIt->second.emit( sendToClient ); } } else if( requestPtr->_requestType == RT_WebsocketOnData ) { const auto& dataStr = requestPtr->_param3; Json::Reader reader; Json::Value data; if( reader.parse(dataStr, data) ) { auto signalIt = _webVizDataSignals.find( moduleName ); if( signalIt != _webVizDataSignals.end() ) { signalIt->second.emit( data, sendToClient ); } } } requestPtr->_done = true; // no one cares about the result, just cleanup immediately } break; } // Notify the requesting thread that the result is now ready { std::unique_lock lk{requestPtr->_readyMutex}; requestPtr->_resultReady = true; } requestPtr->_readyCondition.notify_all(); } } } void WebService::Stop() { s_ShuttingDown = true; if (_ctx) { // Call update to process any pending request(s) and wake up the // thread(s) that are waiting for those request(s) to be processed. // This will allow the mg_stop call below to not take forever waiting // for threads to shut down. Update(); #ifndef SIMULATOR // Notify any pending thread that's waiting for process status, so that // the mg_stop call below will not hang waiting for it { std::unique_lock lk{s_ProcessStatusMutex}; s_ProcessStatuses.clear(); s_WaitingForProcessStatus = false; } s_ProcessStatusCondition.notify_all(); #endif #ifdef VICOS // shutdown nicely on the robot but let the OS handle it for the simulator, mg_stop triggers // the thread sanitizer and execution stops here, by removing this line in SIMULATOR builds // it allows the thread sanitizier to continue to do useful work. mg_stop(_ctx); #endif _ctx = nullptr; } } void WebService::AddRequest(Request* requestPtr) { std::lock_guard lock(_requestMutex); _requests.push_back(requestPtr); } void WebService::RegisterRequestHandler(std::string uri, mg_request_handler handler, void* cbdata) { mg_set_request_handler(_ctx, uri.c_str(), handler, cbdata); } int WebService::ProcessRequestExternal(struct mg_connection* conn, void* cbdata, ExternalCallback extCallback, const std::string& param1, const std::string& param2, const std::string& param3) { // This is a request coming from an 'external' handler that wants WebService // to process the request at the end of the tick (in WebService::Update) static const bool waitAndSendResponse = true; return ProcessRequest(conn, WebService::WebService::RequestType::RT_External, param1, param2, param3, waitAndSendResponse, extCallback, cbdata); } static std::string sanitize_tag(const std::string& tag) { std::string sanitizedTag = tag; for(int i = 0; i < sanitizedTag.size(); ++i) { if (!isalpha(sanitizedTag[i])) { sanitizedTag[i] = '_'; } } return sanitizedTag; } void WebService::GenerateConsoleVarsUI(std::string& page, const std::string& category, const bool standalone) { ANKI_CPU_PROFILE("GenerateConsoleVarsUI"); std::string styleSheetIncludes; std::string jqueryIncludes; std::string style; std::string script; std::string html; std::map category_html; const Anki::Util::ConsoleSystem& consoleSystem = Anki::Util::ConsoleSystem::Instance(); if (standalone) { styleSheetIncludes += "\n"; styleSheetIncludes += "\n"; jqueryIncludes += "\n"; jqueryIncludes += "\n"; } // Variables const Anki::Util::ConsoleSystem::VariableDatabase& varDatabase = consoleSystem.GetVariableDatabase(); for (Anki::Util::ConsoleSystem::VariableDatabase::const_iterator it = varDatabase.begin(); it != varDatabase.end(); ++it) { std::string cat = it->second->GetCategory(); if (!category.empty() && category != cat) { continue; } std::string label = it->second->GetID(); if (it->second->IsToggleable()) { category_html[cat] += "
\n"; category_html[cat] += " \n"; const std::string checked = (it->second->GetAsInt64()) ? " checked" : ""; category_html[cat] += " \n"; category_html[cat] += "
\n"; category_html[cat] += "
\n"; } else if (it->second->IsEnumType()) { category_html[cat] += "
\n"; category_html[cat] += " \n"; category_html[cat] += " \n"; category_html[cat] += "
\n"; category_html[cat] += "
\n"; } else { char sliderRange[200]; char inputRange[200]; if (it->second->IsIntegerType()) { if (it->second->IsSignedType()) { snprintf(sliderRange, sizeof(sliderRange), "data-value=\"%lld\" data-begin=\"%lld\" data-end=\"%lld\" data-scale=\"1\"", it->second->GetAsInt64(), it->second->GetMinAsInt64(), it->second->GetMaxAsInt64()); snprintf(inputRange, sizeof(inputRange), "min=\"%lld\" max=\"%lld\"", it->second->GetMinAsInt64(), it->second->GetMaxAsInt64()); } else { snprintf(sliderRange, sizeof(sliderRange), "data-value=\"%llu\" data-begin=\"%llu\" data-end=\"%llu\" data-scale=\"1\"", it->second->GetAsUInt64(), it->second->GetMinAsUInt64(), it->second->GetMaxAsUInt64()); snprintf(inputRange, sizeof(inputRange), "min=\"%llu\" max=\"%llu\"", it->second->GetMinAsUInt64(), it->second->GetMaxAsUInt64()); } } else { snprintf(sliderRange, sizeof(sliderRange), "data-value=\"%g\" data-begin=\"%g\" data-end=\"%g\" data-scale=\"100.0\"", it->second->GetAsDouble(), it->second->GetMinAsDouble(), it->second->GetMaxAsDouble()); snprintf(inputRange, sizeof(inputRange), "min=\"%g\" max=\"%g\"", it->second->GetMinAsDouble(), it->second->GetMaxAsDouble()); } category_html[cat] += "
\n"; category_html[cat] += " \n"; category_html[cat] += "
\n"; category_html[cat] += " \n"; category_html[cat] += "

\n"; } } // Functions const Anki::Util::ConsoleSystem::FunctionDatabase& funcDatabase = consoleSystem.GetFunctionDatabase(); for (Anki::Util::ConsoleSystem::FunctionDatabase::const_iterator it = funcDatabase.begin(); it != funcDatabase.end(); ++it) { std::string cat = it->second->GetCategory(); if (!category.empty() && category != cat) { continue; } std::string label = it->second->GetID(); std::string sig = it->second->GetSignature(); if (sig.empty()) { category_html[cat] += "
\n"; category_html[cat] += " \n"; category_html[cat] += "

\n"; } else { category_html[cat] += "
\n"; category_html[cat] += " \n"; category_html[cat] += " \n"; category_html[cat] += " \n"; category_html[cat] += "

\n"; } } // merge all categories, categories without subcategories std::map category_merged_html; for (std::map::const_iterator it = category_html.begin(); it != category_html.end(); ++it) { std::string cat = it->first; size_t dot = cat.find("."); if (dot == std::string::npos) { category_merged_html[cat] += it->second; } } // then, append subcategories within
for (std::map::const_iterator it = category_html.begin(); it != category_html.end(); ++it) { std::string cat = it->first; size_t dot = cat.find("."); if (dot != std::string::npos) { std::string group = cat.substr(dot+1); cat = cat.substr(0, dot); category_merged_html[cat] += "
\n"; category_merged_html[cat] += " "+group+"\n"; category_merged_html[cat] += it->second; category_merged_html[cat] += "
\n"; } } // finally, generate tabs html += "
\n"; html += "
    \n"; for (std::map::const_iterator it = category_merged_html.begin(); it != category_merged_html.end(); ++it) { html += "
  • first)+"\">"+it->first+"
  • \n"; } html += "
\n"; for (std::map::const_iterator it = category_merged_html.begin(); it != category_merged_html.end(); ++it) { std::string tag = it->first; html += "
first)+"\">\n"; html += " " + it->second+"\n"; html += "
\n"; } html += "
\n"; page = getConsoleVarsTemplate(); std::string tmp; size_t pos; tmp = "/* -- generated stylesheet includes -- */"; pos = page.find(tmp); if (pos != std::string::npos) { page = page.replace(pos, tmp.length(), styleSheetIncludes); } tmp = "/* -- generated jquery includes -- */"; pos = page.find(tmp); if (pos != std::string::npos) { page = page.replace(pos, tmp.length(), jqueryIncludes); } tmp = "/* -- generated style -- */"; pos = page.find(tmp); if (pos != std::string::npos) { page = page.replace(pos, tmp.length(), style); } tmp = "// -- generated script --"; pos = page.find(tmp); if (pos != std::string::npos) { page = page.replace(pos, tmp.length(), script); } tmp = ""; pos = page.find(tmp); if (pos != std::string::npos) { page = page.replace(pos, tmp.length(), html); } } void WebService::SendToWebSockets(const std::string& moduleName, const Json::Value& data) const { std::lock_guard lock(s_wsConnectionsMutex); Json::Value payload; bool hasAssigned = false; // don't copy payload unless there is >= 1 client for this module for( const auto& connData : _webSocketConnections ) { if( connData.subscribedModules.find( moduleName ) != connData.subscribedModules.end() ) { if( !hasAssigned ) { payload["module"] = moduleName; payload["data"] = data; hasAssigned = true; } SendToWebSocket(connData.conn, payload); } } } bool WebService::IsWebVizClientSubscribed(const std::string& moduleName) const { std::lock_guard lock(s_wsConnectionsMutex); for( const auto& connData : _webSocketConnections ) { if( (connData.subscribedModules.find( moduleName ) != connData.subscribedModules.end()) || (moduleName.empty() && !connData.subscribedModules.empty()) ) // any module subscribed { return true; } } return false; } int WebService::HandleWebSocketsConnect(const struct mg_connection* conn, void* cbparams) { return 0; // proceed with connection } void WebService::HandleWebSocketsReady(struct mg_connection* conn, void* cbparams) { struct mg_context* ctx = mg_get_context(conn); Anki::Vector::WebService::WebService* that = static_cast(mg_get_user_data(ctx)); DEV_ASSERT(that != nullptr, "Expecting valid webservice this pointer"); that->OnOpenWebSocket( conn ); } int WebService::HandleWebSocketsData(struct mg_connection* conn, int bits, char* data, size_t dataLen, void* cbparams) { int ret = 1; // keep open // lower 4 bits const int opcode = bits & 0xF; // see websocket RFC ยง5.2 http://tools.ietf.org/html/rfc6455 switch (opcode) { case WebSocketsTypeText: { if( (dataLen >= 2) && (data[0] == '{') ) { struct mg_context* ctx = mg_get_context(conn); Anki::Vector::WebService::WebService* that = static_cast(mg_get_user_data(ctx)); DEV_ASSERT(that != nullptr, "Expecting valid webservice this pointer"); Json::Reader reader; Json::Value payload; bool success = reader.parse(data, payload); if( success ) { that->OnReceiveWebSocket( conn, payload ); } } } break; case WebSocketsTypeCloseConnection: { // agree to close connection, but don't do anything here until the close event fires ret = 0; } break; default: break; } return ret; } void WebService::HandleWebSocketsClose(const struct mg_connection* conn, void* cbparams) { struct mg_context* ctx = mg_get_context(conn); Anki::Vector::WebService::WebService* that = static_cast(mg_get_user_data(ctx)); DEV_ASSERT(that != nullptr, "Expecting valid webservice this pointer"); that->OnCloseWebSocket( conn ); } // This is always called in the main thread (whether we're sending or receiving) void WebService::SendToWebSocket(struct mg_connection* conn, const Json::Value& data) const { // Dispatch work onto another thread (note we copy 'data' by value here) Util::Dispatch::Async(_dispatchQueue, [conn, data] { std::stringstream ss; ss << data; const std::string& str = ss.str(); mg_websocket_write(conn, WebSocketsTypeText, str.c_str(), str.size()); }); } const std::string& WebService::getConsoleVarsTemplate() { return _consoleVarsUIHTMLTemplate; } void WebService::OnOpenWebSocket(struct mg_connection* conn) { ASSERT_NAMED(conn != nullptr, "Can't create connection to n"); // add a connection to the list that applies to all services std::lock_guard lock(s_wsConnectionsMutex); _webSocketConnections.push_back({}); _webSocketConnections.back().conn = conn; } void WebService::OnReceiveWebSocket(struct mg_connection* conn, const Json::Value& data) { std::lock_guard lock(s_wsConnectionsMutex); // find connection auto it = std::find_if( _webSocketConnections.begin(), _webSocketConnections.end(), [&conn](const auto& perConnData) { return perConnData.conn == conn; }); if( it != _webSocketConnections.end() ) { if( !data["type"].isNull() && !data["module"].isNull() ) { const std::string& moduleName = data["module"].asString(); const size_t idx = it - _webSocketConnections.begin(); if( data["type"].asString() == "subscribe" ) { it->subscribedModules.insert( moduleName ); const bool waitAndSendResponse = false; ProcessRequest(conn, WebService::WebService::RequestType::RT_WebsocketOnSubscribe, moduleName, std::to_string(idx), "", waitAndSendResponse); } else if( data["type"].asString() == "unsubscribe" ) { it->subscribedModules.erase( moduleName ); } else if( (data["type"].asString() == "data") && !data["data"].isNull() ) { const bool waitAndSendResponse = false; std::stringstream ss; ss << data["data"]; ProcessRequest(conn, WebService::WebService::RequestType::RT_WebsocketOnData, moduleName, std::to_string(idx), ss.str(), waitAndSendResponse); } } else if( !data["keepalive"].isNull() ) { Json::Value response; response["keepalive"] = true; SendToWebSocket( it->conn, response ); } } else { std::stringstream ss; ss << data; LOG_ERROR("Webservice.OnReceiveWebSocket", "No connection for data %s", ss.str().c_str()); } } void WebService::OnCloseWebSocket(const struct mg_connection* conn) { std::lock_guard lock(s_wsConnectionsMutex); // find connection auto it = std::find_if( _webSocketConnections.begin(), _webSocketConnections.end(), [&conn](const auto& perConnData) { return perConnData.conn == conn; }); // erase it auto& data = *it; std::swap(data, _webSocketConnections.back()); _webSocketConnections.pop_back(); } } // namespace WebService } // namespace Vector } // namespace Anki #else namespace Anki { namespace Vector { namespace WebService { WebService::WebService() { } WebService::~WebService() { } void WebService::Start(Anki::Util::Data::DataPlatform* /*platform*/, const Json::Value& /*config*/) { } void WebService::Update() { } void WebService::Stop() { } void WebService::SendToWebSockets(const std::string& /*moduleName*/, const Json::Value& /*data*/) const { } bool WebService::IsWebVizClientSubscribed(const std::string& /*moduleName*/) const { return false; } void WebService::RegisterRequestHandler(std::string /*uri*/, mg_request_handler /*handler*/, void* /*cbdata*/) { } int WebService::ProcessRequestExternal(struct mg_connection* /*conn*/, void* /*cbdata*/, ExternalCallback /*extCallback*/, const std::string& /*param1*/, const std::string& /*param2*/, const std::string& /*param3*/) { return 1; } } // namespace WebService } // namespace Vector } // namespace Anki #endif // ANKI_NO_WEBSERVER_ENABLED